Skip to content

Commit

Permalink
vmware: Fall back to vmtoolsd if vmware-rpctool errs
Browse files Browse the repository at this point in the history
This patch udpates the ds-identify script and the VMware datasource
to fall back to using the vmtoolsd program if vmware-rpctool errors.
  • Loading branch information
akutz committed Sep 18, 2023
1 parent e9cdd7e commit 4fb5978
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 104 deletions.
57 changes: 44 additions & 13 deletions cloudinit/sources/DataSourceOVF.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@
#
# This file is part of cloud-init. See LICENSE file for license information.

"""Cloud-Init DataSource for OVF
This module provides a cloud-init datasource for OVF data.
This module may be unit tested by running the following command from the root
of the Cloud-Init repository:
make clean_pyc && PYTHONPATH="$(pwd)" \
python3 -m pytest -v --log-level=DEBUG \
tests/unittests/sources/test_ovf.py
"""

import base64
import os
import re
Expand All @@ -20,7 +32,6 @@


class DataSourceOVF(sources.DataSource):

dsname = "OVF"

def __init__(self, sys_cfg, distro, paths):
Expand Down Expand Up @@ -142,7 +153,7 @@ def read_ovf_environment(contents, read_network=False):
cfg_props = ["password"]
md_props = ["seedfrom", "local-hostname", "public-keys", "instance-id"]
network_props = ["network-config"]
for (prop, val) in props.items():
for prop, val in props.items():
if prop == "hostname":
prop = "local-hostname"
if prop in md_props:
Expand Down Expand Up @@ -220,10 +231,9 @@ def maybe_cdrom_device(devname):
# Transport functions are called with no arguments and return
# either None (indicating not present) or string content of an ovf-env.xml
def transport_iso9660(require_iso=True):

# Go through mounts to see if it was already mounted
mounts = util.mounts()
for (dev, info) in mounts.items():
for dev, info in mounts.items():
fstype = info["fstype"]
if fstype != "iso9660" and require_iso:
continue
Expand Down Expand Up @@ -259,21 +269,43 @@ def transport_iso9660(require_iso=True):


def transport_vmware_guestinfo():
rpctool = "vmware-rpctool"
not_found = None
if not subp.which(rpctool):
return not_found
cmd = [rpctool, "info-get guestinfo.ovfEnv"]
is_vmtoolsd = False
rpctool = subp.which("vmware-rpctool")

if not rpctool:
rpctool = subp.which("vmtoolsd")
is_vmtoolsd = True

if not rpctool:
return None

try:
return transport_vmware_guestinfo_rpctool(rpctool, is_vmtoolsd)
except subp.ProcessExecutionError:
rpctool = subp.which("vmtoolsd")
is_vmtoolsd = True
return transport_vmware_guestinfo_rpctool(rpctool, is_vmtoolsd)


def transport_vmware_guestinfo_rpctool(rpctool, is_vmtoolsd=False):
args = [rpctool]
if is_vmtoolsd:
args.append("--cmd")
args.append("info-get guestinfo.ovfEnv")

try:
out, _err = subp.subp(cmd)
out, _err = subp.subp(args)
if out:
return out
LOG.debug("cmd %s exited 0 with empty stdout: %s", cmd, out)
LOG.debug("cmd %s exited 0 with empty stdout: %s", args, out)
except subp.ProcessExecutionError as e:
if e.exit_code != 1:
LOG.warning("%s exited with code %d", rpctool, e.exit_code)
LOG.debug(e)
return not_found
if not is_vmtoolsd:
raise e

return None


def find_child(node, filter_func):
Expand All @@ -287,7 +319,6 @@ def find_child(node, filter_func):


def get_properties(contents):

