4eb3b8
From 1917af220242840ec1b21f82f80532cf6548cc00 Mon Sep 17 00:00:00 2001
41c250
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
4eb3b8
Date: Fri, 14 Jan 2022 16:34:49 +0100
41c250
Subject: [PATCH 2/6] Datasource for VMware (#953)
41c250
41c250
RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
4eb3b8
RH-MergeRequest: 44: Datasource for VMware
4eb3b8
RH-Commit: [2/6] bb6e58dfeaf8b64d2801ddb4cb73868cf31de3ef
4eb3b8
RH-Bugzilla: 2026587
41c250
RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
41c250
RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
41c250
41c250
commit 8b4a9bc7b81e61943af873bad92e2133f8275b0b
41c250
Author: Andrew Kutz <101085+akutz@users.noreply.github.com>
41c250
Date:   Mon Aug 9 21:24:07 2021 -0500
41c250
41c250
    Datasource for VMware (#953)
41c250
41c250
    This patch finally introduces the Cloud-Init Datasource for VMware
41c250
    GuestInfo as a part of cloud-init proper. This datasource has existed
41c250
    since 2018, and rapidly became the de facto datasource for developers
41c250
    working with Packer, Terraform, for projects like kube-image-builder,
41c250
    and the de jure datasource for Photon OS.
41c250
41c250
    The major change to the datasource from its previous incarnation is
41c250
    the name. Now named DatasourceVMware, this new version of the
41c250
    datasource will allow multiple transport types in addition to
41c250
    GuestInfo keys.
41c250
41c250
    This datasource includes several unique features developed to address
41c250
    real-world situations:
41c250
41c250
      * Support for reading any key (metadata, userdata, vendordata) both
41c250
        from the guestinfo table when running on a VM in vSphere as well as
41c250
        from an environment variable when running inside of a container,
41c250
        useful for rapid dev/test.
41c250
41c250
      * Allows booting with DHCP while still providing full participation
41c250
        in Cloud-Init instance data and Jinja queries. The netifaces library
41c250
        provides the ability to inspect the network after it is online,
41c250
        and the runtime network configuration is then merged into the
41c250
        existing metadata and persisted to disk.
41c250
41c250
      * Advertises the local_ipv4 and local_ipv6 addresses via guestinfo
41c250
        as well. This is useful as Guest Tools is not always able to
41c250
        identify what would be considered the local address.
41c250
41c250
    The primary author and current steward of this datasource spoke at
41c250
    Cloud-Init Con 2020 where there was interest in contributing this datasource
41c250
    to the Cloud-Init codebase.
41c250
41c250
    The datasource currently lives in its own GitHub repository at
41c250
    https://github.com/vmware/cloud-init-vmware-guestinfo. Once the datasource
41c250
    is merged into Cloud-Init, the old repository will be deprecated.
41c250
41c250
Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
41c250
---
41c250
 README.md                                     |   2 +-
41c250
 cloudinit/settings.py                         |   1 +
41c250
 cloudinit/sources/DataSourceVMware.py         | 871 ++++++++++++++++++
41c250
 doc/rtd/topics/availability.rst               |   1 +
41c250
 doc/rtd/topics/datasources.rst                |   2 +-
41c250
 doc/rtd/topics/datasources/vmware.rst         | 359 ++++++++
41c250
 requirements.txt                              |   9 +
41c250
 .../unittests/test_datasource/test_common.py  |   3 +
41c250
 .../unittests/test_datasource/test_vmware.py  | 377 ++++++++
41c250
 tests/unittests/test_ds_identify.py           | 279 +++++-
41c250
 tools/.github-cla-signers                     |   1 +
41c250
 tools/ds-identify                             |  76 +-
41c250
 12 files changed, 1977 insertions(+), 4 deletions(-)
41c250
 create mode 100644 cloudinit/sources/DataSourceVMware.py
41c250
 create mode 100644 doc/rtd/topics/datasources/vmware.rst
41c250
 create mode 100644 tests/unittests/test_datasource/test_vmware.py
41c250
41c250
diff --git a/README.md b/README.md
4eb3b8
index 435405da..aa4fad63 100644
41c250
--- a/README.md
41c250
+++ b/README.md
41c250
@@ -39,7 +39,7 @@ get in contact with that distribution and send them our way!
41c250
 
41c250
 | Supported OSes | Supported Public Clouds | Supported Private Clouds |
41c250
 | --- | --- | --- |
41c250
-| Alpine Linux
ArchLinux
Debian
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
Digital Ocean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















|
4eb3b8
+| Alpine Linux
ArchLinux
Debian
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
Digital Ocean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)
VMware















|
41c250
 
41c250
 ## To start developing cloud-init
41c250
 
41c250
diff --git a/cloudinit/settings.py b/cloudinit/settings.py
41c250
index 2acf2615..d5f32dbb 100644
41c250
--- a/cloudinit/settings.py
41c250
+++ b/cloudinit/settings.py
41c250
@@ -42,6 +42,7 @@ CFG_BUILTIN = {
41c250
         'Exoscale',
41c250
         'RbxCloud',
41c250
         'UpCloud',
41c250
+        'VMware',
41c250
         # At the end to act as a 'catch' when none of the above work...
41c250
         'None',
41c250
     ],
41c250
diff --git a/cloudinit/sources/DataSourceVMware.py b/cloudinit/sources/DataSourceVMware.py
41c250
new file mode 100644
41c250
index 00000000..22ca63de
41c250
--- /dev/null
41c250
+++ b/cloudinit/sources/DataSourceVMware.py
41c250
@@ -0,0 +1,871 @@
41c250
+# Cloud-Init DataSource for VMware
41c250
+#
41c250
+# Copyright (c) 2018-2021 VMware, Inc. All Rights Reserved.
41c250
+#
41c250
+# Authors: Anish Swaminathan <anishs@vmware.com>
41c250
+#          Andrew Kutz <akutz@vmware.com>
41c250
+#
41c250
+# This file is part of cloud-init. See LICENSE file for license information.
41c250
+
41c250
+"""Cloud-Init DataSource for VMware
41c250
+
41c250
+This module provides a cloud-init datasource for VMware systems and supports
41c250
+multiple transports types, including:
41c250
+
41c250
+    * EnvVars
41c250
+    * GuestInfo
41c250
+
41c250
+Netifaces (https://github.com/al45tair/netifaces)
41c250
+
41c250
+    Please note this module relies on the netifaces project to introspect the
41c250
+    runtime, network configuration of the host on which this datasource is
41c250
+    running. This is in contrast to the rest of cloud-init which uses the
41c250
+    cloudinit/netinfo module.
41c250
+
41c250
+    The reasons for using netifaces include:
41c250
+
41c250
+        * Netifaces is built in C and is more portable across multiple systems
41c250
+          and more deterministic than shell exec'ing local network commands and
41c250
+          parsing their output.
41c250
+
41c250
+        * Netifaces provides a stable way to determine the view of the host's
41c250
+          network after DHCP has brought the network online. Unlike most other
41c250
+          datasources, this datasource still provides support for JINJA queries
41c250
+          based on networking information even when the network is based on a
41c250
+          DHCP lease. While this does not tie this datasource directly to
41c250
+          netifaces, it does mean the ability to consistently obtain the
41c250
+          correct information is paramount.
41c250
+
41c250
+        * It is currently possible to execute this datasource on macOS
41c250
+          (which many developers use today) to print the output of the
41c250
+          get_host_info function. This function calls netifaces to obtain
41c250
+          the same runtime network configuration that the datasource would
41c250
+          persist to the local system's instance data.
41c250
+
41c250
+          However, the netinfo module fails on macOS. The result is either a
41c250
+          hung operation that requires a SIGINT to return control to the user,
41c250
+          or, if brew is used to install iproute2mac, the ip commands are used
41c250
+          but produce output the netinfo module is unable to parse.
41c250
+
41c250
+          While macOS is not a target of cloud-init, this feature is quite
41c250
+          useful when working on this datasource.
41c250
+
41c250
+          For more information about this behavior, please see the following
41c250
+          PR comment, https://bit.ly/3fG7OVh.
41c250
+
41c250
+    The authors of this datasource are not opposed to moving away from
41c250
+    netifaces. The goal may be to eventually do just that. This proviso was
41c250
+    added to the top of this module as a way to remind future-us and others
41c250
+    why netifaces was used in the first place in order to either smooth the
41c250
+    transition away from netifaces or embrace it further up the cloud-init
41c250
+    stack.
41c250
+"""
41c250
+
41c250
+import collections
41c250
+import copy
41c250
+from distutils.spawn import find_executable
41c250
+import ipaddress
41c250
+import json
41c250
+import os
41c250
+import socket
41c250
+import time
41c250
+
41c250
+from cloudinit import dmi, log as logging
41c250
+from cloudinit import sources
41c250
+from cloudinit import util
41c250
+from cloudinit.subp import subp, ProcessExecutionError
41c250
+
41c250
+import netifaces
41c250
+
41c250
+
41c250
+PRODUCT_UUID_FILE_PATH = "/sys/class/dmi/id/product_uuid"
41c250
+
41c250
+LOG = logging.getLogger(__name__)
41c250
+NOVAL = "No value found"
41c250
+
41c250
+DATA_ACCESS_METHOD_ENVVAR = "envvar"
41c250
+DATA_ACCESS_METHOD_GUESTINFO = "guestinfo"
41c250
+
41c250
+VMWARE_RPCTOOL = find_executable("vmware-rpctool")
41c250
+REDACT = "redact"
41c250
+CLEANUP_GUESTINFO = "cleanup-guestinfo"
41c250
+VMX_GUESTINFO = "VMX_GUESTINFO"
41c250
+GUESTINFO_EMPTY_YAML_VAL = "---"
41c250
+
41c250
+LOCAL_IPV4 = "local-ipv4"
41c250
+LOCAL_IPV6 = "local-ipv6"
41c250
+WAIT_ON_NETWORK = "wait-on-network"
41c250
+WAIT_ON_NETWORK_IPV4 = "ipv4"
41c250
+WAIT_ON_NETWORK_IPV6 = "ipv6"
41c250
+
41c250
+
41c250
+class DataSourceVMware(sources.DataSource):
41c250
+    """
41c250
+    Setting the hostname:
41c250
+        The hostname is set by way of the metadata key "local-hostname".
41c250
+
41c250
+    Setting the instance ID:
41c250
+        The instance ID may be set by way of the metadata key "instance-id".
41c250
+        However, if this value is absent then the instance ID is read
41c250
+        from the file /sys/class/dmi/id/product_uuid.
41c250
+
41c250
+    Configuring the network:
41c250
+        The network is configured by setting the metadata key "network"
41c250
+        with a value consistent with Network Config Versions 1 or 2,
41c250
+        depending on the Linux distro's version of cloud-init:
41c250
+
41c250
+            Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1
41c250
+            Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2
41c250
+
41c250
+        For example, CentOS 7's official cloud-init package is version
41c250
+        0.7.9 and does not support Network Config Version 2. However,
41c250
+        this datasource still supports supplying Network Config Version 2
41c250
+        data as long as the Linux distro's cloud-init package is new
41c250
+        enough to parse the data.
41c250
+
41c250
+        The metadata key "network.encoding" may be used to indicate the
41c250
+        format of the metadata key "network". Valid encodings are base64
41c250
+        and gzip+base64.
41c250
+    """
41c250
+
41c250
+    dsname = "VMware"
41c250
+
41c250
+    def __init__(self, sys_cfg, distro, paths, ud_proc=None):
41c250
+        sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
41c250
+
41c250
+        self.data_access_method = None
41c250
+        self.vmware_rpctool = VMWARE_RPCTOOL
41c250
+
41c250
+    def _get_data(self):
41c250
+        """
41c250
+        _get_data loads the metadata, userdata, and vendordata from one of
41c250
+        the following locations in the given order:
41c250
+
41c250
+            * envvars
41c250
+            * guestinfo
41c250
+
41c250
+        Please note when updating this function with support for new data
41c250
+        transports, the order should match the order in the dscheck_VMware
41c250
+        function from the file ds-identify.
41c250
+        """
41c250
+
41c250
+        # Initialize the locally scoped metadata, userdata, and vendordata
41c250
+        # variables. They are assigned below depending on the detected data
41c250
+        # access method.
41c250
+        md, ud, vd = None, None, None
41c250
+
41c250
+        # First check to see if there is data via env vars.
41c250
+        if os.environ.get(VMX_GUESTINFO, ""):
41c250
+            md = guestinfo_envvar("metadata")
41c250
+            ud = guestinfo_envvar("userdata")
41c250
+            vd = guestinfo_envvar("vendordata")
41c250
+
41c250
+            if md or ud or vd:
41c250
+                self.data_access_method = DATA_ACCESS_METHOD_ENVVAR
41c250
+
41c250
+        # At this point, all additional data transports are valid only on
41c250
+        # a VMware platform.
41c250
+        if not self.data_access_method:
41c250
+            system_type = dmi.read_dmi_data("system-product-name")
41c250
+            if system_type is None:
41c250
+                LOG.debug("No system-product-name found")
41c250
+                return False
41c250
+            if "vmware" not in system_type.lower():
41c250
+                LOG.debug("Not a VMware platform")
41c250
+                return False
41c250
+
41c250
+        # If no data was detected, check the guestinfo transport next.
41c250
+        if not self.data_access_method:
41c250
+            if self.vmware_rpctool:
41c250
+                md = guestinfo("metadata", self.vmware_rpctool)
41c250
+                ud = guestinfo("userdata", self.vmware_rpctool)
41c250
+                vd = guestinfo("vendordata", self.vmware_rpctool)
41c250
+
41c250
+                if md or ud or vd:
41c250
+                    self.data_access_method = DATA_ACCESS_METHOD_GUESTINFO
41c250
+
41c250
+        if not self.data_access_method:
41c250
+            LOG.error("failed to find a valid data access method")
41c250
+            return False
41c250
+
41c250
+        LOG.info("using data access method %s", self._get_subplatform())
41c250
+
41c250
+        # Get the metadata.
41c250
+        self.metadata = process_metadata(load_json_or_yaml(md))
41c250
+
41c250
+        # Get the user data.
41c250
+        self.userdata_raw = ud
41c250
+
41c250
+        # Get the vendor data.
41c250
+        self.vendordata_raw = vd
41c250
+
41c250
+        # Redact any sensitive information.
41c250
+        self.redact_keys()
41c250
+
41c250
+        # get_data returns true if there is any available metadata,
41c250
+        # userdata, or vendordata.
41c250
+        if self.metadata or self.userdata_raw or self.vendordata_raw:
41c250
+            return True
41c250
+        else:
41c250
+            return False
41c250
+
41c250
+    def setup(self, is_new_instance):
41c250
+        """setup(is_new_instance)
41c250
+
41c250
+        This is called before user-data and vendor-data have been processed.
41c250
+
41c250
+        Unless the datasource has set mode to 'local', then networking
41c250
+        per 'fallback' or per 'network_config' will have been written and
41c250
+        brought up the OS at this point.
41c250
+        """
41c250
+
41c250
+        host_info = wait_on_network(self.metadata)
41c250
+        LOG.info("got host-info: %s", host_info)
41c250
+
41c250
+        # Reflect any possible local IPv4 or IPv6 addresses in the guest
41c250
+        # info.
41c250
+        advertise_local_ip_addrs(host_info)
41c250
+
41c250
+        # Ensure the metadata gets updated with information about the
41c250
+        # host, including the network interfaces, default IP addresses,
41c250
+        # etc.
41c250
+        self.metadata = util.mergemanydict([self.metadata, host_info])
41c250
+
41c250
+        # Persist the instance data for versions of cloud-init that support
41c250
+        # doing so. This occurs here rather than in the get_data call in
41c250
+        # order to ensure that the network interfaces are up and can be
41c250
+        # persisted with the metadata.
41c250
+        self.persist_instance_data()
41c250
+
41c250
+    def _get_subplatform(self):
41c250
+        get_key_name_fn = None
41c250
+        if self.data_access_method == DATA_ACCESS_METHOD_ENVVAR:
41c250
+            get_key_name_fn = get_guestinfo_envvar_key_name
41c250
+        elif self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO:
41c250
+            get_key_name_fn = get_guestinfo_key_name
41c250
+        else:
41c250
+            return sources.METADATA_UNKNOWN
41c250
+
41c250
+        return "%s (%s)" % (
41c250
+            self.data_access_method,
41c250
+            get_key_name_fn("metadata"),
41c250
+        )
41c250
+
41c250
+    @property
41c250
+    def network_config(self):
41c250
+        if "network" in self.metadata:
41c250
+            LOG.debug("using metadata network config")
41c250
+        else:
41c250
+            LOG.debug("using fallback network config")
41c250
+            self.metadata["network"] = {
41c250
+                "config": self.distro.generate_fallback_config(),
41c250
+            }
41c250
+        return self.metadata["network"]["config"]
41c250
+
41c250
+    def get_instance_id(self):
41c250
+        # Pull the instance ID out of the metadata if present. Otherwise
41c250
+        # read the file /sys/class/dmi/id/product_uuid for the instance ID.
41c250
+        if self.metadata and "instance-id" in self.metadata:
41c250
+            return self.metadata["instance-id"]
41c250
+        with open(PRODUCT_UUID_FILE_PATH, "r") as id_file:
41c250
+            self.metadata["instance-id"] = str(id_file.read()).rstrip().lower()
41c250
+            return self.metadata["instance-id"]
41c250
+
41c250
+    def get_public_ssh_keys(self):
41c250
+        for key_name in (
41c250
+            "public-keys-data",
41c250
+            "public_keys_data",
41c250
+            "public-keys",
41c250
+            "public_keys",
41c250
+        ):
41c250
+            if key_name in self.metadata:
41c250
+                return sources.normalize_pubkey_data(self.metadata[key_name])
41c250
+        return []
41c250
+
41c250
+    def redact_keys(self):
41c250
+        # Determine if there are any keys to redact.
41c250
+        keys_to_redact = None
41c250
+        if REDACT in self.metadata:
41c250
+            keys_to_redact = self.metadata[REDACT]
41c250
+        elif CLEANUP_GUESTINFO in self.metadata:
41c250
+            # This is for backwards compatibility.
41c250
+            keys_to_redact = self.metadata[CLEANUP_GUESTINFO]
41c250
+
41c250
+        if self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO:
41c250
+            guestinfo_redact_keys(keys_to_redact, self.vmware_rpctool)
41c250
+
41c250
+
41c250
+def decode(key, enc_type, data):
41c250
+    """
41c250
+    decode returns the decoded string value of data
41c250
+    key is a string used to identify the data being decoded in log messages
41c250
+    """
41c250
+    LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type)
41c250
+
41c250
+    raw_data = None
41c250
+    if enc_type in ["gzip+base64", "gz+b64"]:
41c250
+        LOG.debug("Decoding %s format %s", enc_type, key)
41c250
+        raw_data = util.decomp_gzip(util.b64d(data))
41c250
+    elif enc_type in ["base64", "b64"]:
41c250
+        LOG.debug("Decoding %s format %s", enc_type, key)
41c250
+        raw_data = util.b64d(data)
41c250
+    else:
41c250
+        LOG.debug("Plain-text data %s", key)
41c250
+        raw_data = data
41c250
+
41c250
+    return util.decode_binary(raw_data)
41c250
+
41c250
+
41c250
+def get_none_if_empty_val(val):
41c250
+    """
41c250
+    get_none_if_empty_val returns None if the provided value, once stripped
41c250
+    of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL.
41c250
+
41c250
+    The return value is always a string, regardless of whether the input is
41c250
+    a bytes class or a string.
41c250
+    """
41c250
+
41c250
+    # If the provided value is a bytes class, convert it to a string to
41c250
+    # simplify the rest of this function's logic.
41c250
+    val = util.decode_binary(val)
41c250
+    val = val.rstrip()
41c250
+    if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL:
41c250
+        return None
41c250
+    return val
41c250
+
41c250
+
41c250
+def advertise_local_ip_addrs(host_info):
41c250
+    """
41c250
+    advertise_local_ip_addrs gets the local IP address information from
41c250
+    the provided host_info map and sets the addresses in the guestinfo
41c250
+    namespace
41c250
+    """
41c250
+    if not host_info:
41c250
+        return
41c250
+
41c250
+    # Reflect any possible local IPv4 or IPv6 addresses in the guest
41c250
+    # info.
41c250
+    local_ipv4 = host_info.get(LOCAL_IPV4)
41c250
+    if local_ipv4:
41c250
+        guestinfo_set_value(LOCAL_IPV4, local_ipv4)
41c250
+        LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)
41c250
+
41c250
+    local_ipv6 = host_info.get(LOCAL_IPV6)
41c250
+    if local_ipv6:
41c250
+        guestinfo_set_value(LOCAL_IPV6, local_ipv6)
41c250
+        LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)
41c250
+
41c250
+
41c250
+def handle_returned_guestinfo_val(key, val):
41c250
+    """
41c250
+    handle_returned_guestinfo_val returns the provided value if it is
41c250
+    not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is
41c250
+    returned
41c250
+    """
41c250
+    val = get_none_if_empty_val(val)
41c250
+    if val:
41c250
+        return val
41c250
+    LOG.debug("No value found for key %s", key)
41c250
+    return None
41c250
+
41c250
+
41c250
+def get_guestinfo_key_name(key):
41c250
+    return "guestinfo." + key
41c250
+
41c250
+
41c250
+def get_guestinfo_envvar_key_name(key):
41c250
+    return ("vmx." + get_guestinfo_key_name(key)).upper().replace(".", "_", -1)
41c250
+
41c250
+
41c250
+def guestinfo_envvar(key):
41c250
+    val = guestinfo_envvar_get_value(key)
41c250
+    if not val:
41c250
+        return None
41c250
+    enc_type = guestinfo_envvar_get_value(key + ".encoding")
41c250
+    return decode(get_guestinfo_envvar_key_name(key), enc_type, val)
41c250
+
41c250
+
41c250
+def guestinfo_envvar_get_value(key):
41c250
+    env_key = get_guestinfo_envvar_key_name(key)
41c250
+    return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))
41c250
+
41c250
+
41c250
+def guestinfo(key, vmware_rpctool=VMWARE_RPCTOOL):
41c250
+    """
41c250
+    guestinfo returns the guestinfo value for the provided key, decoding
41c250
+    the value when required
41c250
+    """
41c250
+    val = guestinfo_get_value(key, vmware_rpctool)
41c250
+    if not val:
41c250
+        return None
41c250
+    enc_type = guestinfo_get_value(key + ".encoding", vmware_rpctool)
41c250
+    return decode(get_guestinfo_key_name(key), enc_type, val)
41c250
+
41c250
+
41c250
+def guestinfo_get_value(key, vmware_rpctool=VMWARE_RPCTOOL):
41c250
+    """
41c250
+    Returns a guestinfo value for the specified key.
41c250
+    """
41c250
+    LOG.debug("Getting guestinfo value for key %s", key)
41c250
+
41c250
+    try:
41c250
+        (stdout, stderr) = subp(
41c250
+            [
41c250
+                vmware_rpctool,
41c250
+                "info-get " + get_guestinfo_key_name(key),
41c250
+            ]
41c250
+        )
41c250
+        if stderr == NOVAL:
41c250
+            LOG.debug("No value found for key %s", key)
41c250
+        elif not stdout:
41c250
+            LOG.error("Failed to get guestinfo value for key %s", key)
41c250
+        return handle_returned_guestinfo_val(key, stdout)
41c250
+    except ProcessExecutionError as error:
41c250
+        if error.stderr == NOVAL:
41c250
+            LOG.debug("No value found for key %s", key)
41c250
+        else:
41c250
+            util.logexc(
41c250
+                LOG,
41c250
+                "Failed to get guestinfo value for key %s: %s",
41c250
+                key,
41c250
+                error,
41c250
+            )
41c250
+    except Exception:
41c250
+        util.logexc(
41c250
+            LOG,
41c250
+            "Unexpected error while trying to get "
41c250
+            + "guestinfo value for key %s",
41c250
+            key,
41c250
+        )
41c250
+
41c250
+    return None
41c250
+
41c250
+
41c250
+def guestinfo_set_value(key, value, vmware_rpctool=VMWARE_RPCTOOL):
41c250
+    """
41c250
+    Sets a guestinfo value for the specified key. Set value to an empty string
41c250
+    to clear an existing guestinfo key.
41c250
+    """
41c250
+
41c250
+    # If value is an empty string then set it to a single space as it is not
41c250
+    # possible to set a guestinfo key to an empty string. Setting a guestinfo
41c250
+    # key to a single space is as close as it gets to clearing an existing
41c250
+    # guestinfo key.
41c250
+    if value == "":
41c250
+        value = " "
41c250
+
41c250
+    LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
41c250
+
41c250
+    try:
41c250
+        subp(
41c250
+            [
41c250
+                vmware_rpctool,
41c250
+                ("info-set %s %s" % (get_guestinfo_key_name(key), value)),
41c250
+            ]
41c250
+        )
41c250
+        return True
41c250
+    except ProcessExecutionError as error:
41c250
+        util.logexc(
41c250
+            LOG,
41c250
+            "Failed to set guestinfo key=%s to value=%s: %s",
41c250
+            key,
41c250
+            value,
41c250
+            error,
41c250
+        )
41c250
+    except Exception:
41c250
+        util.logexc(
41c250
+            LOG,
41c250
+            "Unexpected error while trying to set "
41c250
+            + "guestinfo key=%s to value=%s",
41c250
+            key,
41c250
+            value,
41c250
+        )
41c250
+
41c250
+    return None
41c250
+
41c250
+
41c250
+def guestinfo_redact_keys(keys, vmware_rpctool=VMWARE_RPCTOOL):
41c250
+    """
41c250
+    guestinfo_redact_keys redacts guestinfo of all of the keys in the given
41c250
+    list. each key will have its value set to "---". Since the value is valid
41c250
+    YAML, cloud-init can still read it if it tries.
41c250
+    """
41c250
+    if not keys:
41c250
+        return
41c250
+    if not type(keys) in (list, tuple):
41c250
+        keys = [keys]
41c250
+    for key in keys:
41c250
+        key_name = get_guestinfo_key_name(key)
41c250
+        LOG.info("clearing %s", key_name)
41c250
+        if not guestinfo_set_value(
41c250
+            key, GUESTINFO_EMPTY_YAML_VAL, vmware_rpctool
41c250
+        ):
41c250
+            LOG.error("failed to clear %s", key_name)
41c250
+        LOG.info("clearing %s.encoding", key_name)
41c250
+        if not guestinfo_set_value(key + ".encoding", "", vmware_rpctool):
41c250
+            LOG.error("failed to clear %s.encoding", key_name)
41c250
+
41c250
+
41c250
+def load_json_or_yaml(data):
41c250
+    """
41c250
+    load first attempts to unmarshal the provided data as JSON, and if
41c250
+    that fails then attempts to unmarshal the data as YAML. If data is
41c250
+    None then a new dictionary is returned.
41c250
+    """
41c250
+    if not data:
41c250
+        return {}
41c250
+    try:
41c250
+        return util.load_json(data)
41c250
+    except (json.JSONDecodeError, TypeError):
41c250
+        return util.load_yaml(data)
41c250
+
41c250
+
41c250
+def process_metadata(data):
41c250
+    """
41c250
+    process_metadata processes metadata and loads the optional network
41c250
+    configuration.
41c250
+    """
41c250
+    network = None
41c250
+    if "network" in data:
41c250
+        network = data["network"]
41c250
+        del data["network"]
41c250
+
41c250
+    network_enc = None
41c250
+    if "network.encoding" in data:
41c250
+        network_enc = data["network.encoding"]
41c250
+        del data["network.encoding"]
41c250
+
41c250
+    if network:
41c250
+        if isinstance(network, collections.abc.Mapping):
41c250
+            LOG.debug("network data copied to 'config' key")
41c250
+            network = {"config": copy.deepcopy(network)}
41c250
+        else:
41c250
+            LOG.debug("network data to be decoded %s", network)
41c250
+            dec_net = decode("metadata.network", network_enc, network)
41c250
+            network = {
41c250
+                "config": load_json_or_yaml(dec_net),
41c250
+            }
41c250
+
41c250
+        LOG.debug("network data %s", network)
41c250
+        data["network"] = network
41c250
+
41c250
+    return data
41c250
+
41c250
+
41c250
+# Used to match classes to dependencies
41c250
+datasources = [
41c250
+    (DataSourceVMware, (sources.DEP_FILESYSTEM,)),  # Run at init-local
41c250
+    (DataSourceVMware, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
41c250
+]
41c250
+
41c250
+
41c250
+def get_datasource_list(depends):
41c250
+    """
41c250
+    Return a list of data sources that match this set of dependencies
41c250
+    """
41c250
+    return sources.list_from_depends(depends, datasources)
41c250
+
41c250
+
41c250
+def get_default_ip_addrs():
41c250
+    """
41c250
+    Returns the default IPv4 and IPv6 addresses based on the device(s) used for
41c250
+    the default route. Please note that None may be returned for either address
41c250
+    family if that family has no default route or if there are multiple
41c250
+    addresses associated with the device used by the default route for a given
41c250
+    address.
41c250
+    """
41c250
+    # TODO(promote and use netifaces in cloudinit.net* modules)
41c250
+    gateways = netifaces.gateways()
41c250
+    if "default" not in gateways:
41c250
+        return None, None
41c250
+
41c250
+    default_gw = gateways["default"]
41c250
+    if (
41c250
+        netifaces.AF_INET not in default_gw
41c250
+        and netifaces.AF_INET6 not in default_gw
41c250
+    ):
41c250
+        return None, None
41c250
+
41c250
+    ipv4 = None
41c250
+    ipv6 = None
41c250
+
41c250
+    gw4 = default_gw.get(netifaces.AF_INET)
41c250
+    if gw4:
41c250
+        _, dev4 = gw4
41c250
+        addr4_fams = netifaces.ifaddresses(dev4)
41c250
+        if addr4_fams:
41c250
+            af_inet4 = addr4_fams.get(netifaces.AF_INET)
41c250
+            if af_inet4:
41c250
+                if len(af_inet4) > 1:
41c250
+                    LOG.warning(
41c250
+                        "device %s has more than one ipv4 address: %s",
41c250
+                        dev4,
41c250
+                        af_inet4,
41c250
+                    )
41c250
+                elif "addr" in af_inet4[0]:
41c250
+                    ipv4 = af_inet4[0]["addr"]
41c250
+
41c250
+    # Try to get the default IPv6 address by first seeing if there is a default
41c250
+    # IPv6 route.
41c250
+    gw6 = default_gw.get(netifaces.AF_INET6)
41c250
+    if gw6:
41c250
+        _, dev6 = gw6
41c250
+        addr6_fams = netifaces.ifaddresses(dev6)
41c250
+        if addr6_fams:
41c250
+            af_inet6 = addr6_fams.get(netifaces.AF_INET6)
41c250
+            if af_inet6:
41c250
+                if len(af_inet6) > 1:
41c250
+                    LOG.warning(
41c250
+                        "device %s has more than one ipv6 address: %s",
41c250
+                        dev6,
41c250
+                        af_inet6,
41c250
+                    )
41c250
+                elif "addr" in af_inet6[0]:
41c250
+                    ipv6 = af_inet6[0]["addr"]
41c250
+
41c250
+    # If there is a default IPv4 address but not IPv6, then see if there is a
41c250
+    # single IPv6 address associated with the same device associated with the
41c250
+    # default IPv4 address.
41c250
+    if ipv4 and not ipv6:
41c250
+        af_inet6 = addr4_fams.get(netifaces.AF_INET6)
41c250
+        if af_inet6:
41c250
+            if len(af_inet6) > 1:
41c250
+                LOG.warning(
41c250
+                    "device %s has more than one ipv6 address: %s",
41c250
+                    dev4,
41c250
+                    af_inet6,
41c250
+                )
41c250
+            elif "addr" in af_inet6[0]:
41c250
+                ipv6 = af_inet6[0]["addr"]
41c250
+
41c250
+    # If there is a default IPv6 address but not IPv4, then see if there is a
41c250
+    # single IPv4 address associated with the same device associated with the
41c250
+    # default IPv6 address.
41c250
+    if not ipv4 and ipv6:
41c250
+        af_inet4 = addr6_fams.get(netifaces.AF_INET)
41c250
+        if af_inet4:
41c250
+            if len(af_inet4) > 1:
41c250
+                LOG.warning(
41c250
+                    "device %s has more than one ipv4 address: %s",
41c250
+                    dev6,
41c250
+                    af_inet4,
41c250
+                )
41c250
+            elif "addr" in af_inet4[0]:
41c250
+                ipv4 = af_inet4[0]["addr"]
41c250
+
41c250
+    return ipv4, ipv6
41c250
+
41c250
+
41c250
+# patched socket.getfqdn() - see https://bugs.python.org/issue5004
41c250
+
41c250
+
41c250
+def getfqdn(name=""):
41c250
+    """Get fully qualified domain name from name.
41c250
+    An empty argument is interpreted as meaning the local host.
41c250
+    """
41c250
+    # TODO(may want to promote this function to util.getfqdn)
41c250
+    # TODO(may want to extend util.get_hostname to accept fqdn=True param)
41c250
+    name = name.strip()
41c250
+    if not name or name == "0.0.0.0":
41c250
+        name = util.get_hostname()
41c250
+    try:
41c250
+        addrs = socket.getaddrinfo(
41c250
+            name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME
41c250
+        )
41c250
+    except socket.error:
41c250
+        pass
41c250
+    else:
41c250
+        for addr in addrs:
41c250
+            if addr[3]:
41c250
+                name = addr[3]
41c250
+                break
41c250
+    return name
41c250
+
41c250
+
41c250
+def is_valid_ip_addr(val):
41c250
+    """
41c250
+    Returns false if the address is loopback, link local or unspecified;
41c250
+    otherwise true is returned.
41c250
+    """
41c250
+    # TODO(extend cloudinit.net.is_ip_addr exclude link_local/loopback etc)
41c250
+    # TODO(migrate to use cloudinit.net.is_ip_addr)#
41c250
+
41c250
+    addr = None
41c250
+    try:
41c250
+        addr = ipaddress.ip_address(val)
41c250
+    except ipaddress.AddressValueError:
41c250
+        addr = ipaddress.ip_address(str(val))
41c250
+    except Exception:
41c250
+        return None
41c250
+
41c250
+    if addr.is_link_local or addr.is_loopback or addr.is_unspecified:
41c250
+        return False
41c250
+    return True
41c250
+
41c250
+
41c250
+def get_host_info():
41c250
+    """
41c250
+    Returns host information such as the host name and network interfaces.
41c250
+    """
41c250
+    # TODO(look to promote netifices use up in cloud-init netinfo funcs)
41c250
+    host_info = {
41c250
+        "network": {
41c250
+            "interfaces": {
41c250
+                "by-mac": collections.OrderedDict(),
41c250
+                "by-ipv4": collections.OrderedDict(),
41c250
+                "by-ipv6": collections.OrderedDict(),
41c250
+            },
41c250
+        },
41c250
+    }
41c250
+    hostname = getfqdn(util.get_hostname())
41c250
+    if hostname:
41c250
+        host_info["hostname"] = hostname
41c250
+        host_info["local-hostname"] = hostname
41c250
+        host_info["local_hostname"] = hostname
41c250
+
41c250
+    default_ipv4, default_ipv6 = get_default_ip_addrs()
41c250
+    if default_ipv4:
41c250
+        host_info[LOCAL_IPV4] = default_ipv4
41c250
+    if default_ipv6:
41c250
+        host_info[LOCAL_IPV6] = default_ipv6
41c250
+
41c250
+    by_mac = host_info["network"]["interfaces"]["by-mac"]
41c250
+    by_ipv4 = host_info["network"]["interfaces"]["by-ipv4"]
41c250
+    by_ipv6 = host_info["network"]["interfaces"]["by-ipv6"]
41c250
+
41c250
+    ifaces = netifaces.interfaces()
41c250
+    for dev_name in ifaces:
41c250
+        addr_fams = netifaces.ifaddresses(dev_name)
41c250
+        af_link = addr_fams.get(netifaces.AF_LINK)
41c250
+        af_inet4 = addr_fams.get(netifaces.AF_INET)
41c250
+        af_inet6 = addr_fams.get(netifaces.AF_INET6)
41c250
+
41c250
+        mac = None
41c250
+        if af_link and "addr" in af_link[0]:
41c250
+            mac = af_link[0]["addr"]
41c250
+
41c250
+        # Do not bother recording localhost
41c250
+        if mac == "00:00:00:00:00:00":
41c250
+            continue
41c250
+
41c250
+        if mac and (af_inet4 or af_inet6):
41c250
+            key = mac
41c250
+            val = {}
41c250
+            if af_inet4:
41c250
+                af_inet4_vals = []
41c250
+                for ip_info in af_inet4:
41c250
+                    if not is_valid_ip_addr(ip_info["addr"]):
41c250
+                        continue
41c250
+                    af_inet4_vals.append(ip_info)
41c250
+                val["ipv4"] = af_inet4_vals
41c250
+            if af_inet6:
41c250
+                af_inet6_vals = []
41c250
+                for ip_info in af_inet6:
41c250
+                    if not is_valid_ip_addr(ip_info["addr"]):
41c250
+                        continue
41c250
+                    af_inet6_vals.append(ip_info)
41c250
+                val["ipv6"] = af_inet6_vals
41c250
+            by_mac[key] = val
41c250
+
41c250
+        if af_inet4:
41c250
+            for ip_info in af_inet4:
41c250
+                key = ip_info["addr"]
41c250
+                if not is_valid_ip_addr(key):
41c250
+                    continue
41c250
+                val = copy.deepcopy(ip_info)
41c250
+                del val["addr"]
41c250
+                if mac:
41c250
+                    val["mac"] = mac
41c250
+                by_ipv4[key] = val
41c250
+
41c250
+        if af_inet6:
41c250
+            for ip_info in af_inet6:
41c250
+                key = ip_info["addr"]
41c250
+                if not is_valid_ip_addr(key):
41c250
+                    continue
41c250
+                val = copy.deepcopy(ip_info)
41c250
+                del val["addr"]
41c250
+                if mac:
41c250
+                    val["mac"] = mac
41c250
+                by_ipv6[key] = val
41c250
+
41c250
+    return host_info
41c250
+
41c250
+
41c250
+def wait_on_network(metadata):
41c250
+    # Determine whether we need to wait on the network coming online.
41c250
+    wait_on_ipv4 = False
41c250
+    wait_on_ipv6 = False
41c250
+    if WAIT_ON_NETWORK in metadata:
41c250
+        wait_on_network = metadata[WAIT_ON_NETWORK]
41c250
+        if WAIT_ON_NETWORK_IPV4 in wait_on_network:
41c250
+            wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4]
41c250
+            if isinstance(wait_on_ipv4_val, bool):
41c250
+                wait_on_ipv4 = wait_on_ipv4_val
41c250
+            else:
41c250
+                wait_on_ipv4 = util.translate_bool(wait_on_ipv4_val)
41c250
+        if WAIT_ON_NETWORK_IPV6 in wait_on_network:
41c250
+            wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6]
41c250
+            if isinstance(wait_on_ipv6_val, bool):
41c250
+                wait_on_ipv6 = wait_on_ipv6_val
41c250
+            else:
41c250
+                wait_on_ipv6 = util.translate_bool(wait_on_ipv6_val)
41c250
+
41c250
+    # Get information about the host.
41c250
+    host_info = None
41c250
+    while host_info is None:
41c250
+        # This loop + sleep results in two logs every second while waiting
41c250
+        # for either ipv4 or ipv6 up. Do we really need to log each iteration
41c250
+        # or can we log once and log on successful exit?
41c250
+        host_info = get_host_info()
41c250
+
41c250
+        network = host_info.get("network") or {}
41c250
+        interfaces = network.get("interfaces") or {}
41c250
+        by_ipv4 = interfaces.get("by-ipv4") or {}
41c250
+        by_ipv6 = interfaces.get("by-ipv6") or {}
41c250
+
41c250
+        if wait_on_ipv4:
41c250
+            ipv4_ready = len(by_ipv4) > 0 if by_ipv4 else False
41c250
+            if not ipv4_ready:
41c250
+                host_info = None
41c250
+
41c250
+        if wait_on_ipv6:
41c250
+            ipv6_ready = len(by_ipv6) > 0 if by_ipv6 else False
41c250
+            if not ipv6_ready:
41c250
+                host_info = None
41c250
+
41c250
+        if host_info is None:
41c250
+            LOG.debug(
41c250
+                "waiting on network: wait4=%s, ready4=%s, wait6=%s, ready6=%s",
41c250
+                wait_on_ipv4,
41c250
+                ipv4_ready,
41c250
+                wait_on_ipv6,
41c250
+                ipv6_ready,
41c250
+            )
41c250
+            time.sleep(1)
41c250
+
41c250
+    LOG.debug("waiting on network complete")
41c250
+    return host_info
41c250
+
41c250
+
41c250
+def main():
41c250
+    """
41c250
+    Executed when this file is used as a program.
41c250
+    """
41c250
+    try:
41c250
+        logging.setupBasicLogging()
41c250
+    except Exception:
41c250
+        pass
41c250
+    metadata = {
41c250
+        "wait-on-network": {"ipv4": True, "ipv6": "false"},
41c250
+        "network": {"config": {"dhcp": True}},
41c250
+    }
41c250
+    host_info = wait_on_network(metadata)
41c250
+    metadata = util.mergemanydict([metadata, host_info])
41c250
+    print(util.json_dumps(metadata))
41c250
+
41c250
+
41c250
+if __name__ == "__main__":
41c250
+    main()
41c250
+
41c250
+# vi: ts=4 expandtab
41c250
diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst
41c250
index f58b2b38..6606367c 100644
41c250
--- a/doc/rtd/topics/availability.rst
41c250
+++ b/doc/rtd/topics/availability.rst
41c250
@@ -64,5 +64,6 @@ Additionally, cloud-init is supported on these private clouds:
41c250
 - LXD
