diff options
Diffstat (limited to 'cloudinit/sources/DataSourceAzure.py')
-rw-r--r-- | cloudinit/sources/DataSourceAzure.py | 318 |
1 files changed, 47 insertions, 271 deletions
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index e7a0407c..5aea0c5c 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -12,9 +12,9 @@ import os.path import re import xml.etree.ElementTree as ET from enum import Enum +from pathlib import Path from time import sleep, time from typing import Any, Dict, List, Optional -from xml.dom import minidom import requests @@ -34,6 +34,10 @@ from cloudinit.sources.helpers import netlink from cloudinit.sources.helpers.azure import ( DEFAULT_REPORT_FAILURE_USER_VISIBLE_MESSAGE, DEFAULT_WIRESERVER_ENDPOINT, + BrokenAzureDataSource, + ChassisAssetTag, + NonAzureDataSource, + OvfEnvXml, azure_ds_reporter, azure_ds_telemetry_reporter, build_minimal_ovf, @@ -58,8 +62,6 @@ DEFAULT_METADATA = {"instance-id": "iid-AZURE-NODE"} # ensures that it gets linked to this path. RESOURCE_DISK_PATH = "/dev/disk/cloud/azure_resource" DEFAULT_FS = "ext4" -# DMI chassis-asset-tag is set static for all azure instances -AZURE_CHASSIS_ASSET_TAG = "7783-7084-3265-9085-8269-3286-77" AGENT_SEED_DIR = "/var/lib/waagent" DEFAULT_PROVISIONING_ISO_DEV = "/dev/sr0" @@ -305,6 +307,20 @@ DEF_EPHEMERAL_LABEL = "Temporary Storage" DEF_PASSWD_REDACTION = "REDACTED" +@azure_ds_telemetry_reporter +def is_platform_viable(seed_dir: Optional[Path]) -> bool: + """Check platform environment to report if this datasource may run.""" + chassis_tag = ChassisAssetTag.query_system() + if chassis_tag is not None: + return True + + # If no valid chassis tag, check for seeded ovf-env.xml. + if seed_dir is None: + return False + + return (seed_dir / "ovf-env.xml").exists() + + class DataSourceAzure(sources.DataSource): dsname = "Azure" @@ -665,10 +681,6 @@ class DataSourceAzure(sources.DataSource): return crawled_data - def _is_platform_viable(self): - """Check platform environment to report if this datasource may run.""" - return _is_platform_viable(self.seed_dir) - def clear_cached_attrs(self, attr_defaults=()): """Reset any cached class attributes to defaults.""" super(DataSourceAzure, self).clear_cached_attrs(attr_defaults) @@ -681,7 +693,7 @@ class DataSourceAzure(sources.DataSource): @return: True on success, False on error, invalid or disabled datasource. """ - if not self._is_platform_viable(): + if not is_platform_viable(Path(self.seed_dir)): return False try: get_boot_telemetry() @@ -1803,264 +1815,54 @@ def write_files(datadir, files, dirmode=None): util.write_file(filename=fname, content=content, mode=0o600) -def find_child(node, filter_func): - ret = [] - if not node.hasChildNodes(): - return ret - for child in node.childNodes: - if filter_func(child): - ret.append(child) - return ret - - -@azure_ds_telemetry_reporter -def load_azure_ovf_pubkeys(sshnode): - # This parses a 'SSH' node formatted like below, and returns - # an array of dicts. - # [{'fingerprint': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7', - # 'path': '/where/to/go'}] - # - # <SSH><PublicKeys> - # <PublicKey><Fingerprint>ABC</FingerPrint><Path>/x/y/z</Path> - # ... - # </PublicKeys></SSH> - # Under some circumstances, there may be a <Value> element along with the - # Fingerprint and Path. Pass those along if they appear. - results = find_child(sshnode, lambda n: n.localName == "PublicKeys") - if len(results) == 0: - return [] - if len(results) > 1: - raise BrokenAzureDataSource( - "Multiple 'PublicKeys'(%s) in SSH node" % len(results) - ) - - pubkeys_node = results[0] - pubkeys = find_child(pubkeys_node, lambda n: n.localName == "PublicKey") - - if len(pubkeys) == 0: - return [] - - found = [] - text_node = minidom.Document.TEXT_NODE - - for pk_node in pubkeys: - if not pk_node.hasChildNodes(): - continue - - cur = {"fingerprint": "", "path": "", "value": ""} - for child in pk_node.childNodes: - if child.nodeType == text_node or not child.localName: - continue - - name = child.localName.lower() - - if name not in cur.keys(): - continue - - if ( - len(child.childNodes) != 1 - or child.childNodes[0].nodeType != text_node - ): - continue - - cur[name] = child.childNodes[0].wholeText.strip() - found.append(cur) - - return found - - @azure_ds_telemetry_reporter def read_azure_ovf(contents): - try: - dom = minidom.parseString(contents) - except Exception as e: - error_str = "Invalid ovf-env.xml: %s" % e - report_diagnostic_event(error_str, logger_func=LOG.warning) - raise BrokenAzureDataSource(error_str) from e - - results = find_child( - dom.documentElement, lambda n: n.localName == "ProvisioningSection" - ) - - if len(results) == 0: - raise NonAzureDataSource("No ProvisioningSection") - if len(results) > 1: - raise BrokenAzureDataSource( - "found '%d' ProvisioningSection items" % len(results) - ) - provSection = results[0] + """Parse OVF XML contents. - lpcs_nodes = find_child( - provSection, - lambda n: n.localName == "LinuxProvisioningConfigurationSet", - ) - - if len(lpcs_nodes) == 0: - raise NonAzureDataSource("No LinuxProvisioningConfigurationSet") - if len(lpcs_nodes) > 1: - raise BrokenAzureDataSource( - "found '%d' %ss" - % (len(lpcs_nodes), "LinuxProvisioningConfigurationSet") - ) - lpcs = lpcs_nodes[0] - - if not lpcs.hasChildNodes(): - raise BrokenAzureDataSource("no child nodes of configuration set") + :return: Tuple of metadata, configuration, userdata dicts. + :raises NonAzureDataSource: if XML is not in Azure's format. + :raises BrokenAzureDataSource: if XML is unparseable or invalid. + """ + ovf_env = OvfEnvXml.parse_text(contents) md: Dict[str, Any] = {} cfg = {} - ud = "" - password = None - username = None + ud = ovf_env.custom_data or "" - for child in lpcs.childNodes: - if child.nodeType == dom.TEXT_NODE or not child.localName: - continue + if ovf_env.hostname: + md["local-hostname"] = ovf_env.hostname - name = child.localName.lower() + if ovf_env.public_keys: + cfg["_pubkeys"] = ovf_env.public_keys - value = "" - if ( - len(child.childNodes) == 1 - and child.childNodes[0].nodeType == dom.TEXT_NODE - ): - value = child.childNodes[0].wholeText - - if name == "customdata": - ud = base64.b64decode("".join(value.split())) - elif name == "username": - username = value - elif name == "userpassword": - password = value - elif name == "hostname": - md["local-hostname"] = value - elif name == "ssh": - cfg["_pubkeys"] = load_azure_ovf_pubkeys(child) - elif name == "disablesshpasswordauthentication": - cfg["ssh_pwauth"] = util.is_false(value) + if ovf_env.disable_ssh_password_auth is not None: + cfg["ssh_pwauth"] = not ovf_env.disable_ssh_password_auth + elif ovf_env.password: + cfg["ssh_pwauth"] = True defuser = {} - if username: - defuser["name"] = username - if password: + if ovf_env.username: + defuser["name"] = ovf_env.username + if ovf_env.password: defuser["lock_passwd"] = False - if DEF_PASSWD_REDACTION != password: - defuser["passwd"] = cfg["password"] = encrypt_pass(password) + if DEF_PASSWD_REDACTION != ovf_env.password: + defuser["hashed_passwd"] = encrypt_pass(ovf_env.password) if defuser: cfg["system_info"] = {"default_user": defuser} - if "ssh_pwauth" not in cfg and password: - cfg["ssh_pwauth"] = True - - preprovisioning_cfg = _get_preprovisioning_cfgs(dom) - cfg = util.mergemanydict([cfg, preprovisioning_cfg]) - - return (md, ud, cfg) - - -@azure_ds_telemetry_reporter -def _get_preprovisioning_cfgs(dom): - """Read the preprovisioning related flags from ovf and populates a dict - with the info. - - Two flags are in use today: PreprovisionedVm bool and - PreprovisionedVMType enum. In the long term, the PreprovisionedVm bool - will be deprecated in favor of PreprovisionedVMType string/enum. - - Only these combinations of values are possible today: - - PreprovisionedVm=True and PreprovisionedVMType=Running - - PreprovisionedVm=False and PreprovisionedVMType=Savable - - PreprovisionedVm is missing and PreprovisionedVMType=Running/Savable - - PreprovisionedVm=False and PreprovisionedVMType is missing - - More specifically, this will never happen: - - PreprovisionedVm=True and PreprovisionedVMType=Savable - """ - cfg = {"PreprovisionedVm": False, "PreprovisionedVMType": None} - - platform_settings_section = find_child( - dom.documentElement, lambda n: n.localName == "PlatformSettingsSection" - ) - if not platform_settings_section or len(platform_settings_section) == 0: - LOG.debug("PlatformSettingsSection not found") - return cfg - platform_settings = find_child( - platform_settings_section[0], - lambda n: n.localName == "PlatformSettings", - ) - if not platform_settings or len(platform_settings) == 0: - LOG.debug("PlatformSettings not found") - return cfg - - # Read the PreprovisionedVm bool flag. This should be deprecated when the - # platform has removed PreprovisionedVm and only surfaces - # PreprovisionedVMType. - cfg["PreprovisionedVm"] = _get_preprovisionedvm_cfg_value( - platform_settings - ) - - cfg["PreprovisionedVMType"] = _get_preprovisionedvmtype_cfg_value( - platform_settings - ) - return cfg - - -@azure_ds_telemetry_reporter -def _get_preprovisionedvm_cfg_value(platform_settings): - preprovisionedVm = False - - # Read the PreprovisionedVm bool flag. This should be deprecated when the - # platform has removed PreprovisionedVm and only surfaces - # PreprovisionedVMType. - preprovisionedVmVal = find_child( - platform_settings[0], lambda n: n.localName == "PreprovisionedVm" - ) - if not preprovisionedVmVal or len(preprovisionedVmVal) == 0: - LOG.debug("PreprovisionedVm not found") - return preprovisionedVm - preprovisionedVm = util.translate_bool( - preprovisionedVmVal[0].firstChild.nodeValue - ) - + cfg["PreprovisionedVm"] = ovf_env.preprovisioned_vm report_diagnostic_event( - "PreprovisionedVm: %s" % preprovisionedVm, logger_func=LOG.info + "PreprovisionedVm: %s" % ovf_env.preprovisioned_vm, + logger_func=LOG.info, ) - return preprovisionedVm - - -@azure_ds_telemetry_reporter -def _get_preprovisionedvmtype_cfg_value(platform_settings): - preprovisionedVMType = None - - # Read the PreprovisionedVMType value from the ovf. It can be - # 'Running' or 'Savable' or not exist. This enum value is intended to - # replace PreprovisionedVm bool flag in the long term. - # A Running VM is the same as preprovisioned VMs of today. This is - # equivalent to having PreprovisionedVm=True. - # A Savable VM is one whose nic is hot-detached immediately after it - # reports ready the first time to free up the network resources. - # Once assigned to customer, the customer-requested nics are - # hot-attached to it and reprovision happens like today. - preprovisionedVMTypeVal = find_child( - platform_settings[0], lambda n: n.localName == "PreprovisionedVMType" - ) - if ( - not preprovisionedVMTypeVal - or len(preprovisionedVMTypeVal) == 0 - or preprovisionedVMTypeVal[0].firstChild is None - ): - LOG.debug("PreprovisionedVMType not found") - return preprovisionedVMType - - preprovisionedVMType = preprovisionedVMTypeVal[0].firstChild.nodeValue - + cfg["PreprovisionedVMType"] = ovf_env.preprovisioned_vm_type report_diagnostic_event( - "PreprovisionedVMType: %s" % preprovisionedVMType, logger_func=LOG.info + "PreprovisionedVMType: %s" % ovf_env.preprovisioned_vm_type, + logger_func=LOG.info, ) - - return preprovisionedVMType + return (md, ud, cfg) def encrypt_pass(password, salt_id="$6$"): @@ -2346,32 +2148,6 @@ def maybe_remove_ubuntu_network_config_scripts(paths=None): util.del_file(path) -def _is_platform_viable(seed_dir): - """Check platform environment to report if this datasource may run.""" - with events.ReportEventStack( - name="check-platform-viability", - description="found azure asset tag", - parent=azure_ds_reporter, - ) as evt: - asset_tag = dmi.read_dmi_data("chassis-asset-tag") - if asset_tag == AZURE_CHASSIS_ASSET_TAG: - return True - msg = "Non-Azure DMI asset tag '%s' discovered." % asset_tag - evt.description = msg - report_diagnostic_event(msg, logger_func=LOG.debug) - if os.path.exists(os.path.join(seed_dir, "ovf-env.xml")): - return True - return False - - -class BrokenAzureDataSource(Exception): - pass - - -class NonAzureDataSource(Exception): - pass - - # Legacy: Must be present in case we load an old pkl object DataSourceAzureNet = DataSourceAzure |