dom = minidom.parseString(contents)
if dom.documentElement.localName != "Environment":
raise XmlError("No Environment Node")
Expand Down
108 changes: 74 additions & 34 deletions cloudinit/sources/DataSourceVMware.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
why netifaces was used in the first place in order to either smooth the
transition away from netifaces or embrace it further up the cloud-init
stack.
This module may be unit tested by running the following command from the root
of the Cloud-Init repository:
make clean_pyc && PYTHONPATH="$(pwd)" \
python3 -m pytest -v --log-level=DEBUG \
tests/unittests/sources/test_vmware.py
"""

import collections
Expand Down Expand Up @@ -89,6 +96,7 @@
DATA_ACCESS_METHOD_GUESTINFO = "guestinfo"
DATA_ACCESS_METHOD_IMC = "imc"

VMTOOLSD = which("vmtoolsd")
VMWARE_RPCTOOL = which("vmware-rpctool")
REDACT = "redact"
CLEANUP_GUESTINFO = "cleanup-guestinfo"
Expand Down Expand Up @@ -146,7 +154,12 @@ def __init__(self, sys_cfg, distro, paths, ud_proc=None):

self.cfg = {}
self.data_access_method = None
self.vmware_rpctool = VMWARE_RPCTOOL
if VMWARE_RPCTOOL:
self.rpctool = VMWARE_RPCTOOL
elif VMTOOLSD:
self.rpctool = VMTOOLSD
else:
self.rpctool = None

# A list includes all possible data transports, each tuple represents
# one data transport type. This datasource will try to get data from
Expand Down Expand Up @@ -237,7 +250,7 @@ def setup(self, is_new_instance):

# Reflect any possible local IPv4 or IPv6 addresses in the guest
# info.
advertise_local_ip_addrs(host_info)
advertise_local_ip_addrs(host_info, self.rpctool)

# Ensure the metadata gets updated with information about the
# host, including the network interfaces, default IP addresses,
Expand Down Expand Up @@ -313,7 +326,7 @@ def redact_keys(self):
keys_to_redact = self.metadata[CLEANUP_GUESTINFO]

if self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO:
guestinfo_redact_keys(keys_to_redact, self.vmware_rpctool)
guestinfo_redact_keys(keys_to_redact, self.rpctool)

def get_envvar_data_fn(self):
"""
Expand All @@ -331,11 +344,23 @@ def get_guestinfo_data_fn(self):
"""
check to see if there is data via the guestinfo transport
"""
if not self.rpctool:
return (None, None, None)

md, ud, vd = None, None, None
if self.vmware_rpctool:
md = guestinfo("metadata", self.vmware_rpctool)
ud = guestinfo("userdata", self.vmware_rpctool)
vd = guestinfo("vendordata", self.vmware_rpctool)
try:
md = guestinfo("metadata", self.rpctool)
ud = guestinfo("userdata", self.rpctool)
vd = guestinfo("vendordata", self.rpctool)
except ProcessExecutionError:
self.rpctool = VMTOOLSD

if not self.rpctool:
return (None, None, None)

md = guestinfo("metadata", self.rpctool)
ud = guestinfo("userdata", self.rpctool)
vd = guestinfo("vendordata", self.rpctool)

return (md, ud, vd)

Expand Down Expand Up @@ -447,7 +472,7 @@ def get_none_if_empty_val(val):
return val


def advertise_local_ip_addrs(host_info):
def advertise_local_ip_addrs(host_info, rpctool=VMWARE_RPCTOOL):
"""
advertise_local_ip_addrs gets the local IP address information from
the provided host_info map and sets the addresses in the guestinfo
Expand All @@ -460,12 +485,12 @@ def advertise_local_ip_addrs(host_info):
# info.
local_ipv4 = host_info.get(LOCAL_IPV4)
if local_ipv4:
guestinfo_set_value(LOCAL_IPV4, local_ipv4)
guestinfo_set_value(LOCAL_IPV4, local_ipv4, rpctool)
LOG.info("advertised local ipv4 address %s in guestinfo", local_ipv4)

local_ipv6 = host_info.get(LOCAL_IPV6)
if local_ipv6:
guestinfo_set_value(LOCAL_IPV6, local_ipv6)
guestinfo_set_value(LOCAL_IPV6, local_ipv6, rpctool)
LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6)


Expand Down Expand Up @@ -507,46 +532,53 @@ def guestinfo_envvar_get_value(key):
return handle_returned_guestinfo_val(key, os.environ.get(env_key, ""))


def guestinfo(key, vmware_rpctool=VMWARE_RPCTOOL):
def guestinfo(key, rpctool=VMWARE_RPCTOOL):
"""
guestinfo returns the guestinfo value for the provided key, decoding
the value when required
"""
val = guestinfo_get_value(key, vmware_rpctool)
val = guestinfo_get_value(key, rpctool)
if not val:
return None
enc_type = guestinfo_get_value(key + ".encoding", vmware_rpctool)
enc_type = guestinfo_get_value(key + ".encoding", rpctool)
return decode(get_guestinfo_key_name(key), enc_type, val)


def guestinfo_get_value(key, vmware_rpctool=VMWARE_RPCTOOL):
def guestinfo_get_value(key, rpctool=VMWARE_RPCTOOL):
"""
Returns a guestinfo value for the specified key.
"""
LOG.debug("Getting guestinfo value for key %s", key)
LOG.debug("Getting guestinfo value for key %s with %s", key, rpctool)

args = [rpctool]
if rpctool == VMTOOLSD:
args.append("--cmd")
args.append("info-get " + get_guestinfo_key_name(key))

try:
(stdout, stderr) = subp(
[
vmware_rpctool,
"info-get " + get_guestinfo_key_name(key),
]
)
(stdout, stderr) = subp(args)
if stderr == NOVAL:
LOG.debug("No value found for key %s", key)
elif not stdout:
LOG.error("Failed to get guestinfo value for key %s", key)
return handle_returned_guestinfo_val(key, stdout)
except ProcessExecutionError as error:
# No matter the tool used to access the data, if NOVAL was returned on
# stderr, do not raise an exception.
if error.stderr == NOVAL:
LOG.debug("No value found for key %s", key)
else:
# Any other result gets logged as an error, and if the tool was
# VMWARE_RPCTOOL, then raise the exception so the caller can try
# again with VMTOOLSD.
util.logexc(
LOG,
"Failed to get guestinfo value for key %s: %s",
key,
error,
)
if rpctool == VMWARE_RPCTOOL:
raise error
except Exception:
util.logexc(
LOG,
Expand All @@ -558,7 +590,7 @@ def guestinfo_get_value(key, vmware_rpctool=VMWARE_RPCTOOL):
return None


def guestinfo_set_value(key, value, vmware_rpctool=VMWARE_RPCTOOL):
def guestinfo_set_value(key, value, rpctool=VMWARE_RPCTOOL):
"""
Sets a guestinfo value for the specified key. Set value to an empty string
to clear an existing guestinfo key.
Expand All @@ -571,24 +603,34 @@ def guestinfo_set_value(key, value, vmware_rpctool=VMWARE_RPCTOOL):
if value == "":
value = " "