41c250
 - KVM
41c250
 - Metal-as-a-Service (MAAS)
41c250
+- VMware
41c250
 
41c250
 .. vi: textwidth=79
41c250
diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst
41c250
index 228173d2..8afed470 100644
41c250
--- a/doc/rtd/topics/datasources.rst
41c250
+++ b/doc/rtd/topics/datasources.rst
41c250
@@ -49,7 +49,7 @@ The following is a list of documents for each supported datasource:
41c250
    datasources/smartos.rst
41c250
    datasources/upcloud.rst
41c250
    datasources/zstack.rst
41c250
-
41c250
+   datasources/vmware.rst
41c250
 
41c250
 Creation
41c250
 ========
41c250
diff --git a/doc/rtd/topics/datasources/vmware.rst b/doc/rtd/topics/datasources/vmware.rst
41c250
new file mode 100644
41c250
index 00000000..996eb61f
41c250
--- /dev/null
41c250
+++ b/doc/rtd/topics/datasources/vmware.rst
41c250
@@ -0,0 +1,359 @@
41c250
+.. _datasource_vmware:
41c250
+
41c250
+VMware
41c250
+======
41c250
+
41c250
+This datasource is for use with systems running on a VMware platform such as
41c250
+vSphere and currently supports the following data transports:
41c250
+
41c250
+
41c250
+* `GuestInfo <https://github.com/vmware/govmomi/blob/master/govc/USAGE.md#vmchange>`_ keys
41c250
+
41c250
+Configuration
41c250
+-------------
41c250
+
41c250
+The configuration method is dependent upon the transport:
41c250
+
41c250
+GuestInfo Keys
41c250
+^^^^^^^^^^^^^^
41c250
+
41c250
+One method of providing meta, user, and vendor data is by setting the following
41c250
+key/value pairs on a VM's ``extraConfig`` `property <https://vdc-repo.vmware.com/vmwb-repository/dcr-public/723e7f8b-4f21-448b-a830-5f22fd931b01/5a8257bd-7f41-4423-9a73-03307535bd42/doc/vim.vm.ConfigInfo.html>`_ :
41c250
+
41c250
+.. list-table::
41c250
+   :header-rows: 1
41c250
+
41c250
+   * - Property
41c250
+     - Description
41c250
+   * - ``guestinfo.metadata``
41c250
+     - A YAML or JSON document containing the cloud-init metadata.
41c250
+   * - ``guestinfo.metadata.encoding``
41c250
+     - The encoding type for ``guestinfo.metadata``.
41c250
+   * - ``guestinfo.userdata``
41c250
+     - A YAML document containing the cloud-init user data.
41c250
+   * - ``guestinfo.userdata.encoding``
41c250
+     - The encoding type for ``guestinfo.userdata``.
41c250
+   * - ``guestinfo.vendordata``
41c250
+     - A YAML document containing the cloud-init vendor data.
41c250
+   * - ``guestinfo.vendordata.encoding``
41c250
+     - The encoding type for ``guestinfo.vendordata``.
41c250
+
41c250
+
41c250
+All ``guestinfo.*.encoding`` values may be set to ``base64`` or
41c250
+``gzip+base64``.
41c250
+
41c250
+Features
41c250
+--------
41c250
+
41c250
+This section reviews several features available in this datasource, regardless
41c250
+of how the meta, user, and vendor data was discovered.
41c250
+
41c250
+Instance data and lazy networks
41c250
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+One of the hallmarks of cloud-init is `its use of instance-data and JINJA
41c250
+queries <../instancedata.html#using-instance-data>`_
41c250
+-- the ability to write queries in user and vendor data that reference runtime
41c250
+information present in ``/run/cloud-init/instance-data.json``. This works well
41c250
+when the metadata provides all of the information up front, such as the network
41c250
+configuration. For systems that rely on DHCP, however, this information may not
41c250
+be available when the metadata is persisted to disk.
41c250
+
41c250
+This datasource ensures that even if the instance is using DHCP to configure
41c250
+networking, the same details about the configured network are available in
41c250
+``/run/cloud-init/instance-data.json`` as if static networking was used. This
41c250
+information collected at runtime is easy to demonstrate by executing the
41c250
+datasource on the command line. From the root of this repository, run the
41c250
+following command:
41c250
+
41c250
+.. code-block:: bash
41c250
+
41c250
+   PYTHONPATH="$(pwd)" python3 cloudinit/sources/DataSourceVMware.py
41c250
+
41c250
+The above command will result in output similar to the below JSON:
41c250
+
41c250
+.. code-block:: json
41c250
+
41c250
+   {
41c250
+       "hostname": "akutz.localhost",
41c250
+       "local-hostname": "akutz.localhost",
41c250
+       "local-ipv4": "192.168.0.188",
41c250
+       "local_hostname": "akutz.localhost",
41c250
+       "network": {
41c250
+           "config": {
41c250
+               "dhcp": true
41c250
+           },
41c250
+           "interfaces": {
41c250
+               "by-ipv4": {
41c250
+                   "172.0.0.2": {
41c250
+                       "netmask": "255.255.255.255",
41c250
+                       "peer": "172.0.0.2"
41c250
+                   },
41c250
+                   "192.168.0.188": {
41c250
+                       "broadcast": "192.168.0.255",
41c250
+                       "mac": "64:4b:f0:18:9a:21",
41c250
+                       "netmask": "255.255.255.0"
41c250
+                   }
41c250
+               },
41c250
+               "by-ipv6": {
41c250
+                   "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2": {
41c250
+                       "flags": 208,
41c250
+                       "mac": "64:4b:f0:18:9a:21",
41c250
+                       "netmask": "ffff:ffff:ffff:ffff::/64"
41c250
+                   }
41c250
+               },
41c250
+               "by-mac": {
41c250
+                   "64:4b:f0:18:9a:21": {
41c250
+                       "ipv4": [
41c250
+                           {
41c250
+                               "addr": "192.168.0.188",
41c250
+                               "broadcast": "192.168.0.255",
41c250
+                               "netmask": "255.255.255.0"
41c250
+                           }
41c250
+                       ],
41c250
+                       "ipv6": [
41c250
+                           {
41c250
+                               "addr": "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2",
41c250
+                               "flags": 208,
41c250
+                               "netmask": "ffff:ffff:ffff:ffff::/64"
41c250
+                           }
41c250
+                       ]
41c250
+                   },
41c250
+                   "ac:de:48:00:11:22": {
41c250
+                       "ipv6": []
41c250
+                   }
41c250
+               }
41c250
+           }
41c250
+       },
41c250
+       "wait-on-network": {
41c250
+           "ipv4": true,
41c250
+           "ipv6": "false"
41c250
+       }
41c250
+   }
41c250
+
41c250
+
41c250
+Redacting sensitive information
41c250
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+Sometimes the cloud-init userdata might contain sensitive information, and it
41c250
+may be desirable to have the ``guestinfo.userdata`` key (or other guestinfo
41c250
+keys) redacted as soon as its data is read by the datasource. This is possible
41c250
+by adding the following to the metadata:
41c250
+
41c250
+.. code-block:: yaml
41c250
+
41c250
+   redact: # formerly named cleanup-guestinfo, which will also work
41c250
+   - userdata
41c250
+   - vendordata
41c250
+
41c250
+When the above snippet is added to the metadata, the datasource will iterate
41c250
+over the elements in the ``redact`` array and clear each of the keys. For
41c250
+example, when the guestinfo transport is used, the above snippet will cause
41c250
+the following commands to be executed:
41c250
+
41c250
+.. code-block:: shell
41c250
+
41c250
+   vmware-rpctool "info-set guestinfo.userdata ---"
41c250
+   vmware-rpctool "info-set guestinfo.userdata.encoding  "
41c250
+   vmware-rpctool "info-set guestinfo.vendordata ---"
41c250
+   vmware-rpctool "info-set guestinfo.vendordata.encoding  "
41c250
+
41c250
+Please note that keys are set to the valid YAML string ``---`` as it is not
41c250
+possible remove an existing key from the guestinfo key-space. A key's analogous
41c250
+encoding property will be set to a single white-space character, causing the
41c250
+datasource to treat the actual key value as plain-text, thereby loading it as
41c250
+an empty YAML doc (hence the aforementioned ``---``\ ).
41c250
+
41c250
+Reading the local IP addresses
41c250
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+This datasource automatically discovers the local IPv4 and IPv6 addresses for
41c250
+a guest operating system based on the default routes. However, when inspecting
41c250
+a VM externally, it's not possible to know what the *default* IP address is for
41c250
+the guest OS. That's why this datasource sets the discovered, local IPv4 and
41c250
+IPv6 addresses back in the guestinfo namespace as the following keys:
41c250
+
41c250
+
41c250
+* ``guestinfo.local-ipv4``
41c250
+* ``guestinfo.local-ipv6``
41c250
+
41c250
+It is possible that a host may not have any default, local IP addresses. It's
41c250
+also possible the reported, local addresses are link-local addresses. But these
41c250
+two keys may be used to discover what this datasource determined were the local
41c250
+IPv4 and IPv6 addresses for a host.
41c250
+
41c250
+Waiting on the network
41c250
+^^^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+Sometimes cloud-init may bring up the network, but it will not finish coming
41c250
+online before the datasource's ``setup`` function is called, resulting in an
41c250
+``/var/run/cloud-init/instance-data.json`` file that does not have the correct
41c250
+network information. It is possible to instruct the datasource to wait until an
41c250
+IPv4 or IPv6 address is available before writing the instance data with the
41c250
+following metadata properties:
41c250
+
41c250
+.. code-block:: yaml
41c250
+
41c250
+   wait-on-network:
41c250
+     ipv4: true
41c250
+     ipv6: true
41c250
+
41c250
+If either of the above values are true, then the datasource will sleep for a
41c250
+second, check the network status, and repeat until one or both addresses from
41c250
+the specified families are available.
41c250
+
41c250
+Walkthrough
41c250
+-----------
41c250
+
41c250
+The following series of steps is a demonstration on how to configure a VM with
41c250
+this datasource:
41c250
+
41c250
+
41c250
+#. Create the metadata file for the VM. Save the following YAML to a file named
41c250
+   ``metadata.yaml``\ :
41c250
+
41c250
+   .. code-block:: yaml
41c250
+
41c250
+       instance-id: cloud-vm
41c250
+       local-hostname: cloud-vm
41c250
+       network:
41c250
+         version: 2
41c250
+         ethernets:
41c250
+           nics:
41c250
+             match:
41c250
+               name: ens*
41c250
+             dhcp4: yes
41c250
+
41c250
+#. Create the userdata file ``userdata.yaml``\ :
41c250
+
41c250
+   .. code-block:: yaml
41c250
+
41c250
+       #cloud-config
41c250
+
41c250
+       users:
41c250
+       - default
41c250
+       - name: akutz
41c250
+           primary_group: akutz
41c250
+           sudo: ALL=(ALL) NOPASSWD:ALL
41c250
+           groups: sudo, wheel
41c250
+           ssh_import_id: None
41c250
+           lock_passwd: true
41c250
+           ssh_authorized_keys:
41c250
+           - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDE0c5FczvcGSh/tG4iw+Fhfi/O5/EvUM/96js65tly4++YTXK1d9jcznPS5ruDlbIZ30oveCBd3kT8LLVFwzh6hepYTf0YmCTpF4eDunyqmpCXDvVscQYRXyasEm5olGmVe05RrCJSeSShAeptv4ueIn40kZKOghinGWLDSZG4+FFfgrmcMCpx5YSCtX2gvnEYZJr0czt4rxOZuuP7PkJKgC/mt2PcPjooeX00vAj81jjU2f3XKrjjz2u2+KIt9eba+vOQ6HiC8c2IzRkUAJ5i1atLy8RIbejo23+0P4N2jjk17QySFOVHwPBDTYb0/0M/4ideeU74EN/CgVsvO6JrLsPBR4dojkV5qNbMNxIVv5cUwIy2ThlLgqpNCeFIDLCWNZEFKlEuNeSQ2mPtIO7ETxEL2Cz5y/7AIuildzYMc6wi2bofRC8HmQ7rMXRWdwLKWsR0L7SKjHblIwarxOGqLnUI+k2E71YoP7SZSlxaKi17pqkr0OMCF+kKqvcvHAQuwGqyumTEWOlH6TCx1dSPrW+pVCZSHSJtSTfDW2uzL6y8k10MT06+pVunSrWo5LHAXcS91htHV1M1UrH/tZKSpjYtjMb5+RonfhaFRNzvj7cCE1f3Kp8UVqAdcGBTtReoE8eRUT63qIxjw03a7VwAyB2w+9cu1R9/vAo8SBeRqw== sakutz@gmail.com
41c250
+
41c250
+#. Please note this step requires that the VM be powered off. All of the
41c250
+   commands below use the VMware CLI tool, `govc <https://github.com/vmware/govmomi/blob/master/govc>`_.
41c250
+
41c250
+   Go ahead and assign the path to the VM to the environment variable ``VM``\ :
41c250
+
41c250
+   .. code-block:: shell
41c250
+
41c250
+      export VM="/inventory/path/to/the/vm"
41c250
+
41c250
+#. Power off the VM:
41c250
+
41c250
+   .. raw:: html
41c250
+
41c250
+      
41c250
+
41c250
+      ⚠️ First Boot Mode
41c250
+
41c250
+   To ensure the next power-on operation results in a first-boot scenario for
41c250
+   cloud-init, it may be necessary to run the following command just before
41c250
+   powering off the VM:
41c250
+
41c250
+   .. code-block:: bash
41c250
+
41c250
+      cloud-init clean
41c250
+
41c250
+   Otherwise cloud-init may not run in first-boot mode. For more information
41c250
+   on how the boot mode is determined, please see the
41c250
+   `First Boot Documentation <../boot.html#first-boot-determination>`_.
41c250
+
41c250
+   .. raw:: html
41c250
+
41c250
+      
41c250
+
41c250
+   .. code-block:: shell
41c250
+
41c250
+      govc vm.power -off "${VM}"
41c250
+
41c250
+#.
41c250
+   Export the environment variables that contain the cloud-init metadata and
41c250
+   userdata:
41c250
+
41c250
+   .. code-block:: shell
41c250
+
41c250
+      export METADATA=$(gzip -c9 <metadata.yaml | { base64 -w0 2>/dev/null || base64; }) \
41c250
+           USERDATA=$(gzip -c9 <userdata.yaml | { base64 -w0 2>/dev/null || base64; })
41c250
+
41c250
+#.
41c250
+   Assign the metadata and userdata to the VM:
41c250
+
41c250
+   .. code-block:: shell
41c250
+
41c250
+       govc vm.change -vm "${VM}" \
41c250
+       -e guestinfo.metadata="${METADATA}" \
41c250
+       -e guestinfo.metadata.encoding="gzip+base64" \
41c250
+       -e guestinfo.userdata="${USERDATA}" \
41c250
+       -e guestinfo.userdata.encoding="gzip+base64"
41c250
+
41c250
+   Please note the above commands include specifying the encoding for the
41c250
+   properties. This is important as it informs the datasource how to decode
41c250
+   the data for cloud-init. Valid values for ``metadata.encoding`` and
41c250
+   ``userdata.encoding`` include:
41c250
+
41c250
+
41c250
+   * ``base64``
41c250
+   * ``gzip+base64``
41c250
+
41c250
+#.
41c250
+   Power on the VM:
41c250
+
41c250
+   .. code-block:: shell
41c250
+
41c250
+       govc vm.power -vm "${VM}" -on
41c250
+
41c250
+If all went according to plan, the CentOS box is:
41c250
+
41c250
+* Locked down, allowing SSH access only for the user in the userdata
41c250
+* Configured for a dynamic IP address via DHCP
41c250
+* Has a hostname of ``cloud-vm``
41c250
+
41c250
+Examples
41c250
+--------
41c250
+
41c250
+This section reviews common configurations:
41c250
+
41c250
+Setting the hostname
41c250
+^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+The hostname is set by way of the metadata key ``local-hostname``.
41c250
+
41c250
+Setting the instance ID
41c250
+^^^^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+The instance ID may be set by way of the metadata key ``instance-id``. However,
41c250
+if this value is absent then then the instance ID is read from the file
41c250
+``/sys/class/dmi/id/product_uuid``.
41c250
+
41c250
+Providing public SSH keys
41c250
+^^^^^^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+The public SSH keys may be set by way of the metadata key ``public-keys-data``.
41c250
+Each newline-terminated string will be interpreted as a separate SSH public
41c250
+key, which will be placed in distro's default user's
41c250
+``~/.ssh/authorized_keys``. If the value is empty or absent, then nothing will
41c250
+be written to ``~/.ssh/authorized_keys``.
41c250
+
41c250
+Configuring the network
41c250
+^^^^^^^^^^^^^^^^^^^^^^^
41c250
+
41c250
+The network is configured by setting the metadata key ``network`` with a value
41c250
+consistent with Network Config Versions
41c250
+`1 <../network-config-format-v1.html>`_ or
41c250
+`2 <../network-config-format-v2.html>`_\ , depending on the Linux
41c250
+distro's version of cloud-init.
41c250
+
41c250
+The metadata key ``network.encoding`` may be used to indicate the format of
41c250
+the metadata key "network". Valid encodings are ``base64`` and ``gzip+base64``.
41c250
diff --git a/requirements.txt b/requirements.txt
41c250
index 5817da3b..41d01d62 100644
41c250
--- a/requirements.txt
41c250
+++ b/requirements.txt
41c250
@@ -32,3 +32,12 @@ jsonpatch
41c250
 
