summaryrefslogtreecommitdiff
path: root/cloudinit/sources/DataSourceAzure.py
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/sources/DataSourceAzure.py')
-rw-r--r--cloudinit/sources/DataSourceAzure.py318
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