LOG.debug("Setting guestinfo key=%s to value=%s", key, value)
LOG.debug(
"Setting guestinfo key=%s to value=%s with %s",
key,
value,
rpctool,
)

args = [rpctool]
if rpctool == VMTOOLSD:
args.append("--cmd")
args.append("info-set %s %s" % (get_guestinfo_key_name(key), value))

try:
subp(
[
vmware_rpctool,
"info-set %s %s" % (get_guestinfo_key_name(key), value),
]
)
subp(args)
return True
except ProcessExecutionError as error:
# Any error result gets logged as an error, and if the tool was
# VMWARE_RPCTOOL, then raise the exception so the caller can try
# again with VMTOOLSD.
util.logexc(
LOG,
"Failed to set guestinfo key=%s to value=%s: %s",
key,
value,
error,
)
if rpctool == VMWARE_RPCTOOL:
raise error
except Exception:
util.logexc(
LOG,
Expand All @@ -601,7 +643,7 @@ def guestinfo_set_value(key, value, vmware_rpctool=VMWARE_RPCTOOL):
return None


def guestinfo_redact_keys(keys, vmware_rpctool=VMWARE_RPCTOOL):
def guestinfo_redact_keys(keys, rpctool=VMWARE_RPCTOOL):
"""
guestinfo_redact_keys redacts guestinfo of all of the keys in the given
list. each key will have its value set to "---". Since the value is valid
Expand All @@ -614,12 +656,10 @@ def guestinfo_redact_keys(keys, vmware_rpctool=VMWARE_RPCTOOL):
for key in keys:
key_name = get_guestinfo_key_name(key)
LOG.info("clearing %s", key_name)
if not guestinfo_set_value(
key, GUESTINFO_EMPTY_YAML_VAL, vmware_rpctool
):
if not guestinfo_set_value(key, GUESTINFO_EMPTY_YAML_VAL, rpctool):
LOG.error("failed to clear %s", key_name)
LOG.info("clearing %s.encoding", key_name)
if not guestinfo_set_value(key + ".encoding", "", vmware_rpctool):
if not guestinfo_set_value(key + ".encoding", "", rpctool):
LOG.error("failed to clear %s.encoding", key_name)


Expand Down
27 changes: 27 additions & 0 deletions doc/rtd/reference/datasources/ovf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ OVF
The OVF datasource provides a datasource for reading data from an
`Open Virtualization Format`_ ISO transport.

Graceful rpctool fallback
-------------------------

The datasource initially attempts to use the program ``vmware-rpctool`` if it
is available. However, if the program returns a non-zero exit code, then the
datasource falls back to using the program ``vmtoolsd`` with the ``--cmd``
argument.

On some older versions of ESXi and open-vm-tools, the ``vmware-rpctool``
program is much more performant than ``vmtoolsd``. While this gap was
closed, it is not reasonable to expect the guest where Cloud-Init is running to
know whether the underlying hypervisor has the patch.

Additionally, vSphere VMs may have the following present in their VMX file:

.. code-block:: ini
guest_rpc.rpci.auth.cmd.info-set = "TRUE"
guest_rpc.rpci.auth.cmd.info-get = "TRUE"
The above configuration causes the ``vmware-rpctool`` command to return a
non-zero exit code with the error message ``Permission denied``. If this should
occur, the datasource falls back to using ``vmtoolsd``.

Additional information
----------------------

For further information see a full working example in ``cloud-init``'s
source code tree in :file:`doc/sources/ovf`.

Expand Down
Loading

0 comments on commit 4fb5978

Please sign in to comment.