41c250
 # For validating cloud-config sections per schema definitions
41c250
 jsonschema
41c250
+
41c250
+# Used by DataSourceVMware to inspect the host's network configuration during
41c250
+# the "setup()" function.
41c250
+#
41c250
+# This allows a host that uses DHCP to bring up the network during BootLocal
41c250
+# and still participate in instance-data by gathering the network in detail at
41c250
+# runtime and merge that information into the metadata and repersist that to
41c250
+# disk.
41c250
+netifaces>=0.10.9
41c250
diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py
41c250
index 5912f7ee..475a2cf8 100644
41c250
--- a/tests/unittests/test_datasource/test_common.py
41c250
+++ b/tests/unittests/test_datasource/test_common.py
41c250
@@ -28,6 +28,7 @@ from cloudinit.sources import (
41c250
     DataSourceScaleway as Scaleway,
41c250
     DataSourceSmartOS as SmartOS,
41c250
     DataSourceUpCloud as UpCloud,
41c250
+    DataSourceVMware as VMware,
41c250
 )
41c250
 from cloudinit.sources import DataSourceNone as DSNone
41c250
 
41c250
@@ -50,6 +51,7 @@ DEFAULT_LOCAL = [
41c250
     RbxCloud.DataSourceRbxCloud,
41c250
     Scaleway.DataSourceScaleway,
41c250
     UpCloud.DataSourceUpCloudLocal,
41c250
+    VMware.DataSourceVMware,
41c250
 ]
