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















|
46a734
+| 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















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