diff options
Diffstat (limited to 'cloudinit/sources')
-rw-r--r-- | cloudinit/sources/DataSourceOVF.py | 554 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceVMware.py | 196 | ||||
-rw-r--r-- | cloudinit/sources/helpers/vmware/imc/config.py | 6 | ||||
-rw-r--r-- | cloudinit/sources/helpers/vmware/imc/guestcust_util.py | 470 |
4 files changed, 629 insertions, 597 deletions
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py index 05bf84c2..7baef3a5 100644 --- a/cloudinit/sources/DataSourceOVF.py +++ b/cloudinit/sources/DataSourceOVF.py @@ -11,49 +11,13 @@ import base64 import os import re -import time from xml.dom import minidom -from cloudinit import dmi from cloudinit import log as logging from cloudinit import safeyaml, sources, subp, util -from cloudinit.sources.helpers.vmware.imc.config import Config -from cloudinit.sources.helpers.vmware.imc.config_custom_script import ( - PostCustomScript, - PreCustomScript, -) -from cloudinit.sources.helpers.vmware.imc.config_file import ConfigFile -from cloudinit.sources.helpers.vmware.imc.config_nic import NicConfigurator -from cloudinit.sources.helpers.vmware.imc.config_passwd import ( - PasswordConfigurator, -) -from cloudinit.sources.helpers.vmware.imc.guestcust_error import ( - GuestCustErrorEnum, -) -from cloudinit.sources.helpers.vmware.imc.guestcust_event import ( - GuestCustEventEnum as GuestCustEvent, -) -from cloudinit.sources.helpers.vmware.imc.guestcust_state import ( - GuestCustStateEnum, -) -from cloudinit.sources.helpers.vmware.imc.guestcust_util import ( - enable_nics, - get_nics_to_enable, - get_tools_config, - set_customization_status, - set_gc_status, -) LOG = logging.getLogger(__name__) -CONFGROUPNAME_GUESTCUSTOMIZATION = "deployPkg" -GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS = "enable-custom-scripts" -VMWARE_IMC_DIR = "/var/run/vmware-imc" - - -class GuestCustScriptDisabled(Exception): - pass - class DataSourceOVF(sources.DataSource): @@ -66,11 +30,7 @@ class DataSourceOVF(sources.DataSource): self.environment = None self.cfg = {} self.supported_seed_starts = ("/", "file://") - self.vmware_customization_supported = True self._network_config = None - self._vmware_nics_to_enable = None - self._vmware_cust_conf = None - self._vmware_cust_found = False def __str__(self): root = sources.DataSource.__str__(self) @@ -81,8 +41,6 @@ class DataSourceOVF(sources.DataSource): md = {} ud = "" vd = "" - vmwareImcConfigFilePath = None - nicspath = None defaults = { "instance-id": "iid-dsovf", @@ -90,305 +48,12 @@ class DataSourceOVF(sources.DataSource): (seedfile, contents) = get_ovf_env(self.paths.seed_dir) - system_type = dmi.read_dmi_data("system-product-name") - if system_type is None: - LOG.debug("No system-product-name found") - if seedfile: # Found a seed dir seed = os.path.join(self.paths.seed_dir, seedfile) (md, ud, cfg) = read_ovf_environment(contents) self.environment = contents found.append(seed) - elif system_type and "vmware" in system_type.lower(): - LOG.debug("VMware Virtualization Platform found") - allow_vmware_cust = False - allow_raw_data = False - if not self.vmware_customization_supported: - LOG.debug( - "Skipping the check for VMware Customization support" - ) - else: - allow_vmware_cust = not util.get_cfg_option_bool( - self.sys_cfg, "disable_vmware_customization", True - ) - allow_raw_data = util.get_cfg_option_bool( - self.ds_cfg, "allow_raw_data", True - ) - - if not (allow_vmware_cust or allow_raw_data): - LOG.debug("Customization for VMware platform is disabled.") - else: - search_paths = ( - "/usr/lib/vmware-tools", - "/usr/lib64/vmware-tools", - "/usr/lib/open-vm-tools", - "/usr/lib64/open-vm-tools", - "/usr/lib/x86_64-linux-gnu/open-vm-tools", - "/usr/lib/aarch64-linux-gnu/open-vm-tools", - ) - - plugin = "libdeployPkgPlugin.so" - deployPkgPluginPath = None - for path in search_paths: - deployPkgPluginPath = search_file(path, plugin) - if deployPkgPluginPath: - LOG.debug( - "Found the customization plugin at %s", - deployPkgPluginPath, - ) - break - - if deployPkgPluginPath: - # When the VM is powered on, the "VMware Tools" daemon - # copies the customization specification file to - # /var/run/vmware-imc directory. cloud-init code needs - # to search for the file in that directory which indicates - # that required metadata and userdata files are now - # present. - max_wait = get_max_wait_from_cfg(self.ds_cfg) - vmwareImcConfigFilePath = util.log_time( - logfunc=LOG.debug, - msg="waiting for configuration file", - func=wait_for_imc_cfg_file, - args=("cust.cfg", max_wait), - ) - else: - LOG.debug("Did not find the customization plugin.") - - md_path = None - if vmwareImcConfigFilePath: - imcdirpath = os.path.dirname(vmwareImcConfigFilePath) - cf = ConfigFile(vmwareImcConfigFilePath) - self._vmware_cust_conf = Config(cf) - LOG.debug( - "Found VMware Customization Config File at %s", - vmwareImcConfigFilePath, - ) - try: - (md_path, ud_path, nicspath) = collect_imc_file_paths( - self._vmware_cust_conf - ) - except FileNotFoundError as e: - _raise_error_status( - "File(s) missing in directory", - e, - GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - # Don't handle the customization for below 2 cases: - # 1. meta data is found, allow_raw_data is False. - # 2. no meta data is found, allow_vmware_cust is False. - if md_path and not allow_raw_data: - LOG.debug("Customization using raw data is disabled.") - # reset vmwareImcConfigFilePath to None to avoid - # customization for VMware platform - vmwareImcConfigFilePath = None - if md_path is None and not allow_vmware_cust: - LOG.debug( - "Customization using VMware config is disabled." - ) - vmwareImcConfigFilePath = None - else: - LOG.debug("Did not find VMware Customization Config File") - - use_raw_data = bool(vmwareImcConfigFilePath and md_path) - if use_raw_data: - set_gc_status(self._vmware_cust_conf, "Started") - LOG.debug("Start to load cloud-init meta data and user data") - try: - (md, ud, cfg, network) = load_cloudinit_data(md_path, ud_path) - - if network: - self._network_config = network - else: - self._network_config = ( - self.distro.generate_fallback_config() - ) - - except safeyaml.YAMLError as e: - _raise_error_status( - "Error parsing the cloud-init meta data", - e, - GuestCustErrorEnum.GUESTCUST_ERROR_WRONG_META_FORMAT, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - except Exception as e: - _raise_error_status( - "Error loading cloud-init configuration", - e, - GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - - self._vmware_cust_found = True - found.append("vmware-tools") - - util.del_dir(imcdirpath) - set_customization_status( - GuestCustStateEnum.GUESTCUST_STATE_DONE, - GuestCustErrorEnum.GUESTCUST_ERROR_SUCCESS, - ) - set_gc_status(self._vmware_cust_conf, "Successful") - - elif vmwareImcConfigFilePath: - # Load configuration from vmware_imc - self._vmware_nics_to_enable = "" - try: - set_gc_status(self._vmware_cust_conf, "Started") - - (md, ud, cfg) = read_vmware_imc(self._vmware_cust_conf) - self._vmware_nics_to_enable = get_nics_to_enable(nicspath) - product_marker = self._vmware_cust_conf.marker_id - hasmarkerfile = check_marker_exists( - product_marker, os.path.join(self.paths.cloud_dir, "data") - ) - special_customization = product_marker and not hasmarkerfile - customscript = self._vmware_cust_conf.custom_script_name - - # In case there is a custom script, check whether VMware - # Tools configuration allow the custom script to run. - if special_customization and customscript: - defVal = "false" - if self._vmware_cust_conf.default_run_post_script: - LOG.debug( - "Set default value to true due to" - " customization configuration." - ) - defVal = "true" - - custScriptConfig = get_tools_config( - CONFGROUPNAME_GUESTCUSTOMIZATION, - GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS, - defVal, - ) - if custScriptConfig.lower() != "true": - # Update the customization status if custom script - # is disabled - msg = "Custom script is disabled by VM Administrator" - LOG.debug(msg) - set_customization_status( - GuestCustStateEnum.GUESTCUST_STATE_RUNNING, - GuestCustErrorEnum.GUESTCUST_ERROR_SCRIPT_DISABLED, - ) - raise GuestCustScriptDisabled(msg) - - ccScriptsDir = os.path.join( - self.paths.get_cpath("scripts"), "per-instance" - ) - except GuestCustScriptDisabled as e: - LOG.debug("GuestCustScriptDisabled") - _raise_error_status( - "Error parsing the customization Config File", - e, - GuestCustErrorEnum.GUESTCUST_ERROR_SCRIPT_DISABLED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - except Exception as e: - _raise_error_status( - "Error parsing the customization Config File", - e, - GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - - if special_customization: - if customscript: - try: - precust = PreCustomScript(customscript, imcdirpath) - precust.execute() - except Exception as e: - _raise_error_status( - "Error executing pre-customization script", - e, - GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - - try: - LOG.debug("Preparing the Network configuration") - self._network_config = get_network_config_from_conf( - self._vmware_cust_conf, True, True, self.distro.osfamily - ) - except Exception as e: - _raise_error_status( - "Error preparing Network Configuration", - e, - GuestCustEvent.GUESTCUST_EVENT_NETWORK_SETUP_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - - if special_customization: - LOG.debug("Applying password customization") - pwdConfigurator = PasswordConfigurator() - adminpwd = self._vmware_cust_conf.admin_password - try: - resetpwd = self._vmware_cust_conf.reset_password - if adminpwd or resetpwd: - pwdConfigurator.configure( - adminpwd, resetpwd, self.distro - ) - else: - LOG.debug("Changing password is not needed") - except Exception as e: - _raise_error_status( - "Error applying Password Configuration", - e, - GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - - if customscript: - try: - postcust = PostCustomScript( - customscript, imcdirpath, ccScriptsDir - ) - postcust.execute() - except Exception as e: - _raise_error_status( - "Error executing post-customization script", - e, - GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - - if product_marker: - try: - setup_marker_files( - product_marker, - os.path.join(self.paths.cloud_dir, "data"), - ) - except Exception as e: - _raise_error_status( - "Error creating marker files", - e, - GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED, - vmwareImcConfigFilePath, - self._vmware_cust_conf, - ) - - self._vmware_cust_found = True - found.append("vmware-tools") - - # TODO: Need to set the status to DONE only when the - # customization is done successfully. - util.del_dir(os.path.dirname(vmwareImcConfigFilePath)) - enable_nics(self._vmware_nics_to_enable) - set_customization_status( - GuestCustStateEnum.GUESTCUST_STATE_DONE, - GuestCustErrorEnum.GUESTCUST_ERROR_SUCCESS, - ) - set_gc_status(self._vmware_cust_conf, "Successful") - else: np = [ ("com.vmware.guestInfo", transport_vmware_guestinfo), @@ -438,9 +103,6 @@ class DataSourceOVF(sources.DataSource): return True def _get_subplatform(self): - system_type = dmi.read_dmi_data("system-product-name").lower() - if system_type == "vmware": - return "vmware (%s)" % self.seed return "ovf (%s)" % self.seed def get_public_ssh_keys(self): @@ -468,94 +130,6 @@ class DataSourceOVFNet(DataSourceOVF): DataSourceOVF.__init__(self, sys_cfg, distro, paths) self.seed_dir = os.path.join(paths.seed_dir, "ovf-net") self.supported_seed_starts = ("http://", "https://") - self.vmware_customization_supported = False - - -def get_max_wait_from_cfg(cfg): - default_max_wait = 15 - max_wait_cfg_option = "vmware_cust_file_max_wait" - max_wait = default_max_wait - - if not cfg: - return max_wait - - try: - max_wait = int(cfg.get(max_wait_cfg_option, default_max_wait)) - except ValueError: - LOG.warning( - "Failed to get '%s', using %s", - max_wait_cfg_option, - default_max_wait, - ) - - if max_wait < 0: - LOG.warning( - "Invalid value '%s' for '%s', using '%s' instead", - max_wait, - max_wait_cfg_option, - default_max_wait, - ) - max_wait = default_max_wait - - return max_wait - - -def wait_for_imc_cfg_file( - filename, maxwait=180, naplen=5, dirpath="/var/run/vmware-imc" -): - waited = 0 - if maxwait <= naplen: - naplen = 1 - - while waited < maxwait: - fileFullPath = os.path.join(dirpath, filename) - if os.path.isfile(fileFullPath): - return fileFullPath - LOG.debug("Waiting for VMware Customization Config File") - time.sleep(naplen) - waited += naplen - return None - - -def get_network_config_from_conf( - config, use_system_devices=True, configure=False, osfamily=None -): - nicConfigurator = NicConfigurator(config.nics, use_system_devices) - nics_cfg_list = nicConfigurator.generate(configure, osfamily) - - return get_network_config( - nics_cfg_list, config.name_servers, config.dns_suffixes - ) - - -def get_network_config(nics=None, nameservers=None, search=None): - config_list = nics - - if nameservers or search: - config_list.append( - {"type": "nameserver", "address": nameservers, "search": search} - ) - - return {"version": 1, "config": config_list} - - -# This will return a dict with some content -# meta-data, user-data, some config -def read_vmware_imc(config): - md = {} - cfg = {} - ud = None - if config.host_name: - if config.domain_name: - md["local-hostname"] = config.host_name + "." + config.domain_name - else: - md["local-hostname"] = config.host_name - - if config.timezone: - cfg["timezone"] = config.timezone - - md["instance-id"] = "iid-vmware-imc" - return (md, ud, cfg) # This will return a dict with some content @@ -745,17 +319,6 @@ def get_properties(contents): return props -def search_file(dirpath, filename): - if not dirpath or not filename: - return None - - for root, _dirs, files in os.walk(dirpath): - if filename in files: - return os.path.join(root, filename) - - return None - - class XmlError(Exception): pass @@ -772,80 +335,6 @@ def get_datasource_list(depends): return sources.list_from_depends(depends, datasources) -# To check if marker file exists -def check_marker_exists(markerid, marker_dir): - """ - Check the existence of a marker file. - Presence of marker file determines whether a certain code path is to be - executed. It is needed for partial guest customization in VMware. - @param markerid: is an unique string representing a particular product - marker. - @param: marker_dir: The directory in which markers exist. - """ - if not markerid: - return False - markerfile = os.path.join(marker_dir, ".markerfile-" + markerid + ".txt") - if os.path.exists(markerfile): - return True - return False - - -# Create a marker file -def setup_marker_files(markerid, marker_dir): - """ - Create a new marker file. - Marker files are unique to a full customization workflow in VMware - environment. - @param markerid: is an unique string representing a particular product - marker. - @param: marker_dir: The directory in which markers exist. - - """ - LOG.debug("Handle marker creation") - markerfile = os.path.join(marker_dir, ".markerfile-" + markerid + ".txt") - for fname in os.listdir(marker_dir): - if fname.startswith(".markerfile"): - util.del_file(os.path.join(marker_dir, fname)) - open(markerfile, "w").close() - - -def _raise_error_status(prefix, error, event, config_file, conf): - """ - Raise error and send customization status to the underlying VMware - Virtualization Platform. Also, cleanup the imc directory. - """ - LOG.debug("%s: %s", prefix, error) - set_customization_status(GuestCustStateEnum.GUESTCUST_STATE_RUNNING, event) - set_gc_status(conf, prefix) - util.del_dir(os.path.dirname(config_file)) - raise error - - -def load_cloudinit_data(md_path, ud_path): - """ - Load the cloud-init meta data, user data, cfg and network from the - given files - - @return: 4-tuple of configuration - metadata, userdata, cfg={}, network - - @raises: FileNotFoundError if md_path or ud_path are absent - """ - LOG.debug("load meta data from: %s: user data from: %s", md_path, ud_path) - md = {} - ud = None - network = None - - md = safeload_yaml_or_dict(util.load_file(md_path)) - - if "network" in md: - network = md["network"] - - if ud_path: - ud = util.load_file(ud_path).replace("\r", "") - return md, ud, {}, network - - def safeload_yaml_or_dict(data): """ The meta data could be JSON or YAML. Since YAML is a strict superset of @@ -857,47 +346,4 @@ def safeload_yaml_or_dict(data): return safeyaml.load(data) -def collect_imc_file_paths(cust_conf): - """ - collect all the other imc files. - - metadata is preferred to nics.txt configuration data. - - If metadata file exists because it is specified in customization - configuration, then metadata is required and userdata is optional. - - @return a 3-tuple containing desired configuration file paths if present - Expected returns: - 1. user provided metadata and userdata (md_path, ud_path, None) - 2. user provided metadata (md_path, None, None) - 3. user-provided network config (None, None, nics_path) - 4. No config found (None, None, None) - """ - md_path = None - ud_path = None - nics_path = None - md_file = cust_conf.meta_data_name - if md_file: - md_path = os.path.join(VMWARE_IMC_DIR, md_file) - if not os.path.exists(md_path): - raise FileNotFoundError( - "meta data file is not found: %s" % md_path - ) - - ud_file = cust_conf.user_data_name - if ud_file: - ud_path = os.path.join(VMWARE_IMC_DIR, ud_file) - if not os.path.exists(ud_path): - raise FileNotFoundError( - "user data file is not found: %s" % ud_path - ) - else: - nics_path = os.path.join(VMWARE_IMC_DIR, "nics.txt") - if not os.path.exists(nics_path): - LOG.debug("%s does not exist.", nics_path) - nics_path = None - - return md_path, ud_path, nics_path - - # vi: ts=4 expandtab diff --git a/cloudinit/sources/DataSourceVMware.py b/cloudinit/sources/DataSourceVMware.py index 308e02e8..07a80222 100644 --- a/cloudinit/sources/DataSourceVMware.py +++ b/cloudinit/sources/DataSourceVMware.py @@ -1,9 +1,10 @@ # Cloud-Init DataSource for VMware # -# Copyright (c) 2018-2021 VMware, Inc. All Rights Reserved. +# Copyright (c) 2018-2022 VMware, Inc. All Rights Reserved. # # Authors: Anish Swaminathan <anishs@vmware.com> # Andrew Kutz <akutz@vmware.com> +# Pengpeng Sun <pengpengs@vmware.com> # # This file is part of cloud-init. See LICENSE file for license information. @@ -14,6 +15,7 @@ multiple transports types, including: * EnvVars * GuestInfo + * IMC (Guest Customization) Netifaces (https://github.com/al45tair/netifaces) @@ -74,6 +76,7 @@ import netifaces from cloudinit import dmi from cloudinit import log as logging from cloudinit import net, sources, util +from cloudinit.sources.helpers.vmware.imc import guestcust_util from cloudinit.subp import ProcessExecutionError, subp, which PRODUCT_UUID_FILE_PATH = "/sys/class/dmi/id/product_uuid" @@ -81,8 +84,10 @@ PRODUCT_UUID_FILE_PATH = "/sys/class/dmi/id/product_uuid" LOG = logging.getLogger(__name__) NOVAL = "No value found" +# Data transports names DATA_ACCESS_METHOD_ENVVAR = "envvar" DATA_ACCESS_METHOD_GUESTINFO = "guestinfo" +DATA_ACCESS_METHOD_IMC = "imc" VMWARE_RPCTOOL = which("vmware-rpctool") REDACT = "redact" @@ -116,14 +121,22 @@ class DataSourceVMware(sources.DataSource): Network Config Version 2 - http://bit.ly/cloudinit-net-conf-v2 For example, CentOS 7's official cloud-init package is version - 0.7.9 and does not support Network Config Version 2. However, - this datasource still supports supplying Network Config Version 2 - data as long as the Linux distro's cloud-init package is new - enough to parse the data. + 0.7.9 and does not support Network Config Version 2. - The metadata key "network.encoding" may be used to indicate the - format of the metadata key "network". Valid encodings are base64 - and gzip+base64. + imc transport: + Either Network Config Version 1 or Network Config Version 2 is + supported which depends on the customization type. + For LinuxPrep customization, Network config Version 1 data is + parsed from the customization specification. + For CloudinitPrep customization, Network config Version 2 data + is parsed from the customization specification. + + envvar and guestinfo tranports: + Network Config Version 2 data is supported as long as the Linux + distro's cloud-init package is new enough to parse the data. + The metadata key "network.encoding" may be used to indicate the + format of the metadata key "network". Valid encodings are base64 + and gzip+base64. """ dsname = "VMware" @@ -131,9 +144,27 @@ class DataSourceVMware(sources.DataSource): def __init__(self, sys_cfg, distro, paths, ud_proc=None): sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc) + self.cfg = {} self.data_access_method = None self.vmware_rpctool = VMWARE_RPCTOOL + # A list includes all possible data transports, each tuple represents + # one data transport type. This datasource will try to get data from + # each of transports follows the tuples order in this list. + # A tuple has 3 elements which are: + # 1. The transport name + # 2. The function name to get data for the transport + # 3. A boolean tells whether the transport requires VMware platform + self.possible_data_access_method_list = [ + (DATA_ACCESS_METHOD_ENVVAR, self.get_envvar_data_fn, False), + (DATA_ACCESS_METHOD_GUESTINFO, self.get_guestinfo_data_fn, True), + (DATA_ACCESS_METHOD_IMC, self.get_imc_data_fn, True), + ] + + def __str__(self): + root = sources.DataSource.__str__(self) + return "%s [seed=%s]" % (root, self.data_access_method) + def _get_data(self): """ _get_data loads the metadata, userdata, and vendordata from one of @@ -141,6 +172,7 @@ class DataSourceVMware(sources.DataSource): * envvars * guestinfo + * imc Please note when updating this function with support for new data transports, the order should match the order in the dscheck_VMware @@ -152,35 +184,18 @@ class DataSourceVMware(sources.DataSource): # access method. md, ud, vd = None, None, None - # First check to see if there is data via env vars. - if os.environ.get(VMX_GUESTINFO, ""): - md = guestinfo_envvar("metadata") - ud = guestinfo_envvar("userdata") - vd = guestinfo_envvar("vendordata") - + # Crawl data from all possible data transports + for ( + data_access_method, + get_data_fn, + require_vmware_platform, + ) in self.possible_data_access_method_list: + if require_vmware_platform and not is_vmware_platform(): + continue + (md, ud, vd) = get_data_fn() if md or ud or vd: - self.data_access_method = DATA_ACCESS_METHOD_ENVVAR - - # At this point, all additional data transports are valid only on - # a VMware platform. - if not self.data_access_method: - system_type = dmi.read_dmi_data("system-product-name") - if system_type is None: - LOG.debug("No system-product-name found") - return False - if "vmware" not in system_type.lower(): - LOG.debug("Not a VMware platform") - return False - - # If no data was detected, check the guestinfo transport next. - if not self.data_access_method: - if self.vmware_rpctool: - md = guestinfo("metadata", self.vmware_rpctool) - ud = guestinfo("userdata", self.vmware_rpctool) - vd = guestinfo("vendordata", self.vmware_rpctool) - - if md or ud or vd: - self.data_access_method = DATA_ACCESS_METHOD_GUESTINFO + self.data_access_method = data_access_method + break if not self.data_access_method: LOG.error("failed to find a valid data access method") @@ -241,6 +256,8 @@ class DataSourceVMware(sources.DataSource): get_key_name_fn = get_guestinfo_envvar_key_name elif self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO: get_key_name_fn = get_guestinfo_key_name + elif self.data_access_method == DATA_ACCESS_METHOD_IMC: + get_key_name_fn = get_imc_key_name else: return sources.METADATA_UNKNOWN @@ -249,6 +266,12 @@ class DataSourceVMware(sources.DataSource): get_key_name_fn("metadata"), ) + # The data sources' config_obj is a cloud-config formatted + # object that came to it from ways other than cloud-config + # because cloud-config content would be handled elsewhere + def get_config_obj(self): + return self.cfg + @property def network_config(self): if "network" in self.metadata: @@ -292,6 +315,98 @@ class DataSourceVMware(sources.DataSource): if self.data_access_method == DATA_ACCESS_METHOD_GUESTINFO: guestinfo_redact_keys(keys_to_redact, self.vmware_rpctool) + def get_envvar_data_fn(self): + """ + check to see if there is data via env vars + """ + md, ud, vd = None, None, None + if os.environ.get(VMX_GUESTINFO, ""): + md = guestinfo_envvar("metadata") + ud = guestinfo_envvar("userdata") + vd = guestinfo_envvar("vendordata") + + return (md, ud, vd) + + def get_guestinfo_data_fn(self): + """ + check to see if there is data via the guestinfo transport + """ + 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) + + return (md, ud, vd) + + def get_imc_data_fn(self): + """ + check to see if there is data via vmware guest customization + """ + md, ud, vd = None, None, None + + # Check if vmware guest customization is enabled. + allow_vmware_cust = guestcust_util.is_vmware_cust_enabled(self.sys_cfg) + allow_raw_data_cust = guestcust_util.is_raw_data_cust_enabled( + self.ds_cfg + ) + if not allow_vmware_cust and not allow_raw_data_cust: + LOG.debug("Customization for VMware platform is disabled") + return (md, ud, vd) + + # Check if "VMware Tools" plugin is available. + if not guestcust_util.is_cust_plugin_available(): + return (md, ud, vd) + + # Wait for vmware guest customization configuration file. + cust_cfg_file = guestcust_util.get_cust_cfg_file(self.ds_cfg) + if cust_cfg_file is None: + return (md, ud, vd) + + # Check what type of guest customization is this. + cust_cfg_dir = os.path.dirname(cust_cfg_file) + cust_cfg = guestcust_util.parse_cust_cfg(cust_cfg_file) + ( + is_vmware_cust_cfg, + is_raw_data_cust_cfg, + ) = guestcust_util.get_cust_cfg_type(cust_cfg) + + # Get data only if guest customization type and flag matches. + if is_vmware_cust_cfg and allow_vmware_cust: + LOG.debug("Getting data via VMware customization configuration") + (md, ud, vd, self.cfg) = guestcust_util.get_data_from_imc_cust_cfg( + self.paths.cloud_dir, + self.paths.get_cpath("scripts"), + cust_cfg, + cust_cfg_dir, + self.distro, + ) + elif is_raw_data_cust_cfg and allow_raw_data_cust: + LOG.debug( + "Getting data via VMware raw cloudinit data " + "customization configuration" + ) + (md, ud, vd) = guestcust_util.get_data_from_imc_raw_data_cust_cfg( + cust_cfg + ) + else: + LOG.debug("No allowed customization configuration data found") + + # Clean customization configuration file and directory + util.del_dir(cust_cfg_dir) + return (md, ud, vd) + + +def is_vmware_platform(): + system_type = dmi.read_dmi_data("system-product-name") + if system_type is None: + LOG.debug("No system-product-name found") + return False + elif "vmware" not in system_type.lower(): + LOG.debug("Not a VMware platform") + return False + return True + def decode(key, enc_type, data): """ @@ -367,6 +482,10 @@ def handle_returned_guestinfo_val(key, val): return None +def get_imc_key_name(key): + return "vmware-tools" + + def get_guestinfo_key_name(key): return "guestinfo." + key @@ -512,6 +631,9 @@ def load_json_or_yaml(data): """ if not data: return {} + # If data is already a dictionary, here will return it directly. + if isinstance(data, dict): + return data try: return util.load_json(data) except (json.JSONDecodeError, TypeError): @@ -523,6 +645,8 @@ def process_metadata(data): process_metadata processes metadata and loads the optional network configuration. """ + if not data: + return {} network = None if "network" in data: network = data["network"] diff --git a/cloudinit/sources/helpers/vmware/imc/config.py b/cloudinit/sources/helpers/vmware/imc/config.py index 8b2deb65..df9e5c4b 100644 --- a/cloudinit/sources/helpers/vmware/imc/config.py +++ b/cloudinit/sources/helpers/vmware/imc/config.py @@ -29,6 +29,7 @@ class Config: DEFAULT_RUN_POST_SCRIPT = "MISC|DEFAULT-RUN-POST-CUST-SCRIPT" CLOUDINIT_META_DATA = "CLOUDINIT|METADATA" CLOUDINIT_USER_DATA = "CLOUDINIT|USERDATA" + CLOUDINIT_INSTANCE_ID = "CLOUDINIT|INSTANCE-ID" def __init__(self, configFile): self._configFile = configFile @@ -142,5 +143,10 @@ class Config: """Return the name of cloud-init user data.""" return self._configFile.get(Config.CLOUDINIT_USER_DATA, None) + @property + def instance_id(self): + """Return instance id""" + return self._configFile.get(Config.CLOUDINIT_INSTANCE_ID, None) + # vi: ts=4 expandtab diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py index 5b5f02ca..6ffbae40 100644 --- a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py +++ b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py @@ -1,7 +1,8 @@ # Copyright (C) 2016 Canonical Ltd. -# Copyright (C) 2016 VMware Inc. +# Copyright (C) 2016-2022 VMware Inc. # # Author: Sankar Tanguturi <stanguturi@vmware.com> +# Pengpeng Sun <pegnpengs@vmware.com> # # This file is part of cloud-init. See LICENSE file for license information. @@ -10,13 +11,20 @@ import os import re import time -from cloudinit import subp -from cloudinit.sources.helpers.vmware.imc.guestcust_event import ( - GuestCustEventEnum, -) -from cloudinit.sources.helpers.vmware.imc.guestcust_state import ( - GuestCustStateEnum, +from cloudinit import subp, util + +from .config import Config +from .config_custom_script import ( + CustomScriptNotFound, + PostCustomScript, + PreCustomScript, ) +from .config_file import ConfigFile +from .config_nic import NicConfigurator +from .config_passwd import PasswordConfigurator +from .guestcust_error import GuestCustErrorEnum +from .guestcust_event import GuestCustEventEnum +from .guestcust_state import GuestCustStateEnum logger = logging.getLogger(__name__) @@ -24,6 +32,11 @@ logger = logging.getLogger(__name__) CLOUDINIT_LOG_FILE = "/var/log/cloud-init.log" QUERY_NICS_SUPPORTED = "queryNicsSupported" NICS_STATUS_CONNECTED = "connected" +# Path to the VMware IMC directory +IMC_DIR_PATH = "/var/run/vmware-imc" +# Customization script configuration in tools conf +IMC_TOOLS_CONF_GROUPNAME = "deployPkg" +IMC_TOOLS_CONF_ENABLE_CUST_SCRIPTS = "enable-custom-scripts" # This will send a RPC command to the underlying @@ -183,4 +196,447 @@ def set_gc_status(config, gcMsg): return None +def get_imc_dir_path(): + return IMC_DIR_PATH + + +def get_data_from_imc_cust_cfg( + cloud_dir, + scripts_cpath, + cust_cfg, + cust_cfg_dir, + distro, +): + md, ud, vd, cfg = {}, None, None, {} + set_gc_status(cust_cfg, "Started") + (md, cfg) = get_non_network_data_from_vmware_cust_cfg(cust_cfg) + is_special_customization = check_markers(cloud_dir, cust_cfg) + if is_special_customization: + if not do_special_customization( + scripts_cpath, cust_cfg, cust_cfg_dir, distro + ): + return (None, None, None, None) + if not recheck_markers(cloud_dir, cust_cfg): + return (None, None, None, None) + try: + logger.debug("Preparing the Network configuration") + md["network"] = get_network_data_from_vmware_cust_cfg( + cust_cfg, True, True, distro.osfamily + ) + except Exception as e: + set_cust_error_status( + "Error preparing Network Configuration", + str(e), + GuestCustEventEnum.GUESTCUST_EVENT_NETWORK_SETUP_FAILED, + cust_cfg, + ) + return (None, None, None, None) + connect_nics(cust_cfg_dir) + set_customization_status( + GuestCustStateEnum.GUESTCUST_STATE_DONE, + GuestCustErrorEnum.GUESTCUST_ERROR_SUCCESS, + ) + set_gc_status(cust_cfg, "Successful") + return (md, ud, vd, cfg) + + +def get_data_from_imc_raw_data_cust_cfg(cust_cfg): + set_gc_status(cust_cfg, "Started") + md, ud, vd = None, None, None + md_file = cust_cfg.meta_data_name + if md_file: + md_path = os.path.join(get_imc_dir_path(), md_file) + if not os.path.exists(md_path): + set_cust_error_status( + "Error locating the cloud-init meta data file", + "Meta data file is not found: %s" % md_path, + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return (None, None, None) + try: + md = util.load_file(md_path) + except Exception as e: + set_cust_error_status( + "Error loading cloud-init meta data file", + str(e), + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return (None, None, None) + + ud_file = cust_cfg.user_data_name + if ud_file: + ud_path = os.path.join(get_imc_dir_path(), ud_file) + if not os.path.exists(ud_path): + set_cust_error_status( + "Error locating the cloud-init userdata file", + "Userdata file is not found: %s" % ud_path, + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return (None, None, None) + try: + ud = util.load_file(ud_path).replace("\r", "") + except Exception as e: + set_cust_error_status( + "Error loading cloud-init userdata file", + str(e), + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return (None, None, None) + + set_customization_status( + GuestCustStateEnum.GUESTCUST_STATE_DONE, + GuestCustErrorEnum.GUESTCUST_ERROR_SUCCESS, + ) + set_gc_status(cust_cfg, "Successful") + return (md, ud, vd) + + +def get_non_network_data_from_vmware_cust_cfg(cust_cfg): + md, cfg = {}, {} + if cust_cfg.host_name: + if cust_cfg.domain_name: + md["local-hostname"] = ( + cust_cfg.host_name + "." + cust_cfg.domain_name + ) + else: + md["local-hostname"] = cust_cfg.host_name + if cust_cfg.timezone: + cfg["timezone"] = cust_cfg.timezone + if cust_cfg.instance_id: + md["instance-id"] = cust_cfg.instance_id + return (md, cfg) + + +def get_network_data_from_vmware_cust_cfg( + cust_cfg, use_system_devices=True, configure=False, osfamily=None +): + nicConfigurator = NicConfigurator(cust_cfg.nics, use_system_devices) + nics_cfg_list = nicConfigurator.generate(configure, osfamily) + + return get_v1_network_config( + nics_cfg_list, cust_cfg.name_servers, cust_cfg.dns_suffixes + ) + + +def get_v1_network_config(nics_cfg_list=None, nameservers=None, search=None): + config_list = nics_cfg_list + + if nameservers or search: + config_list.append( + {"type": "nameserver", "address": nameservers, "search": search} + ) + + return {"version": 1, "config": config_list} + + +def connect_nics(cust_cfg_dir): + nics_file = os.path.join(cust_cfg_dir, "nics.txt") + if os.path.exists(nics_file): + logger.debug("%s file found, to connect nics", nics_file) + enable_nics(get_nics_to_enable(nics_file)) + + +def is_vmware_cust_enabled(sys_cfg): + return not util.get_cfg_option_bool( + sys_cfg, "disable_vmware_customization", True + ) + + +def is_raw_data_cust_enabled(ds_cfg): + return util.get_cfg_option_bool(ds_cfg, "allow_raw_data", True) + + +def get_cust_cfg_file(ds_cfg): + # When the VM is powered on, the "VMware Tools" daemon + # copies the customization specification file to + # /var/run/vmware-imc directory. cloud-init code needs + # to search for the file in that directory which indicates + # that required metadata and userdata files are now + # present. + max_wait = get_max_wait_from_cfg(ds_cfg) + cust_cfg_file_path = util.log_time( + logfunc=logger.debug, + msg="Waiting for VMware customization configuration file", + func=wait_for_cust_cfg_file, + args=("cust.cfg", max_wait), + ) + if cust_cfg_file_path: + logger.debug( + "Found VMware customization configuration file at %s", + cust_cfg_file_path, + ) + return cust_cfg_file_path + else: + logger.debug("No VMware customization configuration file found") + return None + + +def wait_for_cust_cfg_file( + filename, maxwait=180, naplen=5, dirpath="/var/run/vmware-imc" +): + waited = 0 + if maxwait <= naplen: + naplen = 1 + + while waited < maxwait: + fileFullPath = os.path.join(dirpath, filename) + if os.path.isfile(fileFullPath): + return fileFullPath + logger.debug("Waiting for VMware customization configuration file") + time.sleep(naplen) + waited += naplen + return None + + +def get_max_wait_from_cfg(ds_cfg): + default_max_wait = 15 + max_wait_cfg_option = "vmware_cust_file_max_wait" + max_wait = default_max_wait + if not ds_cfg: + return max_wait + try: + max_wait = int(ds_cfg.get(max_wait_cfg_option, default_max_wait)) + except ValueError: + logger.warning( + "Failed to get '%s', using %s", + max_wait_cfg_option, + default_max_wait, + ) + if max_wait < 0: + logger.warning( + "Invalid value '%s' for '%s', using '%s' instead", + max_wait, + max_wait_cfg_option, + default_max_wait, + ) + max_wait = default_max_wait + return max_wait + + +def check_markers(cloud_dir, cust_cfg): + product_marker = cust_cfg.marker_id + has_marker_file = check_marker_exists( + product_marker, os.path.join(cloud_dir, "data") + ) + return product_marker and not has_marker_file + + +def check_marker_exists(markerid, marker_dir): + """ + Check the existence of a marker file. + Presence of marker file determines whether a certain code path is to be + executed. It is needed for partial guest customization in VMware. + @param markerid: is an unique string representing a particular product + marker. + @param: marker_dir: The directory in which markers exist. + """ + if not markerid: + return False + markerfile = os.path.join(marker_dir, ".markerfile-" + markerid + ".txt") + if os.path.exists(markerfile): + return True + return False + + +def recheck_markers(cloud_dir, cust_cfg): + product_marker = cust_cfg.marker_id + if product_marker: + if not create_marker_file(cloud_dir, cust_cfg): + return False + return True + + +def create_marker_file(cloud_dir, cust_cfg): + try: + setup_marker_files(cust_cfg.marker_id, os.path.join(cloud_dir, "data")) + except Exception as e: + set_cust_error_status( + "Error creating marker files", + str(e), + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return False + return True + + +def setup_marker_files(marker_id, marker_dir): + """ + Create a new marker file. + Marker files are unique to a full customization workflow in VMware + environment. + @param marker_id: is an unique string representing a particular product + marker. + @param: marker_dir: The directory in which markers exist. + """ + logger.debug("Handle marker creation") + marker_file = os.path.join(marker_dir, ".markerfile-" + marker_id + ".txt") + for fname in os.listdir(marker_dir): + if fname.startswith(".markerfile"): + util.del_file(os.path.join(marker_dir, fname)) + open(marker_file, "w").close() + + +def do_special_customization(scripts_cpath, cust_cfg, cust_cfg_dir, distro): + is_pre_custom_successful = False + is_password_custom_successful = False + is_post_custom_successful = False + is_custom_script_enabled = False + custom_script = cust_cfg.custom_script_name + if custom_script: + is_custom_script_enabled = check_custom_script_enablement(cust_cfg) + if is_custom_script_enabled: + is_pre_custom_successful = do_pre_custom_script( + cust_cfg, custom_script, cust_cfg_dir + ) + is_password_custom_successful = do_password_customization(cust_cfg, distro) + if custom_script and is_custom_script_enabled: + ccScriptsDir = os.path.join(scripts_cpath, "per-instance") + is_post_custom_successful = do_post_custom_script( + cust_cfg, custom_script, cust_cfg_dir, ccScriptsDir + ) + if custom_script: + return ( + is_pre_custom_successful + and is_password_custom_successful + and is_post_custom_successful + ) + return is_password_custom_successful + + +def do_pre_custom_script(cust_cfg, custom_script, cust_cfg_dir): + try: + precust = PreCustomScript(custom_script, cust_cfg_dir) + precust.execute() + except CustomScriptNotFound as e: + set_cust_error_status( + "Error executing pre-customization script", + str(e), + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return False + return True + + +def do_post_custom_script(cust_cfg, custom_script, cust_cfg_dir, ccScriptsDir): + try: + postcust = PostCustomScript(custom_script, cust_cfg_dir, ccScriptsDir) + postcust.execute() + except CustomScriptNotFound as e: + set_cust_error_status( + "Error executing post-customization script", + str(e), + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return False + return True + + +def check_custom_script_enablement(cust_cfg): + is_custom_script_enabled = False + default_value = "false" + if cust_cfg.default_run_post_script: + logger.debug( + "Set default value to true due to customization configuration." + ) + default_value = "true" + custom_script_enablement = get_tools_config( + IMC_TOOLS_CONF_GROUPNAME, + IMC_TOOLS_CONF_ENABLE_CUST_SCRIPTS, + default_value, + ) + if custom_script_enablement.lower() != "true": + set_cust_error_status( + "Custom script is disabled by VM Administrator", + "Error checking custom script enablement", + GuestCustErrorEnum.GUESTCUST_ERROR_SCRIPT_DISABLED, + cust_cfg, + ) + else: + is_custom_script_enabled = True + return is_custom_script_enabled + + +def do_password_customization(cust_cfg, distro): + logger.debug("Applying password customization") + pwdConfigurator = PasswordConfigurator() + admin_pwd = cust_cfg.admin_password + try: + reset_pwd = cust_cfg.reset_password + if admin_pwd or reset_pwd: + pwdConfigurator.configure(admin_pwd, reset_pwd, distro) + else: + logger.debug("Changing password is not needed") + except Exception as e: + set_cust_error_status( + "Error applying password configuration", + str(e), + GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED, + cust_cfg, + ) + return False + return True + + +def parse_cust_cfg(cfg_file): + return Config(ConfigFile(cfg_file)) + + +def get_cust_cfg_type(cust_cfg): + is_vmware_cust_cfg, is_raw_data_cust_cfg = False, False + if cust_cfg.meta_data_name: + is_raw_data_cust_cfg = True + logger.debug("raw cloudinit data cust cfg found") + else: + is_vmware_cust_cfg = True + logger.debug("vmware cust cfg found") + return (is_vmware_cust_cfg, is_raw_data_cust_cfg) + + +def is_cust_plugin_available(): + search_paths = ( + "/usr/lib/vmware-tools", + "/usr/lib64/vmware-tools", + "/usr/lib/open-vm-tools", + "/usr/lib64/open-vm-tools", + "/usr/lib/x86_64-linux-gnu/open-vm-tools", + "/usr/lib/aarch64-linux-gnu/open-vm-tools", + ) + cust_plugin = "libdeployPkgPlugin.so" + for path in search_paths: + cust_plugin_path = search_file(path, cust_plugin) + if cust_plugin_path: + logger.debug( + "Found the customization plugin at %s", cust_plugin_path + ) + return True + return False + + +def search_file(dirpath, filename): + if not dirpath or not filename: + return None + + for root, _dirs, files in os.walk(dirpath): + if filename in files: + return os.path.join(root, filename) + + return None + + +def set_cust_error_status(prefix, error, event, cust_cfg): + """ + Set customization status to the underlying VMware Virtualization Platform + """ + util.logexc(logger, "%s: %s", prefix, error) + set_customization_status(GuestCustStateEnum.GUESTCUST_STATE_RUNNING, event) + set_gc_status(cust_cfg, prefix) + + # vi: ts=4 expandtab |