41c250
 
41c250
 DEFAULT_NETWORK = [
41c250
@@ -66,6 +68,7 @@ DEFAULT_NETWORK = [
41c250
     OpenStack.DataSourceOpenStack,
41c250
     OVF.DataSourceOVFNet,
41c250
     UpCloud.DataSourceUpCloud,
41c250
+    VMware.DataSourceVMware,
41c250
 ]
41c250
 
41c250
 
41c250
diff --git a/tests/unittests/test_datasource/test_vmware.py b/tests/unittests/test_datasource/test_vmware.py
41c250
new file mode 100644
41c250
index 00000000..597db7c8
41c250
--- /dev/null
41c250
+++ b/tests/unittests/test_datasource/test_vmware.py
41c250
@@ -0,0 +1,377 @@
41c250
+# Copyright (c) 2021 VMware, Inc. All Rights Reserved.
41c250
+#
41c250
+# Authors: Andrew Kutz <akutz@vmware.com>
41c250
+#
41c250
+# This file is part of cloud-init. See LICENSE file for license information.
41c250
+
41c250
+import base64
41c250
+import gzip
41c250
+from cloudinit import dmi, helpers, safeyaml
41c250
+from cloudinit import settings
41c250
+from cloudinit.sources import DataSourceVMware
41c250
+from cloudinit.tests.helpers import (
41c250
+    mock,
41c250
+    CiTestCase,
41c250
+    FilesystemMockingTestCase,
41c250
+    populate_dir,
41c250
+)
41c250
+
41c250
+import os
41c250
+
41c250
+PRODUCT_NAME_FILE_PATH = "/sys/class/dmi/id/product_name"
41c250
+PRODUCT_NAME = "VMware7,1"
41c250
+PRODUCT_UUID = "82343CED-E4C7-423B-8F6B-0D34D19067AB"
41c250
+REROOT_FILES = {
41c250
+    DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
41c250
+    PRODUCT_NAME_FILE_PATH: PRODUCT_NAME,
41c250
+}
41c250
+
41c250
+VMW_MULTIPLE_KEYS = [
41c250
+    "ssh-rsa AAAAB3NzaC1yc2EAAAA... test1@vmw.com",
41c250
+    "ssh-rsa AAAAB3NzaC1yc2EAAAA... test2@vmw.com",
41c250
+]
41c250
+VMW_SINGLE_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAA... test@vmw.com"
41c250
+
41c250
+VMW_METADATA_YAML = """instance-id: cloud-vm
41c250
+local-hostname: cloud-vm
41c250
+network:
41c250
+  version: 2
41c250
+  ethernets:
41c250
+    nics:
41c250
+      match:
41c250
+        name: ens*
41c250
+      dhcp4: yes
41c250
+"""
41c250
+
41c250
+VMW_USERDATA_YAML = """## template: jinja
41c250
+#cloud-config
41c250
+users:
41c250
+- default
41c250
+"""
41c250
+
41c250
+VMW_VENDORDATA_YAML = """## template: jinja
41c250
+#cloud-config
41c250
+runcmd:
41c250
+- echo "Hello, world."
41c250
+"""
41c250
+
41c250
+
41c250
+class TestDataSourceVMware(CiTestCase):
41c250
+    """
41c250
+    Test common functionality that is not transport specific.
41c250
+    """
41c250
+
41c250
+    def setUp(self):
41c250
+        super(TestDataSourceVMware, self).setUp()
41c250
+        self.tmp = self.tmp_dir()
41c250
+
41c250
+    def test_no_data_access_method(self):
41c250
+        ds = get_ds(self.tmp)
41c250
+        ds.vmware_rpctool = None
41c250
+        ret = ds.get_data()
41c250
+        self.assertFalse(ret)
41c250
+
41c250
+    def test_get_host_info(self):
41c250
+        host_info = DataSourceVMware.get_host_info()
41c250
+        self.assertTrue(host_info)
41c250
+        self.assertTrue(host_info["hostname"])
41c250
+        self.assertTrue(host_info["local-hostname"])
41c250
+        self.assertTrue(host_info["local_hostname"])
41c250
+        self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4])
41c250
+
41c250
+
41c250
+class TestDataSourceVMwareEnvVars(FilesystemMockingTestCase):
41c250
+    """
41c250
+    Test the envvar transport.
41c250
+    """
41c250
+
41c250
+    def setUp(self):
41c250
+        super(TestDataSourceVMwareEnvVars, self).setUp()
41c250
+        self.tmp = self.tmp_dir()
41c250
+        os.environ[DataSourceVMware.VMX_GUESTINFO] = "1"
41c250
+        self.create_system_files()
41c250
+
41c250
+    def tearDown(self):
41c250
+        del os.environ[DataSourceVMware.VMX_GUESTINFO]
41c250
+        return super(TestDataSourceVMwareEnvVars, self).tearDown()
41c250
+
41c250
+    def create_system_files(self):
41c250
+        rootd = self.tmp_dir()
41c250
+        populate_dir(
41c250
+            rootd,
41c250
+            {
41c250
+                DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
41c250
+            },
41c250
+        )
41c250
+        self.assertTrue(self.reRoot(rootd))
41c250
+
41c250
+    def assert_get_data_ok(self, m_fn, m_fn_call_count=6):
41c250
+        ds = get_ds(self.tmp)
41c250
+        ds.vmware_rpctool = None
41c250
+        ret = ds.get_data()
41c250
+        self.assertTrue(ret)
41c250
+        self.assertEqual(m_fn_call_count, m_fn.call_count)
41c250
+        self.assertEqual(
41c250
+            ds.data_access_method, DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR
41c250
+        )
41c250
+        return ds
41c250
+
41c250
+    def assert_metadata(self, metadata, m_fn, m_fn_call_count=6):
41c250
+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count)
41c250
+        assert_metadata(self, ds, metadata)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_subplatform(self, m_fn):
41c250
+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
41c250
+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+        self.assertEqual(
41c250
+            ds.subplatform,
41c250
+            "%s (%s)"
41c250
+            % (
41c250
+                DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR,
41c250
+                DataSourceVMware.get_guestinfo_envvar_key_name("metadata"),
41c250
+            ),
41c250
+        )
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_data_metadata_only(self, m_fn):
41c250
+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_data_userdata_only(self, m_fn):
41c250
+        m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_data_vendordata_only(self, m_fn):
41c250
+        m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_data_metadata_base64(self, m_fn):
41c250
+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
41c250
+        m_fn.side_effect = [data, "base64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_data_metadata_b64(self, m_fn):
41c250
+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
41c250
+        m_fn.side_effect = [data, "b64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_data_metadata_gzip_base64(self, m_fn):
41c250
+        data = VMW_METADATA_YAML.encode("utf-8")
41c250
+        data = gzip.compress(data)
41c250
+        data = base64.b64encode(data)
41c250
+        m_fn.side_effect = [data, "gzip+base64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_get_data_metadata_gz_b64(self, m_fn):
41c250
+        data = VMW_METADATA_YAML.encode("utf-8")
41c250
+        data = gzip.compress(data)
41c250
+        data = base64.b64encode(data)
41c250
+        m_fn.side_effect = [data, "gz+b64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_metadata_single_ssh_key(self, m_fn):
41c250
+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
41c250
+        metadata["public_keys"] = VMW_SINGLE_KEY
41c250
+        metadata_yaml = safeyaml.dumps(metadata)
41c250
+        m_fn.side_effect = [metadata_yaml, "", "", ""]
41c250
+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch(
41c250
+        "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value"
41c250
+    )
41c250
+    def test_metadata_multiple_ssh_keys(self, m_fn):
41c250
+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
41c250
+        metadata["public_keys"] = VMW_MULTIPLE_KEYS
41c250
+        metadata_yaml = safeyaml.dumps(metadata)
41c250
+        m_fn.side_effect = [metadata_yaml, "", "", ""]
41c250
+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
41c250
+
41c250
+
41c250
+class TestDataSourceVMwareGuestInfo(FilesystemMockingTestCase):
41c250
+    """
41c250
+    Test the guestinfo transport on a VMware platform.
41c250
+    """
41c250
+
41c250
+    def setUp(self):
41c250
+        super(TestDataSourceVMwareGuestInfo, self).setUp()
41c250
+        self.tmp = self.tmp_dir()
41c250
+        self.create_system_files()
41c250
+
41c250
+    def create_system_files(self):
41c250
+        rootd = self.tmp_dir()
41c250
+        populate_dir(
41c250
+            rootd,
41c250
+            {
41c250
+                DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
41c250
+                PRODUCT_NAME_FILE_PATH: PRODUCT_NAME,
41c250
+            },
41c250
+        )
41c250
+        self.assertTrue(self.reRoot(rootd))
41c250
+
41c250
+    def assert_get_data_ok(self, m_fn, m_fn_call_count=6):
41c250
+        ds = get_ds(self.tmp)
41c250
+        ds.vmware_rpctool = "vmware-rpctool"
41c250
+        ret = ds.get_data()
41c250
+        self.assertTrue(ret)
41c250
+        self.assertEqual(m_fn_call_count, m_fn.call_count)
41c250
+        self.assertEqual(
41c250
+            ds.data_access_method,
41c250
+            DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO,
41c250
+        )
41c250
+        return ds
41c250
+
41c250
+    def assert_metadata(self, metadata, m_fn, m_fn_call_count=6):
41c250
+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count)
41c250
+        assert_metadata(self, ds, metadata)
41c250
+
41c250
+    def test_ds_valid_on_vmware_platform(self):
41c250
+        system_type = dmi.read_dmi_data("system-product-name")
41c250
+        self.assertEqual(system_type, PRODUCT_NAME)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_get_subplatform(self, m_fn):
41c250
+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
41c250
+        ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+        self.assertEqual(
41c250
+            ds.subplatform,
41c250
+            "%s (%s)"
41c250
+            % (
41c250
+                DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO,
41c250
+                DataSourceVMware.get_guestinfo_key_name("metadata"),
41c250
+            ),
41c250
+        )
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_get_data_userdata_only(self, m_fn):
41c250
+        m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_get_data_vendordata_only(self, m_fn):
41c250
+        m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_metadata_single_ssh_key(self, m_fn):
41c250
+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
41c250
+        metadata["public_keys"] = VMW_SINGLE_KEY
41c250
+        metadata_yaml = safeyaml.dumps(metadata)
41c250
+        m_fn.side_effect = [metadata_yaml, "", "", ""]
41c250
+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_metadata_multiple_ssh_keys(self, m_fn):
41c250
+        metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML)
41c250
+        metadata["public_keys"] = VMW_MULTIPLE_KEYS
41c250
+        metadata_yaml = safeyaml.dumps(metadata)
41c250
+        m_fn.side_effect = [metadata_yaml, "", "", ""]
41c250
+        self.assert_metadata(metadata, m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_get_data_metadata_base64(self, m_fn):
41c250
+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
41c250
+        m_fn.side_effect = [data, "base64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_get_data_metadata_b64(self, m_fn):
41c250
+        data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8"))
41c250
+        m_fn.side_effect = [data, "b64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_get_data_metadata_gzip_base64(self, m_fn):
41c250
+        data = VMW_METADATA_YAML.encode("utf-8")
41c250
+        data = gzip.compress(data)
41c250
+        data = base64.b64encode(data)
41c250
+        m_fn.side_effect = [data, "gzip+base64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_get_data_metadata_gz_b64(self, m_fn):
41c250
+        data = VMW_METADATA_YAML.encode("utf-8")
41c250
+        data = gzip.compress(data)
41c250
+        data = base64.b64encode(data)
41c250
+        m_fn.side_effect = [data, "gz+b64", "", ""]
41c250
+        self.assert_get_data_ok(m_fn, m_fn_call_count=4)
41c250
+
41c250
+
41c250
+class TestDataSourceVMwareGuestInfo_InvalidPlatform(FilesystemMockingTestCase):
41c250
+    """
41c250
+    Test the guestinfo transport on a non-VMware platform.
41c250
+    """
41c250
+
41c250
+    def setUp(self):
41c250
+        super(TestDataSourceVMwareGuestInfo_InvalidPlatform, self).setUp()
41c250
+        self.tmp = self.tmp_dir()
41c250
+        self.create_system_files()
41c250
+
41c250
+    def create_system_files(self):
41c250
+        rootd = self.tmp_dir()
41c250
+        populate_dir(
41c250
+            rootd,
41c250
+            {
41c250
+                DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID,
41c250
+            },
41c250
+        )
41c250
+        self.assertTrue(self.reRoot(rootd))
41c250
+
41c250
+    @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value")
41c250
+    def test_ds_invalid_on_non_vmware_platform(self, m_fn):
41c250
+        system_type = dmi.read_dmi_data("system-product-name")
41c250
+        self.assertEqual(system_type, None)
41c250
+
41c250
+        m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""]
41c250
+        ds = get_ds(self.tmp)
41c250
+        ds.vmware_rpctool = "vmware-rpctool"
41c250
+        ret = ds.get_data()
41c250
+        self.assertFalse(ret)
41c250
+
41c250
+
41c250
+def assert_metadata(test_obj, ds, metadata):
41c250
+    test_obj.assertEqual(metadata.get("instance-id"), ds.get_instance_id())
41c250
+    test_obj.assertEqual(metadata.get("local-hostname"), ds.get_hostname())
41c250
+
41c250
+    expected_public_keys = metadata.get("public_keys")
41c250
+    if not isinstance(expected_public_keys, list):
41c250
+        expected_public_keys = [expected_public_keys]
41c250
+
41c250
+    test_obj.assertEqual(expected_public_keys, ds.get_public_ssh_keys())
41c250
+    test_obj.assertIsInstance(ds.get_public_ssh_keys(), list)
41c250
+
41c250
+
41c250
+def get_ds(temp_dir):
41c250
+    ds = DataSourceVMware.DataSourceVMware(
41c250
+        settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": temp_dir})
41c250
+    )
41c250
+    ds.vmware_rpctool = "vmware-rpctool"
41c250
+    return ds
41c250
+
41c250
+
41c250
+# vi: ts=4 expandtab
41c250
diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
41c250
index 1d8aaf18..8617d7bd 100644
41c250
--- a/tests/unittests/test_ds_identify.py
41c250
+++ b/tests/unittests/test_ds_identify.py
41c250
@@ -649,6 +649,50 @@ class TestDsIdentify(DsIdentifyBase):
41c250
         """EC2: bobrightbox.com in product_serial is not brightbox'"""
41c250
         self._test_ds_not_found('Ec2-E24Cloud-negative')
41c250
 
41c250
+    def test_vmware_no_valid_transports(self):
41c250
+        """VMware: no valid transports"""
41c250
+        self._test_ds_not_found('VMware-NoValidTransports')
41c250
+
41c250
+    def test_vmware_envvar_no_data(self):
41c250
+        """VMware: envvar transport no data"""
41c250
+        self._test_ds_not_found('VMware-EnvVar-NoData')
41c250
+
41c250
+    def test_vmware_envvar_no_virt_id(self):
41c250
+        """VMware: envvar transport success if no virt id"""
41c250
+        self._test_ds_found('VMware-EnvVar-NoVirtID')
41c250
+
41c250
+    def test_vmware_envvar_activated_by_metadata(self):
41c250
+        """VMware: envvar transport activated by metadata"""
41c250
+        self._test_ds_found('VMware-EnvVar-Metadata')
41c250
+
41c250
+    def test_vmware_envvar_activated_by_userdata(self):
41c250
+        """VMware: envvar transport activated by userdata"""
41c250
+        self._test_ds_found('VMware-EnvVar-Userdata')
41c250
+
41c250
+    def test_vmware_envvar_activated_by_vendordata(self):
41c250
+        """VMware: envvar transport activated by vendordata"""
41c250
+        self._test_ds_found('VMware-EnvVar-Vendordata')
41c250
+
41c250
+    def test_vmware_guestinfo_no_data(self):
41c250
+        """VMware: guestinfo transport no data"""
41c250
+        self._test_ds_not_found('VMware-GuestInfo-NoData')
41c250
+
41c250
+    def test_vmware_guestinfo_no_virt_id(self):
41c250
+        """VMware: guestinfo transport fails if no virt id"""
41c250
+        self._test_ds_not_found('VMware-GuestInfo-NoVirtID')
41c250
+
41c250
+    def test_vmware_guestinfo_activated_by_metadata(self):
41c250
+        """VMware: guestinfo transport activated by metadata"""
41c250
+        self._test_ds_found('VMware-GuestInfo-Metadata')
41c250
+
41c250
+    def test_vmware_guestinfo_activated_by_userdata(self):
41c250
+        """VMware: guestinfo transport activated by userdata"""
41c250
+        self._test_ds_found('VMware-GuestInfo-Userdata')
41c250
+
41c250
+    def test_vmware_guestinfo_activated_by_vendordata(self):
41c250
+        """VMware: guestinfo transport activated by vendordata"""
41c250
+        self._test_ds_found('VMware-GuestInfo-Vendordata')
41c250
+
41c250
 
41c250
 class TestBSDNoSys(DsIdentifyBase):
41c250
     """Test *BSD code paths
41c250
@@ -1136,7 +1180,240 @@ VALID_CFG = {
41c250
     'Ec2-E24Cloud-negative': {
41c250
         'ds': 'Ec2',
41c250
         'files': {P_SYS_VENDOR: 'e24cloudyday\n'},
41c250
-    }
41c250
+    },
41c250
+    'VMware-NoValidTransports': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-EnvVar-NoData': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-EnvVar-NoVirtID': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+        ],
41c250
+    },
41c250
+    'VMware-EnvVar-Metadata': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-EnvVar-Userdata': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-EnvVar-Vendordata': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_metadata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_has_envvar_vmx_guestinfo_vendordata',
41c250
+                'ret': 0,
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-GuestInfo-NoData': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_rpctool',
41c250
+                'ret': 0,
41c250
+                'out': '/usr/bin/vmware-rpctool',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_metadata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-GuestInfo-NoVirtID': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_rpctool',
41c250
+                'ret': 0,
41c250
+                'out': '/usr/bin/vmware-rpctool',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_metadata',
41c250
+                'ret': 0,
41c250
+                'out': '---',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+        ],
41c250
+    },
41c250
+    'VMware-GuestInfo-Metadata': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_rpctool',
41c250
+                'ret': 0,
41c250
+                'out': '/usr/bin/vmware-rpctool',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_metadata',
41c250
+                'ret': 0,
41c250
+                'out': '---',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-GuestInfo-Userdata': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_rpctool',
41c250
+                'ret': 0,
41c250
+                'out': '/usr/bin/vmware-rpctool',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_metadata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_userdata',
41c250
+                'ret': 0,
41c250
+                'out': '---',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_vendordata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
+    'VMware-GuestInfo-Vendordata': {
41c250
+        'ds': 'VMware',
41c250
+        'mocks': [
41c250
+            {
41c250
+                'name': 'vmware_has_rpctool',
41c250
+                'ret': 0,
41c250
+                'out': '/usr/bin/vmware-rpctool',
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_metadata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_userdata',
41c250
+                'ret': 1,
41c250
+            },
41c250
+            {
41c250
+                'name': 'vmware_rpctool_guestinfo_vendordata',
41c250
+                'ret': 0,
41c250
+                'out': '---',
41c250
+            },
41c250
+            MOCK_VIRT_IS_VMWARE,
41c250
+        ],
41c250
+    },
41c250
 }
41c250
 
41c250
 # vi: ts=4 expandtab
41c250
diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers
41c250
index 689d7902..cbfa883c 100644
41c250
--- a/tools/.github-cla-signers
41c250
+++ b/tools/.github-cla-signers
41c250
@@ -1,5 +1,6 @@
41c250
 ader1990
41c250
 ajmyyra
41c250
+akutz
41c250
 AlexBaranowski
41c250
 Aman306
41c250
 andrewbogott
41c250
diff --git a/tools/ds-identify b/tools/ds-identify
41c250
index 2f2486f7..c01eae3d 100755
41c250
--- a/tools/ds-identify
41c250
+++ b/tools/ds-identify
41c250
@@ -125,7 +125,7 @@ DI_DSNAME=""
41c250
 # be searched if there is no setting found in config.
41c250
 DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \
41c250
 CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \
41c250
-OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud"
41c250
+OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud VMware"
41c250
 DI_DSLIST=""
41c250
 DI_MODE=""
41c250
 DI_ON_FOUND=""
41c250
@@ -1350,6 +1350,80 @@ dscheck_IBMCloud() {
41c250
     return ${DS_NOT_FOUND}
41c250
 }
41c250
 
41c250
+vmware_has_envvar_vmx_guestinfo() {
41c250
+    [ -n "${VMX_GUESTINFO:-}" ]
41c250
+}
41c250
+
41c250
+vmware_has_envvar_vmx_guestinfo_metadata() {
41c250
+    [ -n "${VMX_GUESTINFO_METADATA:-}" ]
41c250
+}
41c250
+
41c250
+vmware_has_envvar_vmx_guestinfo_userdata() {
41c250
+    [ -n "${VMX_GUESTINFO_USERDATA:-}" ]
41c250
+}
41c250
+
41c250
+vmware_has_envvar_vmx_guestinfo_vendordata() {
41c250
+    [ -n "${VMX_GUESTINFO_VENDORDATA:-}" ]
41c250
+}
41c250
+
41c250
+vmware_has_rpctool() {
41c250
+    command -v vmware-rpctool >/dev/null 2>&1
41c250
+}
41c250
+
41c250
+vmware_rpctool_guestinfo_metadata() {
41c250
+    vmware-rpctool "info-get guestinfo.metadata"
41c250
+}
41c250
+
41c250
+vmware_rpctool_guestinfo_userdata() {
41c250
+    vmware-rpctool "info-get guestinfo.userdata"
41c250
+}
41c250
+
41c250
+vmware_rpctool_guestinfo_vendordata() {
41c250
+    vmware-rpctool "info-get guestinfo.vendordata"
41c250
+}
41c250
+
41c250
+dscheck_VMware() {
41c250
+    # Checks to see if there is valid data for the VMware datasource.
41c250
+    # The data transports are checked in the following order:
41c250
+    #
41c250
+    #   * envvars
41c250
+    #   * guestinfo
41c250
+    #
41c250
+    # Please note when updating this function with support for new data
41c250
+    # transports, the order should match the order in the _get_data
41c250
+    # function from the file DataSourceVMware.py.
41c250
+
41c250
+    # Check to see if running in a container and the VMware
41c250
+    # datasource is configured via environment variables.
41c250
+    if vmware_has_envvar_vmx_guestinfo; then
41c250
+        if vmware_has_envvar_vmx_guestinfo_metadata || \
41c250
+            vmware_has_envvar_vmx_guestinfo_userdata || \
41c250
+            vmware_has_envvar_vmx_guestinfo_vendordata; then
41c250
+            return "${DS_FOUND}"
41c250
+        fi
41c250
+    fi
41c250
+
41c250
+    # Do not proceed unless the detected platform is VMware.
41c250
+    if [ ! "${DI_VIRT}" = "vmware" ]; then
41c250
+        return "${DS_NOT_FOUND}"
41c250
+    fi
41c250
+
41c250
+    # Do not proceed if the vmware-rpctool command is not present.
41c250
+    if ! vmware_has_rpctool; then
41c250
+        return "${DS_NOT_FOUND}"
41c250
+    fi
41c250
+
41c250
+    # Activate the VMware datasource only if any of the fields used
41c250
+    # by the datasource are present in the guestinfo table.
41c250
+    if { vmware_rpctool_guestinfo_metadata || \
41c250
+         vmware_rpctool_guestinfo_userdata || \
41c250
+         vmware_rpctool_guestinfo_vendordata; } >/dev/null 2>&1; then
41c250
+        return "${DS_FOUND}"
41c250
+    fi
41c250
+
41c250
+    return "${DS_NOT_FOUND}"
41c250
+}
41c250
+
41c250
 collect_info() {
41c250
     read_uname_info
41c250
     read_virt
41c250
-- 
41c250
2.27.0
41c250