diff --git a/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch b/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch new file mode 100644 index 0000000..4c3dbc3 --- /dev/null +++ b/SOURCES/ci-Add-flexibility-to-IMDS-api-version-793.patch @@ -0,0 +1,295 @@ +From f844e9c263e59a623ca8c647bd87bf4f91374d54 Mon Sep 17 00:00:00 2001 +From: Thomas Stringer <thstring@microsoft.com> +Date: Wed, 3 Mar 2021 11:07:43 -0500 +Subject: [PATCH 1/7] Add flexibility to IMDS api-version (#793) + +RH-Author: Eduardo Otubo <otubo@redhat.com> +RH-MergeRequest: 18: Add support for userdata on Azure from IMDS +RH-Commit: [1/7] 99a3db20e3f277a2f12ea21e937e06939434a2ca (otubo/cloud-init-src) +RH-Bugzilla: 2042351 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> + +Add flexibility to IMDS api-version by having both a desired IMDS +api-version and a minimum api-version. The desired api-version will +be used first, and if that fails it will fall back to the minimum +api-version. +--- + cloudinit/sources/DataSourceAzure.py | 113 ++++++++++++++---- + tests/unittests/test_datasource/test_azure.py | 42 ++++++- + 2 files changed, 129 insertions(+), 26 deletions(-) + +diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py +index 553b5a7e..de1452ce 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -78,17 +78,15 @@ AGENT_SEED_DIR = '/var/lib/waagent' + # In the event where the IMDS primary server is not + # available, it takes 1s to fallback to the secondary one + IMDS_TIMEOUT_IN_SECONDS = 2 +-IMDS_URL = "http://169.254.169.254/metadata/" +-IMDS_VER = "2019-06-01" +-IMDS_VER_PARAM = "api-version={}".format(IMDS_VER) ++IMDS_URL = "http://169.254.169.254/metadata" ++IMDS_VER_MIN = "2019-06-01" ++IMDS_VER_WANT = "2020-09-01" + + + class metadata_type(Enum): +- compute = "{}instance?{}".format(IMDS_URL, IMDS_VER_PARAM) +- network = "{}instance/network?{}".format(IMDS_URL, +- IMDS_VER_PARAM) +- reprovisiondata = "{}reprovisiondata?{}".format(IMDS_URL, +- IMDS_VER_PARAM) ++ compute = "{}/instance".format(IMDS_URL) ++ network = "{}/instance/network".format(IMDS_URL) ++ reprovisiondata = "{}/reprovisiondata".format(IMDS_URL) + + + PLATFORM_ENTROPY_SOURCE = "/sys/firmware/acpi/tables/OEM0" +@@ -349,6 +347,8 @@ class DataSourceAzure(sources.DataSource): + self.update_events['network'].add(EventType.BOOT) + self._ephemeral_dhcp_ctx = None + ++ self.failed_desired_api_version = False ++ + def __str__(self): + root = sources.DataSource.__str__(self) + return "%s [seed=%s]" % (root, self.seed) +@@ -520,8 +520,10 @@ class DataSourceAzure(sources.DataSource): + self._wait_for_all_nics_ready() + ret = self._reprovision() + +- imds_md = get_metadata_from_imds( +- self.fallback_interface, retries=10) ++ imds_md = self.get_imds_data_with_api_fallback( ++ self.fallback_interface, ++ retries=10 ++ ) + (md, userdata_raw, cfg, files) = ret + self.seed = cdev + crawled_data.update({ +@@ -652,6 +654,57 @@ class DataSourceAzure(sources.DataSource): + self.ds_cfg['data_dir'], crawled_data['files'], dirmode=0o700) + return True + ++ @azure_ds_telemetry_reporter ++ def get_imds_data_with_api_fallback( ++ self, ++ fallback_nic, ++ retries, ++ md_type=metadata_type.compute): ++ """ ++ Wrapper for get_metadata_from_imds so that we can have flexibility ++ in which IMDS api-version we use. If a particular instance of IMDS ++ does not have the api version that is desired, we want to make ++ this fault tolerant and fall back to a good known minimum api ++ version. ++ """ ++ ++ if not self.failed_desired_api_version: ++ for _ in range(retries): ++ try: ++ LOG.info( ++ "Attempting IMDS api-version: %s", ++ IMDS_VER_WANT ++ ) ++ return get_metadata_from_imds( ++ fallback_nic=fallback_nic, ++ retries=0, ++ md_type=md_type, ++ api_version=IMDS_VER_WANT ++ ) ++ except UrlError as err: ++ LOG.info( ++ "UrlError with IMDS api-version: %s", ++ IMDS_VER_WANT ++ ) ++ if err.code == 400: ++ log_msg = "Fall back to IMDS api-version: {}".format( ++ IMDS_VER_MIN ++ ) ++ report_diagnostic_event( ++ log_msg, ++ logger_func=LOG.info ++ ) ++ self.failed_desired_api_version = True ++ break ++ ++ LOG.info("Using IMDS api-version: %s", IMDS_VER_MIN) ++ return get_metadata_from_imds( ++ fallback_nic=fallback_nic, ++ retries=retries, ++ md_type=md_type, ++ api_version=IMDS_VER_MIN ++ ) ++ + def device_name_to_device(self, name): + return self.ds_cfg['disk_aliases'].get(name) + +@@ -880,10 +933,11 @@ class DataSourceAzure(sources.DataSource): + # primary nic is being attached first helps here. Otherwise each nic + # could add several seconds of delay. + try: +- imds_md = get_metadata_from_imds( ++ imds_md = self.get_imds_data_with_api_fallback( + ifname, + 5, +- metadata_type.network) ++ metadata_type.network ++ ) + except Exception as e: + LOG.warning( + "Failed to get network metadata using nic %s. Attempt to " +@@ -1017,7 +1071,10 @@ class DataSourceAzure(sources.DataSource): + def _poll_imds(self): + """Poll IMDS for the new provisioning data until we get a valid + response. Then return the returned JSON object.""" +- url = metadata_type.reprovisiondata.value ++ url = "{}?api-version={}".format( ++ metadata_type.reprovisiondata.value, ++ IMDS_VER_MIN ++ ) + headers = {"Metadata": "true"} + nl_sock = None + report_ready = bool(not os.path.isfile(REPORTED_READY_MARKER_FILE)) +@@ -2059,7 +2116,8 @@ def _generate_network_config_from_fallback_config() -> dict: + @azure_ds_telemetry_reporter + def get_metadata_from_imds(fallback_nic, + retries, +- md_type=metadata_type.compute): ++ md_type=metadata_type.compute, ++ api_version=IMDS_VER_MIN): + """Query Azure's instance metadata service, returning a dictionary. + + If network is not up, setup ephemeral dhcp on fallback_nic to talk to the +@@ -2069,13 +2127,16 @@ def get_metadata_from_imds(fallback_nic, + @param fallback_nic: String. The name of the nic which requires active + network in order to query IMDS. + @param retries: The number of retries of the IMDS_URL. ++ @param md_type: Metadata type for IMDS request. ++ @param api_version: IMDS api-version to use in the request. + + @return: A dict of instance metadata containing compute and network + info. + """ + kwargs = {'logfunc': LOG.debug, + 'msg': 'Crawl of Azure Instance Metadata Service (IMDS)', +- 'func': _get_metadata_from_imds, 'args': (retries, md_type,)} ++ 'func': _get_metadata_from_imds, ++ 'args': (retries, md_type, api_version,)} + if net.is_up(fallback_nic): + return util.log_time(**kwargs) + else: +@@ -2091,20 +2152,26 @@ def get_metadata_from_imds(fallback_nic, + + + @azure_ds_telemetry_reporter +-def _get_metadata_from_imds(retries, md_type=metadata_type.compute): +- +- url = md_type.value ++def _get_metadata_from_imds( ++ retries, ++ md_type=metadata_type.compute, ++ api_version=IMDS_VER_MIN): ++ url = "{}?api-version={}".format(md_type.value, api_version) + headers = {"Metadata": "true"} + try: + response = readurl( + url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers, + retries=retries, exception_cb=retry_on_url_exc) + except Exception as e: +- report_diagnostic_event( +- 'Ignoring IMDS instance metadata. ' +- 'Get metadata from IMDS failed: %s' % e, +- logger_func=LOG.warning) +- return {} ++ # pylint:disable=no-member ++ if isinstance(e, UrlError) and e.code == 400: ++ raise ++ else: ++ report_diagnostic_event( ++ 'Ignoring IMDS instance metadata. ' ++ 'Get metadata from IMDS failed: %s' % e, ++ logger_func=LOG.warning) ++ return {} + try: + from json.decoder import JSONDecodeError + json_decode_error = JSONDecodeError +diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py +index f597c723..dedebeb1 100644 +--- a/tests/unittests/test_datasource/test_azure.py ++++ b/tests/unittests/test_datasource/test_azure.py +@@ -408,7 +408,9 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): + + def setUp(self): + super(TestGetMetadataFromIMDS, self).setUp() +- self.network_md_url = dsaz.IMDS_URL + "instance?api-version=2019-06-01" ++ self.network_md_url = "{}/instance?api-version=2019-06-01".format( ++ dsaz.IMDS_URL ++ ) + + @mock.patch(MOCKPATH + 'readurl') + @mock.patch(MOCKPATH + 'EphemeralDHCPv4', autospec=True) +@@ -518,7 +520,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): + """Return empty dict when IMDS network metadata is absent.""" + httpretty.register_uri( + httpretty.GET, +- dsaz.IMDS_URL + 'instance?api-version=2017-12-01', ++ dsaz.IMDS_URL + '/instance?api-version=2017-12-01', + body={}, status=404) + + m_net_is_up.return_value = True # skips dhcp +@@ -1877,6 +1879,40 @@ scbus-1 on xpt0 bus 0 + ssh_keys = dsrc.get_public_ssh_keys() + self.assertEqual(ssh_keys, ['key2']) + ++ @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ def test_imds_api_version_wanted_nonexistent( ++ self, ++ m_get_metadata_from_imds): ++ def get_metadata_from_imds_side_eff(*args, **kwargs): ++ if kwargs['api_version'] == dsaz.IMDS_VER_WANT: ++ raise url_helper.UrlError("No IMDS version", code=400) ++ return NETWORK_METADATA ++ m_get_metadata_from_imds.side_effect = get_metadata_from_imds_side_eff ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {'HostName': "myhost", 'UserName': "myuser"} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ dsrc = self._get_ds(data) ++ dsrc.get_data() ++ self.assertIsNotNone(dsrc.metadata) ++ self.assertTrue(dsrc.failed_desired_api_version) ++ ++ @mock.patch( ++ MOCKPATH + 'get_metadata_from_imds', return_value=NETWORK_METADATA) ++ def test_imds_api_version_wanted_exists(self, m_get_metadata_from_imds): ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {'HostName': "myhost", 'UserName': "myuser"} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ dsrc = self._get_ds(data) ++ dsrc.get_data() ++ self.assertIsNotNone(dsrc.metadata) ++ self.assertFalse(dsrc.failed_desired_api_version) ++ + + class TestAzureBounce(CiTestCase): + +@@ -2657,7 +2693,7 @@ class TestPreprovisioningHotAttachNics(CiTestCase): + @mock.patch(MOCKPATH + 'DataSourceAzure.wait_for_link_up') + @mock.patch('cloudinit.sources.helpers.netlink.wait_for_nic_attach_event') + @mock.patch('cloudinit.sources.net.find_fallback_nic') +- @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback') + @mock.patch(MOCKPATH + 'EphemeralDHCPv4') + @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach') + @mock.patch('os.path.isfile') +-- +2.27.0 + diff --git a/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch b/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch new file mode 100644 index 0000000..c30ed09 --- /dev/null +++ b/SOURCES/ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch @@ -0,0 +1,397 @@ +From 68f058e8d20a499f74bc78af8e0c6a90ca57ae20 Mon Sep 17 00:00:00 2001 +From: Thomas Stringer <thstring@microsoft.com> +Date: Mon, 26 Apr 2021 09:41:38 -0400 +Subject: [PATCH 5/7] Azure: Retrieve username and hostname from IMDS (#865) + +RH-Author: Eduardo Otubo <otubo@redhat.com> +RH-MergeRequest: 18: Add support for userdata on Azure from IMDS +RH-Commit: [5/7] 6a768d31e63e5f00dae0fad2712a7618d62b0879 (otubo/cloud-init-src) +RH-Bugzilla: 2042351 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> + +This change allows us to retrieve the username and hostname from +IMDS instead of having to rely on the mounted OVF. +--- + cloudinit/sources/DataSourceAzure.py | 149 ++++++++++++++---- + tests/unittests/test_datasource/test_azure.py | 87 +++++++++- + 2 files changed, 205 insertions(+), 31 deletions(-) + +diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py +index 39e67c4f..6d7954ee 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -5,6 +5,7 @@ + # This file is part of cloud-init. See LICENSE file for license information. + + import base64 ++from collections import namedtuple + import contextlib + import crypt + from functools import partial +@@ -25,6 +26,7 @@ from cloudinit.net import device_driver + from cloudinit.net.dhcp import EphemeralDHCPv4 + from cloudinit import sources + from cloudinit.sources.helpers import netlink ++from cloudinit import ssh_util + from cloudinit import subp + from cloudinit.url_helper import UrlError, readurl, retry_on_url_exc + from cloudinit import util +@@ -80,7 +82,12 @@ AGENT_SEED_DIR = '/var/lib/waagent' + IMDS_TIMEOUT_IN_SECONDS = 2 + IMDS_URL = "http://169.254.169.254/metadata" + IMDS_VER_MIN = "2019-06-01" +-IMDS_VER_WANT = "2020-09-01" ++IMDS_VER_WANT = "2020-10-01" ++ ++ ++# This holds SSH key data including if the source was ++# from IMDS, as well as the SSH key data itself. ++SSHKeys = namedtuple("SSHKeys", ("keys_from_imds", "ssh_keys")) + + + class metadata_type(Enum): +@@ -391,6 +398,8 @@ class DataSourceAzure(sources.DataSource): + """Return the subplatform metadata source details.""" + if self.seed.startswith('/dev'): + subplatform_type = 'config-disk' ++ elif self.seed.lower() == 'imds': ++ subplatform_type = 'imds' + else: + subplatform_type = 'seed-dir' + return '%s (%s)' % (subplatform_type, self.seed) +@@ -433,9 +442,11 @@ class DataSourceAzure(sources.DataSource): + + found = None + reprovision = False ++ ovf_is_accessible = True + reprovision_after_nic_attach = False + for cdev in candidates: + try: ++ LOG.debug("cdev: %s", cdev) + if cdev == "IMDS": + ret = None + reprovision = True +@@ -462,8 +473,18 @@ class DataSourceAzure(sources.DataSource): + raise sources.InvalidMetaDataException(msg) + except util.MountFailedError: + report_diagnostic_event( +- '%s was not mountable' % cdev, logger_func=LOG.warning) +- continue ++ '%s was not mountable' % cdev, logger_func=LOG.debug) ++ cdev = 'IMDS' ++ ovf_is_accessible = False ++ empty_md = {'local-hostname': ''} ++ empty_cfg = dict( ++ system_info=dict( ++ default_user=dict( ++ name='' ++ ) ++ ) ++ ) ++ ret = (empty_md, '', empty_cfg, {}) + + report_diagnostic_event("Found provisioning metadata in %s" % cdev, + logger_func=LOG.debug) +@@ -490,6 +511,10 @@ class DataSourceAzure(sources.DataSource): + self.fallback_interface, + retries=10 + ) ++ if not imds_md and not ovf_is_accessible: ++ msg = 'No OVF or IMDS available' ++ report_diagnostic_event(msg) ++ raise sources.InvalidMetaDataException(msg) + (md, userdata_raw, cfg, files) = ret + self.seed = cdev + crawled_data.update({ +@@ -498,6 +523,21 @@ class DataSourceAzure(sources.DataSource): + 'metadata': util.mergemanydict( + [md, {'imds': imds_md}]), + 'userdata_raw': userdata_raw}) ++ imds_username = _username_from_imds(imds_md) ++ imds_hostname = _hostname_from_imds(imds_md) ++ imds_disable_password = _disable_password_from_imds(imds_md) ++ if imds_username: ++ LOG.debug('Username retrieved from IMDS: %s', imds_username) ++ cfg['system_info']['default_user']['name'] = imds_username ++ if imds_hostname: ++ LOG.debug('Hostname retrieved from IMDS: %s', imds_hostname) ++ crawled_data['metadata']['local-hostname'] = imds_hostname ++ if imds_disable_password: ++ LOG.debug( ++ 'Disable password retrieved from IMDS: %s', ++ imds_disable_password ++ ) ++ crawled_data['metadata']['disable_password'] = imds_disable_password # noqa: E501 + found = cdev + + report_diagnostic_event( +@@ -676,6 +716,13 @@ class DataSourceAzure(sources.DataSource): + + @azure_ds_telemetry_reporter + def get_public_ssh_keys(self): ++ """ ++ Retrieve public SSH keys. ++ """ ++ ++ return self._get_public_ssh_keys_and_source().ssh_keys ++ ++ def _get_public_ssh_keys_and_source(self): + """ + Try to get the ssh keys from IMDS first, and if that fails + (i.e. IMDS is unavailable) then fallback to getting the ssh +@@ -685,30 +732,50 @@ class DataSourceAzure(sources.DataSource): + advantage, so this is a strong preference. But we must keep + OVF as a second option for environments that don't have IMDS. + """ ++ + LOG.debug('Retrieving public SSH keys') + ssh_keys = [] ++ keys_from_imds = True ++ LOG.debug('Attempting to get SSH keys from IMDS') + try: +- raise KeyError( +- "Not using public SSH keys from IMDS" +- ) +- # pylint:disable=unreachable + ssh_keys = [ + public_key['keyData'] + for public_key + in self.metadata['imds']['compute']['publicKeys'] + ] +- LOG.debug('Retrieved SSH keys from IMDS') ++ for key in ssh_keys: ++ if not _key_is_openssh_formatted(key=key): ++ keys_from_imds = False ++ break ++ ++ if not keys_from_imds: ++ log_msg = 'Keys not in OpenSSH format, using OVF' ++ else: ++ log_msg = 'Retrieved {} keys from IMDS'.format( ++ len(ssh_keys) ++ if ssh_keys is not None ++ else 0 ++ ) + except KeyError: + log_msg = 'Unable to get keys from IMDS, falling back to OVF' ++ keys_from_imds = False ++ finally: + report_diagnostic_event(log_msg, logger_func=LOG.debug) ++ ++ if not keys_from_imds: ++ LOG.debug('Attempting to get SSH keys from OVF') + try: + ssh_keys = self.metadata['public-keys'] +- LOG.debug('Retrieved keys from OVF') ++ log_msg = 'Retrieved {} keys from OVF'.format(len(ssh_keys)) + except KeyError: + log_msg = 'No keys available from OVF' ++ finally: + report_diagnostic_event(log_msg, logger_func=LOG.debug) + +- return ssh_keys ++ return SSHKeys( ++ keys_from_imds=keys_from_imds, ++ ssh_keys=ssh_keys ++ ) + + def get_config_obj(self): + return self.cfg +@@ -1325,30 +1392,21 @@ class DataSourceAzure(sources.DataSource): + self.bounce_network_with_azure_hostname() + + pubkey_info = None +- try: +- raise KeyError( +- "Not using public SSH keys from IMDS" +- ) +- # pylint:disable=unreachable +- public_keys = self.metadata['imds']['compute']['publicKeys'] +- LOG.debug( +- 'Successfully retrieved %s key(s) from IMDS', +- len(public_keys) +- if public_keys is not None ++ ssh_keys_and_source = self._get_public_ssh_keys_and_source() ++ ++ if not ssh_keys_and_source.keys_from_imds: ++ pubkey_info = self.cfg.get('_pubkeys', None) ++ log_msg = 'Retrieved {} fingerprints from OVF'.format( ++ len(pubkey_info) ++ if pubkey_info is not None + else 0 + ) +- except KeyError: +- LOG.debug( +- 'Unable to retrieve SSH keys from IMDS during ' +- 'negotiation, falling back to OVF' +- ) +- pubkey_info = self.cfg.get('_pubkeys', None) ++ report_diagnostic_event(log_msg, logger_func=LOG.debug) + + metadata_func = partial(get_metadata_from_fabric, + fallback_lease_file=self. + dhclient_lease_file, +- pubkey_info=pubkey_info, +- iso_dev=self.iso_dev) ++ pubkey_info=pubkey_info) + + LOG.debug("negotiating with fabric via agent command %s", + self.ds_cfg['agent_command']) +@@ -1404,6 +1462,41 @@ class DataSourceAzure(sources.DataSource): + return self.metadata.get('imds', {}).get('compute', {}).get('location') + + ++def _username_from_imds(imds_data): ++ try: ++ return imds_data['compute']['osProfile']['adminUsername'] ++ except KeyError: ++ return None ++ ++ ++def _hostname_from_imds(imds_data): ++ try: ++ return imds_data['compute']['osProfile']['computerName'] ++ except KeyError: ++ return None ++ ++ ++def _disable_password_from_imds(imds_data): ++ try: ++ return imds_data['compute']['osProfile']['disablePasswordAuthentication'] == 'true' # noqa: E501 ++ except KeyError: ++ return None ++ ++ ++def _key_is_openssh_formatted(key): ++ """ ++ Validate whether or not the key is OpenSSH-formatted. ++ """ ++ ++ parser = ssh_util.AuthKeyLineParser() ++ try: ++ akl = parser.parse(key) ++ except TypeError: ++ return False ++ ++ return akl.keytype is not None ++ ++ + def _partitions_on_device(devpath, maxnum=16): + # return a list of tuples (ptnum, path) for each part on devpath + for suff in ("-part", "p", ""): +diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py +index 320fa857..d9817d84 100644 +--- a/tests/unittests/test_datasource/test_azure.py ++++ b/tests/unittests/test_datasource/test_azure.py +@@ -108,7 +108,7 @@ NETWORK_METADATA = { + "zone": "", + "publicKeys": [ + { +- "keyData": "key1", ++ "keyData": "ssh-rsa key1", + "path": "path1" + } + ] +@@ -1761,8 +1761,29 @@ scbus-1 on xpt0 bus 0 + dsrc.get_data() + dsrc.setup(True) + ssh_keys = dsrc.get_public_ssh_keys() +- # Temporarily alter this test so that SSH public keys +- # from IMDS are *not* going to be in use to fix a regression. ++ self.assertEqual(ssh_keys, ["ssh-rsa key1"]) ++ self.assertEqual(m_parse_certificates.call_count, 0) ++ ++ @mock.patch( ++ 'cloudinit.sources.helpers.azure.OpenSSLManager.parse_certificates') ++ @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ def test_get_public_ssh_keys_with_no_openssh_format( ++ self, ++ m_get_metadata_from_imds, ++ m_parse_certificates): ++ imds_data = copy.deepcopy(NETWORK_METADATA) ++ imds_data['compute']['publicKeys'][0]['keyData'] = 'no-openssh-format' ++ m_get_metadata_from_imds.return_value = imds_data ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {'HostName': "myhost", 'UserName': "myuser"} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ dsrc = self._get_ds(data) ++ dsrc.get_data() ++ dsrc.setup(True) ++ ssh_keys = dsrc.get_public_ssh_keys() + self.assertEqual(ssh_keys, []) + self.assertEqual(m_parse_certificates.call_count, 0) + +@@ -1818,6 +1839,66 @@ scbus-1 on xpt0 bus 0 + self.assertIsNotNone(dsrc.metadata) + self.assertFalse(dsrc.failed_desired_api_version) + ++ @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ def test_hostname_from_imds(self, m_get_metadata_from_imds): ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {'HostName': "myhost", 'UserName': "myuser"} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA) ++ imds_data_with_os_profile["compute"]["osProfile"] = dict( ++ adminUsername="username1", ++ computerName="hostname1", ++ disablePasswordAuthentication="true" ++ ) ++ m_get_metadata_from_imds.return_value = imds_data_with_os_profile ++ dsrc = self._get_ds(data) ++ dsrc.get_data() ++ self.assertEqual(dsrc.metadata["local-hostname"], "hostname1") ++ ++ @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ def test_username_from_imds(self, m_get_metadata_from_imds): ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {'HostName': "myhost", 'UserName': "myuser"} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA) ++ imds_data_with_os_profile["compute"]["osProfile"] = dict( ++ adminUsername="username1", ++ computerName="hostname1", ++ disablePasswordAuthentication="true" ++ ) ++ m_get_metadata_from_imds.return_value = imds_data_with_os_profile ++ dsrc = self._get_ds(data) ++ dsrc.get_data() ++ self.assertEqual( ++ dsrc.cfg["system_info"]["default_user"]["name"], ++ "username1" ++ ) ++ ++ @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ def test_disable_password_from_imds(self, m_get_metadata_from_imds): ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {'HostName': "myhost", 'UserName': "myuser"} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ imds_data_with_os_profile = copy.deepcopy(NETWORK_METADATA) ++ imds_data_with_os_profile["compute"]["osProfile"] = dict( ++ adminUsername="username1", ++ computerName="hostname1", ++ disablePasswordAuthentication="true" ++ ) ++ m_get_metadata_from_imds.return_value = imds_data_with_os_profile ++ dsrc = self._get_ds(data) ++ dsrc.get_data() ++ self.assertTrue(dsrc.metadata["disable_password"]) ++ + + class TestAzureBounce(CiTestCase): + +-- +2.27.0 + diff --git a/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch b/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch new file mode 100644 index 0000000..2d02d7d --- /dev/null +++ b/SOURCES/ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch @@ -0,0 +1,315 @@ +From 816fe5c2e6d5dcc68f292092b00b2acfbc4c8e88 Mon Sep 17 00:00:00 2001 +From: aswinrajamannar <39812128+aswinrajamannar@users.noreply.github.com> +Date: Mon, 26 Apr 2021 07:28:39 -0700 +Subject: [PATCH 6/7] Azure: Retry net metadata during nic attach for + non-timeout errs (#878) + +RH-Author: Eduardo Otubo <otubo@redhat.com> +RH-MergeRequest: 18: Add support for userdata on Azure from IMDS +RH-Commit: [6/7] 794cd340644260bb43a7c8582a8067f403b9842d (otubo/cloud-init-src) +RH-Bugzilla: 2042351 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> + +When network interfaces are hot-attached to the VM, attempting to get +network metadata might return 410 (or 500, 503 etc) because the info +is not yet available. In those cases, we retry getting the metadata +before giving up. The only case where we can move on to wait for more +nic attach events is if the call times out despite retries, which +means the interface is not likely a primary interface, and we should +try for more nic attach events. +--- + cloudinit/sources/DataSourceAzure.py | 65 +++++++++++-- + tests/unittests/test_datasource/test_azure.py | 95 ++++++++++++++++--- + 2 files changed, 140 insertions(+), 20 deletions(-) + +diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py +index 6d7954ee..d0be6d84 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -17,6 +17,7 @@ from time import sleep + from xml.dom import minidom + import xml.etree.ElementTree as ET + from enum import Enum ++import requests + + from cloudinit import dmi + from cloudinit import log as logging +@@ -665,7 +666,9 @@ class DataSourceAzure(sources.DataSource): + self, + fallback_nic, + retries, +- md_type=metadata_type.compute): ++ md_type=metadata_type.compute, ++ exc_cb=retry_on_url_exc, ++ infinite=False): + """ + Wrapper for get_metadata_from_imds so that we can have flexibility + in which IMDS api-version we use. If a particular instance of IMDS +@@ -685,7 +688,8 @@ class DataSourceAzure(sources.DataSource): + fallback_nic=fallback_nic, + retries=0, + md_type=md_type, +- api_version=IMDS_VER_WANT ++ api_version=IMDS_VER_WANT, ++ exc_cb=exc_cb + ) + except UrlError as err: + LOG.info( +@@ -708,7 +712,9 @@ class DataSourceAzure(sources.DataSource): + fallback_nic=fallback_nic, + retries=retries, + md_type=md_type, +- api_version=IMDS_VER_MIN ++ api_version=IMDS_VER_MIN, ++ exc_cb=exc_cb, ++ infinite=infinite + ) + + def device_name_to_device(self, name): +@@ -938,6 +944,9 @@ class DataSourceAzure(sources.DataSource): + is_primary = False + expected_nic_count = -1 + imds_md = None ++ metadata_poll_count = 0 ++ metadata_logging_threshold = 1 ++ metadata_timeout_count = 0 + + # For now, only a VM's primary NIC can contact IMDS and WireServer. If + # DHCP fails for a NIC, we have no mechanism to determine if the NIC is +@@ -962,14 +971,48 @@ class DataSourceAzure(sources.DataSource): + % (ifname, e), logger_func=LOG.error) + raise + ++ # Retry polling network metadata for a limited duration only when the ++ # calls fail due to timeout. This is because the platform drops packets ++ # going towards IMDS when it is not a primary nic. If the calls fail ++ # due to other issues like 410, 503 etc, then it means we are primary ++ # but IMDS service is unavailable at the moment. Retry indefinitely in ++ # those cases since we cannot move on without the network metadata. ++ def network_metadata_exc_cb(msg, exc): ++ nonlocal metadata_timeout_count, metadata_poll_count ++ nonlocal metadata_logging_threshold ++ ++ metadata_poll_count = metadata_poll_count + 1 ++ ++ # Log when needed but back off exponentially to avoid exploding ++ # the log file. ++ if metadata_poll_count >= metadata_logging_threshold: ++ metadata_logging_threshold *= 2 ++ report_diagnostic_event( ++ "Ran into exception when attempting to reach %s " ++ "after %d polls." % (msg, metadata_poll_count), ++ logger_func=LOG.error) ++ ++ if isinstance(exc, UrlError): ++ report_diagnostic_event("poll IMDS with %s failed. " ++ "Exception: %s and code: %s" % ++ (msg, exc.cause, exc.code), ++ logger_func=LOG.error) ++ ++ if exc.cause and isinstance(exc.cause, requests.Timeout): ++ metadata_timeout_count = metadata_timeout_count + 1 ++ return (metadata_timeout_count <= 10) ++ return True ++ + # Primary nic detection will be optimized in the future. The fact that + # primary nic is being attached first helps here. Otherwise each nic + # could add several seconds of delay. + try: + imds_md = self.get_imds_data_with_api_fallback( + ifname, +- 5, +- metadata_type.network ++ 0, ++ metadata_type.network, ++ network_metadata_exc_cb, ++ True + ) + except Exception as e: + LOG.warning( +@@ -2139,7 +2182,9 @@ def _generate_network_config_from_fallback_config() -> dict: + def get_metadata_from_imds(fallback_nic, + retries, + md_type=metadata_type.compute, +- api_version=IMDS_VER_MIN): ++ api_version=IMDS_VER_MIN, ++ exc_cb=retry_on_url_exc, ++ infinite=False): + """Query Azure's instance metadata service, returning a dictionary. + + If network is not up, setup ephemeral dhcp on fallback_nic to talk to the +@@ -2158,7 +2203,7 @@ def get_metadata_from_imds(fallback_nic, + kwargs = {'logfunc': LOG.debug, + 'msg': 'Crawl of Azure Instance Metadata Service (IMDS)', + 'func': _get_metadata_from_imds, +- 'args': (retries, md_type, api_version,)} ++ 'args': (retries, exc_cb, md_type, api_version, infinite)} + if net.is_up(fallback_nic): + return util.log_time(**kwargs) + else: +@@ -2176,14 +2221,16 @@ def get_metadata_from_imds(fallback_nic, + @azure_ds_telemetry_reporter + def _get_metadata_from_imds( + retries, ++ exc_cb, + md_type=metadata_type.compute, +- api_version=IMDS_VER_MIN): ++ api_version=IMDS_VER_MIN, ++ infinite=False): + url = "{}?api-version={}".format(md_type.value, api_version) + headers = {"Metadata": "true"} + try: + response = readurl( + url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers, +- retries=retries, exception_cb=retry_on_url_exc) ++ retries=retries, exception_cb=exc_cb, infinite=infinite) + except Exception as e: + # pylint:disable=no-member + if isinstance(e, UrlError) and e.code == 400: +diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py +index d9817d84..c4a8e08d 100644 +--- a/tests/unittests/test_datasource/test_azure.py ++++ b/tests/unittests/test_datasource/test_azure.py +@@ -448,7 +448,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): + "http://169.254.169.254/metadata/instance?api-version=" + "2019-06-01", exception_cb=mock.ANY, + headers=mock.ANY, retries=mock.ANY, +- timeout=mock.ANY) ++ timeout=mock.ANY, infinite=False) + + @mock.patch(MOCKPATH + 'readurl', autospec=True) + @mock.patch(MOCKPATH + 'EphemeralDHCPv4') +@@ -467,7 +467,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): + "http://169.254.169.254/metadata/instance/network?api-version=" + "2019-06-01", exception_cb=mock.ANY, + headers=mock.ANY, retries=mock.ANY, +- timeout=mock.ANY) ++ timeout=mock.ANY, infinite=False) + + @mock.patch(MOCKPATH + 'readurl', autospec=True) + @mock.patch(MOCKPATH + 'EphemeralDHCPv4') +@@ -486,7 +486,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): + "http://169.254.169.254/metadata/instance?api-version=" + "2019-06-01", exception_cb=mock.ANY, + headers=mock.ANY, retries=mock.ANY, +- timeout=mock.ANY) ++ timeout=mock.ANY, infinite=False) + + @mock.patch(MOCKPATH + 'readurl', autospec=True) + @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting', autospec=True) +@@ -511,7 +511,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase): + m_readurl.assert_called_with( + self.network_md_url, exception_cb=mock.ANY, + headers={'Metadata': 'true'}, retries=2, +- timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS) ++ timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS, infinite=False) + + @mock.patch('cloudinit.url_helper.time.sleep') + @mock.patch(MOCKPATH + 'net.is_up', autospec=True) +@@ -2694,15 +2694,22 @@ class TestPreprovisioningHotAttachNics(CiTestCase): + + def nic_attach_ret(nl_sock, nics_found): + nonlocal m_attach_call_count +- if m_attach_call_count == 0: +- m_attach_call_count = m_attach_call_count + 1 ++ m_attach_call_count = m_attach_call_count + 1 ++ if m_attach_call_count == 1: + return "eth0" +- return "eth1" ++ elif m_attach_call_count == 2: ++ return "eth1" ++ raise RuntimeError("Must have found primary nic by now.") ++ ++ # Simulate two NICs by adding the same one twice. ++ md = { ++ "interface": [ ++ IMDS_NETWORK_METADATA['interface'][0], ++ IMDS_NETWORK_METADATA['interface'][0] ++ ] ++ } + +- def network_metadata_ret(ifname, retries, type): +- # Simulate two NICs by adding the same one twice. +- md = IMDS_NETWORK_METADATA +- md['interface'].append(md['interface'][0]) ++ def network_metadata_ret(ifname, retries, type, exc_cb, infinite): + if ifname == "eth0": + return md + raise requests.Timeout('Fake connection timeout') +@@ -2724,6 +2731,72 @@ class TestPreprovisioningHotAttachNics(CiTestCase): + self.assertEqual(1, m_imds.call_count) + self.assertEqual(2, m_link_up.call_count) + ++ @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback') ++ @mock.patch(MOCKPATH + 'EphemeralDHCPv4') ++ def test_check_if_nic_is_primary_retries_on_failures( ++ self, m_dhcpv4, m_imds): ++ """Retry polling for network metadata on all failures except timeout""" ++ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths) ++ lease = { ++ 'interface': 'eth9', 'fixed-address': '192.168.2.9', ++ 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0', ++ 'unknown-245': '624c3620'} ++ ++ eth0Retries = [] ++ eth1Retries = [] ++ # Simulate two NICs by adding the same one twice. ++ md = { ++ "interface": [ ++ IMDS_NETWORK_METADATA['interface'][0], ++ IMDS_NETWORK_METADATA['interface'][0] ++ ] ++ } ++ ++ def network_metadata_ret(ifname, retries, type, exc_cb, infinite): ++ nonlocal eth0Retries, eth1Retries ++ ++ # Simulate readurl functionality with retries and ++ # exception callbacks so that the callback logic can be ++ # validated. ++ if ifname == "eth0": ++ cause = requests.HTTPError() ++ for _ in range(0, 15): ++ error = url_helper.UrlError(cause=cause, code=410) ++ eth0Retries.append(exc_cb("No goal state.", error)) ++ else: ++ cause = requests.Timeout('Fake connection timeout') ++ for _ in range(0, 10): ++ error = url_helper.UrlError(cause=cause) ++ eth1Retries.append(exc_cb("Connection timeout", error)) ++ # Should stop retrying after 10 retries ++ eth1Retries.append(exc_cb("Connection timeout", error)) ++ raise cause ++ return md ++ ++ m_imds.side_effect = network_metadata_ret ++ ++ dhcp_ctx = mock.MagicMock(lease=lease) ++ dhcp_ctx.obtain_lease.return_value = lease ++ m_dhcpv4.return_value = dhcp_ctx ++ ++ is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth0") ++ self.assertEqual(True, is_primary) ++ self.assertEqual(2, expected_nic_count) ++ ++ # All Eth0 errors are non-timeout errors. So we should have been ++ # retrying indefinitely until success. ++ for i in eth0Retries: ++ self.assertTrue(i) ++ ++ is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth1") ++ self.assertEqual(False, is_primary) ++ ++ # All Eth1 errors are timeout errors. Retry happens for a max of 10 and ++ # then we should have moved on assuming it is not the primary nic. ++ for i in range(0, 10): ++ self.assertTrue(eth1Retries[i]) ++ self.assertFalse(eth1Retries[10]) ++ + @mock.patch('cloudinit.distros.networking.LinuxNetworking.try_set_link_up') + def test_wait_for_link_up_returns_if_already_up( + self, m_is_link_up): +-- +2.27.0 + diff --git a/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch b/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch new file mode 100644 index 0000000..0f17062 --- /dev/null +++ b/SOURCES/ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch @@ -0,0 +1,129 @@ +From 0def71378dc7abf682727c600b696f7313cdcf60 Mon Sep 17 00:00:00 2001 +From: Anh Vo <anhvo@microsoft.com> +Date: Tue, 27 Apr 2021 13:40:59 -0400 +Subject: [PATCH 7/7] Azure: adding support for consuming userdata from IMDS + (#884) + +RH-Author: Eduardo Otubo <otubo@redhat.com> +RH-MergeRequest: 18: Add support for userdata on Azure from IMDS +RH-Commit: [7/7] 1e7ab925162ed9ef2c9b5b9f5c6d5e6ec6e623dd (otubo/cloud-init-src) +RH-Bugzilla: 2042351 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + cloudinit/sources/DataSourceAzure.py | 23 ++++++++- + tests/unittests/test_datasource/test_azure.py | 50 +++++++++++++++++++ + 2 files changed, 72 insertions(+), 1 deletion(-) + +diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py +index d0be6d84..a66f023d 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -83,7 +83,7 @@ AGENT_SEED_DIR = '/var/lib/waagent' + IMDS_TIMEOUT_IN_SECONDS = 2 + IMDS_URL = "http://169.254.169.254/metadata" + IMDS_VER_MIN = "2019-06-01" +-IMDS_VER_WANT = "2020-10-01" ++IMDS_VER_WANT = "2021-01-01" + + + # This holds SSH key data including if the source was +@@ -539,6 +539,20 @@ class DataSourceAzure(sources.DataSource): + imds_disable_password + ) + crawled_data['metadata']['disable_password'] = imds_disable_password # noqa: E501 ++ ++ # only use userdata from imds if OVF did not provide custom data ++ # userdata provided by IMDS is always base64 encoded ++ if not userdata_raw: ++ imds_userdata = _userdata_from_imds(imds_md) ++ if imds_userdata: ++ LOG.debug("Retrieved userdata from IMDS") ++ try: ++ crawled_data['userdata_raw'] = base64.b64decode( ++ ''.join(imds_userdata.split())) ++ except Exception: ++ report_diagnostic_event( ++ "Bad userdata in IMDS", ++ logger_func=LOG.warning) + found = cdev + + report_diagnostic_event( +@@ -1512,6 +1526,13 @@ def _username_from_imds(imds_data): + return None + + ++def _userdata_from_imds(imds_data): ++ try: ++ return imds_data['compute']['userData'] ++ except KeyError: ++ return None ++ ++ + def _hostname_from_imds(imds_data): + try: + return imds_data['compute']['osProfile']['computerName'] +diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py +index c4a8e08d..f8433690 100644 +--- a/tests/unittests/test_datasource/test_azure.py ++++ b/tests/unittests/test_datasource/test_azure.py +@@ -1899,6 +1899,56 @@ scbus-1 on xpt0 bus 0 + dsrc.get_data() + self.assertTrue(dsrc.metadata["disable_password"]) + ++ @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ def test_userdata_from_imds(self, m_get_metadata_from_imds): ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ odata = {'HostName': "myhost", 'UserName': "myuser"} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ userdata = "userdataImds" ++ imds_data = copy.deepcopy(NETWORK_METADATA) ++ imds_data["compute"]["osProfile"] = dict( ++ adminUsername="username1", ++ computerName="hostname1", ++ disablePasswordAuthentication="true", ++ ) ++ imds_data["compute"]["userData"] = b64e(userdata) ++ m_get_metadata_from_imds.return_value = imds_data ++ dsrc = self._get_ds(data) ++ ret = dsrc.get_data() ++ self.assertTrue(ret) ++ self.assertEqual(dsrc.userdata_raw, userdata.encode('utf-8')) ++ ++ @mock.patch(MOCKPATH + 'get_metadata_from_imds') ++ def test_userdata_from_imds_with_customdata_from_OVF( ++ self, m_get_metadata_from_imds): ++ userdataOVF = "userdataOVF" ++ odata = { ++ 'HostName': "myhost", 'UserName': "myuser", ++ 'UserData': {'text': b64e(userdataOVF), 'encoding': 'base64'} ++ } ++ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}} ++ data = { ++ 'ovfcontent': construct_valid_ovf_env(data=odata), ++ 'sys_cfg': sys_cfg ++ } ++ ++ userdataImds = "userdataImds" ++ imds_data = copy.deepcopy(NETWORK_METADATA) ++ imds_data["compute"]["osProfile"] = dict( ++ adminUsername="username1", ++ computerName="hostname1", ++ disablePasswordAuthentication="true", ++ ) ++ imds_data["compute"]["userData"] = b64e(userdataImds) ++ m_get_metadata_from_imds.return_value = imds_data ++ dsrc = self._get_ds(data) ++ ret = dsrc.get_data() ++ self.assertTrue(ret) ++ self.assertEqual(dsrc.userdata_raw, userdataOVF.encode('utf-8')) ++ + + class TestAzureBounce(CiTestCase): + +-- +2.27.0 + diff --git a/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch b/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch new file mode 100644 index 0000000..947a035 --- /dev/null +++ b/SOURCES/ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch @@ -0,0 +1,177 @@ +From 2ece71923a37a5e1107c80f091a1cc620943fbf2 Mon Sep 17 00:00:00 2001 +From: Anh Vo <anhvo@microsoft.com> +Date: Fri, 23 Apr 2021 10:18:05 -0400 +Subject: [PATCH 4/7] Azure: eject the provisioning iso before reporting ready + (#861) + +RH-Author: Eduardo Otubo <otubo@redhat.com> +RH-MergeRequest: 18: Add support for userdata on Azure from IMDS +RH-Commit: [4/7] 63e379a4406530c0c15c733f8eee35421079508b (otubo/cloud-init-src) +RH-Bugzilla: 2042351 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> + +Due to hyper-v implementations, iso ejection is more efficient if performed +from within the guest. The code will attempt to perform a best-effort ejection. +Failure during ejection will not prevent reporting ready from happening. If iso +ejection is successful, later iso ejection from the platform will be a no-op. +In the event the iso ejection from the guest fails, iso ejection will still happen at +the platform level. +--- + cloudinit/sources/DataSourceAzure.py | 22 +++++++++++++++--- + cloudinit/sources/helpers/azure.py | 23 ++++++++++++++++--- + .../test_datasource/test_azure_helper.py | 13 +++++++++-- + 3 files changed, 50 insertions(+), 8 deletions(-) + +diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py +index 020b7006..39e67c4f 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -332,6 +332,7 @@ class DataSourceAzure(sources.DataSource): + dsname = 'Azure' + _negotiated = False + _metadata_imds = sources.UNSET ++ _ci_pkl_version = 1 + + def __init__(self, sys_cfg, distro, paths): + sources.DataSource.__init__(self, sys_cfg, distro, paths) +@@ -346,8 +347,13 @@ class DataSourceAzure(sources.DataSource): + # Regenerate network config new_instance boot and every boot + self.update_events['network'].add(EventType.BOOT) + self._ephemeral_dhcp_ctx = None +- + self.failed_desired_api_version = False ++ self.iso_dev = None ++ ++ def _unpickle(self, ci_pkl_version: int) -> None: ++ super()._unpickle(ci_pkl_version) ++ if "iso_dev" not in self.__dict__: ++ self.iso_dev = None + + def __str__(self): + root = sources.DataSource.__str__(self) +@@ -459,6 +465,13 @@ class DataSourceAzure(sources.DataSource): + '%s was not mountable' % cdev, logger_func=LOG.warning) + continue + ++ report_diagnostic_event("Found provisioning metadata in %s" % cdev, ++ logger_func=LOG.debug) ++ ++ # save the iso device for ejection before reporting ready ++ if cdev.startswith("/dev"): ++ self.iso_dev = cdev ++ + perform_reprovision = reprovision or self._should_reprovision(ret) + perform_reprovision_after_nic_attach = ( + reprovision_after_nic_attach or +@@ -1226,7 +1239,9 @@ class DataSourceAzure(sources.DataSource): + @return: The success status of sending the ready signal. + """ + try: +- get_metadata_from_fabric(None, lease['unknown-245']) ++ get_metadata_from_fabric(fallback_lease_file=None, ++ dhcp_opts=lease['unknown-245'], ++ iso_dev=self.iso_dev) + return True + except Exception as e: + report_diagnostic_event( +@@ -1332,7 +1347,8 @@ class DataSourceAzure(sources.DataSource): + metadata_func = partial(get_metadata_from_fabric, + fallback_lease_file=self. + dhclient_lease_file, +- pubkey_info=pubkey_info) ++ pubkey_info=pubkey_info, ++ iso_dev=self.iso_dev) + + LOG.debug("negotiating with fabric via agent command %s", + self.ds_cfg['agent_command']) +diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py +index 03e7156b..ad476076 100755 +--- a/cloudinit/sources/helpers/azure.py ++++ b/cloudinit/sources/helpers/azure.py +@@ -865,7 +865,19 @@ class WALinuxAgentShim: + return endpoint_ip_address + + @azure_ds_telemetry_reporter +- def register_with_azure_and_fetch_data(self, pubkey_info=None) -> dict: ++ def eject_iso(self, iso_dev) -> None: ++ try: ++ LOG.debug("Ejecting the provisioning iso") ++ subp.subp(['eject', iso_dev]) ++ except Exception as e: ++ report_diagnostic_event( ++ "Failed ejecting the provisioning iso: %s" % e, ++ logger_func=LOG.debug) ++ ++ @azure_ds_telemetry_reporter ++ def register_with_azure_and_fetch_data(self, ++ pubkey_info=None, ++ iso_dev=None) -> dict: + """Gets the VM's GoalState from Azure, uses the GoalState information + to report ready/send the ready signal/provisioning complete signal to + Azure, and then uses pubkey_info to filter and obtain the user's +@@ -891,6 +903,10 @@ class WALinuxAgentShim: + ssh_keys = self._get_user_pubkeys(goal_state, pubkey_info) + health_reporter = GoalStateHealthReporter( + goal_state, self.azure_endpoint_client, self.endpoint) ++ ++ if iso_dev is not None: ++ self.eject_iso(iso_dev) ++ + health_reporter.send_ready_signal() + return {'public-keys': ssh_keys} + +@@ -1046,11 +1062,12 @@ class WALinuxAgentShim: + + @azure_ds_telemetry_reporter + def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None, +- pubkey_info=None): ++ pubkey_info=None, iso_dev=None): + shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file, + dhcp_options=dhcp_opts) + try: +- return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info) ++ return shim.register_with_azure_and_fetch_data( ++ pubkey_info=pubkey_info, iso_dev=iso_dev) + finally: + shim.clean_up() + +diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py +index 63482c6c..552c7905 100644 +--- a/tests/unittests/test_datasource/test_azure_helper.py ++++ b/tests/unittests/test_datasource/test_azure_helper.py +@@ -1009,6 +1009,14 @@ class TestWALinuxAgentShim(CiTestCase): + self.GoalState.return_value.container_id = self.test_container_id + self.GoalState.return_value.instance_id = self.test_instance_id + ++ def test_eject_iso_is_called(self): ++ shim = wa_shim() ++ with mock.patch.object( ++ shim, 'eject_iso', autospec=True ++ ) as m_eject_iso: ++ shim.register_with_azure_and_fetch_data(iso_dev="/dev/sr0") ++ m_eject_iso.assert_called_once_with("/dev/sr0") ++ + def test_http_client_does_not_use_certificate_for_report_ready(self): + shim = wa_shim() + shim.register_with_azure_and_fetch_data() +@@ -1283,13 +1291,14 @@ class TestGetMetadataGoalStateXMLAndReportReadyToFabric(CiTestCase): + + def test_calls_shim_register_with_azure_and_fetch_data(self): + m_pubkey_info = mock.MagicMock() +- azure_helper.get_metadata_from_fabric(pubkey_info=m_pubkey_info) ++ azure_helper.get_metadata_from_fabric( ++ pubkey_info=m_pubkey_info, iso_dev="/dev/sr0") + self.assertEqual( + 1, + self.m_shim.return_value + .register_with_azure_and_fetch_data.call_count) + self.assertEqual( +- mock.call(pubkey_info=m_pubkey_info), ++ mock.call(iso_dev="/dev/sr0", pubkey_info=m_pubkey_info), + self.m_shim.return_value + .register_with_azure_and_fetch_data.call_args) + +-- +2.27.0 + diff --git a/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch b/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch new file mode 100644 index 0000000..33a8acc --- /dev/null +++ b/SOURCES/ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch @@ -0,0 +1,90 @@ +From 3ee42e6e6ca51b3fd0b6461f707d62c89d54e227 Mon Sep 17 00:00:00 2001 +From: Johnson Shi <Johnson.Shi@microsoft.com> +Date: Thu, 25 Mar 2021 07:20:10 -0700 +Subject: [PATCH 2/7] Azure helper: Ensure Azure http handler sleeps between + retries (#842) + +RH-Author: Eduardo Otubo <otubo@redhat.com> +RH-MergeRequest: 18: Add support for userdata on Azure from IMDS +RH-Commit: [2/7] 65672cdfe2265f32e6d3c440ba5a8accafdb6ca6 (otubo/cloud-init-src) +RH-Bugzilla: 2042351 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> + +Ensure that the Azure helper's http handler sleeps a fixed duration +between retry failure attempts. The http handler will sleep a fixed +duration between failed attempts regardless of whether the attempt +failed due to (1) request timing out or (2) instant failure (no +timeout). + +Due to certain platform issues, the http request to the Azure endpoint +may instantly fail without reaching the http timeout duration. Without +sleeping a fixed duration in between retry attempts, the http handler +will loop through the max retry attempts quickly. This causes the +communication between cloud-init and the Azure platform to be less +resilient due to the short total duration if there is no sleep in +between retries. +--- + cloudinit/sources/helpers/azure.py | 2 ++ + tests/unittests/test_datasource/test_azure_helper.py | 11 +++++++++-- + 2 files changed, 11 insertions(+), 2 deletions(-) + +diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py +index d3055d08..03e7156b 100755 +--- a/cloudinit/sources/helpers/azure.py ++++ b/cloudinit/sources/helpers/azure.py +@@ -303,6 +303,7 @@ def http_with_retries(url, **kwargs) -> str: + + max_readurl_attempts = 240 + default_readurl_timeout = 5 ++ sleep_duration_between_retries = 5 + periodic_logging_attempts = 12 + + if 'timeout' not in kwargs: +@@ -338,6 +339,7 @@ def http_with_retries(url, **kwargs) -> str: + 'attempt %d with exception: %s' % + (url, attempt, e), + logger_func=LOG.debug) ++ time.sleep(sleep_duration_between_retries) + + raise exc + +diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py +index b8899807..63482c6c 100644 +--- a/tests/unittests/test_datasource/test_azure_helper.py ++++ b/tests/unittests/test_datasource/test_azure_helper.py +@@ -384,6 +384,7 @@ class TestAzureHelperHttpWithRetries(CiTestCase): + + max_readurl_attempts = 240 + default_readurl_timeout = 5 ++ sleep_duration_between_retries = 5 + periodic_logging_attempts = 12 + + def setUp(self): +@@ -394,8 +395,8 @@ class TestAzureHelperHttpWithRetries(CiTestCase): + self.m_readurl = patches.enter_context( + mock.patch.object( + azure_helper.url_helper, 'readurl', mock.MagicMock())) +- patches.enter_context( +- mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock())) ++ self.m_sleep = patches.enter_context( ++ mock.patch.object(azure_helper.time, 'sleep', autospec=True)) + + def test_http_with_retries(self): + self.m_readurl.return_value = 'TestResp' +@@ -438,6 +439,12 @@ class TestAzureHelperHttpWithRetries(CiTestCase): + self.m_readurl.call_count, + self.periodic_logging_attempts + 1) + ++ # Ensure that cloud-init did sleep between each failed request ++ self.assertEqual( ++ self.m_sleep.call_count, ++ self.periodic_logging_attempts) ++ self.m_sleep.assert_called_with(self.sleep_duration_between_retries) ++ + def test_http_with_retries_long_delay_logs_periodic_failure_msg(self): + self.m_readurl.side_effect = \ + [SentinelException] * self.periodic_logging_attempts + \ +-- +2.27.0 + diff --git a/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch b/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch new file mode 100644 index 0000000..666ef73 --- /dev/null +++ b/SOURCES/ci-Change-netifaces-dependency-to-0.10.4-965.patch @@ -0,0 +1,47 @@ +From 18138313e009a08592fe79c5e66d6eba8f027f19 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Fri, 14 Jan 2022 16:49:57 +0100 +Subject: [PATCH 2/5] Change netifaces dependency to 0.10.4 (#965) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 17: Datasource for VMware +RH-Commit: [2/5] 8688e8b955a7ee15cf66de0b2a242c7c418b7630 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2040090 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> + +commit b9d308b4d61d22bacc05bcae59819755975631f8 +Author: Andrew Kutz <101085+akutz@users.noreply.github.com> +Date: Tue Aug 10 15:10:44 2021 -0500 + + Change netifaces dependency to 0.10.4 (#965) + + Change netifaces dependency to 0.10.4 + + Currently versions Ubuntu <=20.10 use netifaces 0.10.4 By requiring + netifaces 0.10.9, the VMware datasource omitted itself from cloud-init + on Ubuntu <=20.10. + + This patch changes the netifaces dependency to 0.10.4. While it is true + there are patches to netifaces post 0.10.4 that are desirable, testing + against the most common network configuration was performed to verify + the VMware datasource will still function with netifaces 0.10.4. + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + requirements.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/requirements.txt b/requirements.txt +index 41d01d62..c4adc455 100644 +--- a/requirements.txt ++++ b/requirements.txt +@@ -40,4 +40,4 @@ jsonschema + # and still participate in instance-data by gathering the network in detail at + # runtime and merge that information into the metadata and repersist that to + # disk. +-netifaces>=0.10.9 ++netifaces>=0.10.4 +-- +2.27.0 + diff --git a/SOURCES/ci-Datasource-for-VMware-953.patch b/SOURCES/ci-Datasource-for-VMware-953.patch new file mode 100644 index 0000000..74af6f6 --- /dev/null +++ b/SOURCES/ci-Datasource-for-VMware-953.patch @@ -0,0 +1,2201 @@ +From 078f3a218394eef3b28a2a061d836efe42b6c9ed Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Fri, 14 Jan 2022 16:49:28 +0100 +Subject: [PATCH 1/5] Datasource for VMware (#953) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 17: Datasource for VMware +RH-Commit: [1/5] 7b47334ec524dcf1b8edd02b65df7d0ff5a366e0 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2040090 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> + +commit 8b4a9bc7b81e61943af873bad92e2133f8275b0b +Author: Andrew Kutz <101085+akutz@users.noreply.github.com> +Date: Mon Aug 9 21:24:07 2021 -0500 + + Datasource for VMware (#953) + + This patch finally introduces the Cloud-Init Datasource for VMware + GuestInfo as a part of cloud-init proper. This datasource has existed + since 2018, and rapidly became the de facto datasource for developers + working with Packer, Terraform, for projects like kube-image-builder, + and the de jure datasource for Photon OS. + + The major change to the datasource from its previous incarnation is + the name. Now named DatasourceVMware, this new version of the + datasource will allow multiple transport types in addition to + GuestInfo keys. + + This datasource includes several unique features developed to address + real-world situations: + + * Support for reading any key (metadata, userdata, vendordata) both + from the guestinfo table when running on a VM in vSphere as well as + from an environment variable when running inside of a container, + useful for rapid dev/test. + + * Allows booting with DHCP while still providing full participation + in Cloud-Init instance data and Jinja queries. The netifaces library + provides the ability to inspect the network after it is online, + and the runtime network configuration is then merged into the + existing metadata and persisted to disk. + + * Advertises the local_ipv4 and local_ipv6 addresses via guestinfo + as well. This is useful as Guest Tools is not always able to + identify what would be considered the local address. + + The primary author and current steward of this datasource spoke at + Cloud-Init Con 2020 where there was interest in contributing this datasource + to the Cloud-Init codebase. + + The datasource currently lives in its own GitHub repository at + https://github.com/vmware/cloud-init-vmware-guestinfo. Once the datasource + is merged into Cloud-Init, the old repository will be deprecated. + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + README.md | 2 +- + cloudinit/settings.py | 1 + + cloudinit/sources/DataSourceVMware.py | 871 ++++++++++++++++++ + doc/rtd/topics/availability.rst | 1 + + doc/rtd/topics/datasources.rst | 2 +- + doc/rtd/topics/datasources/vmware.rst | 359 ++++++++ + requirements.txt | 12 + + .../unittests/test_datasource/test_common.py | 3 + + .../unittests/test_datasource/test_vmware.py | 377 ++++++++ + tests/unittests/test_ds_identify.py | 279 +++++- + tools/.github-cla-signers | 1 + + tools/ds-identify | 76 +- + 12 files changed, 1980 insertions(+), 4 deletions(-) + create mode 100644 cloudinit/sources/DataSourceVMware.py + create mode 100644 doc/rtd/topics/datasources/vmware.rst + create mode 100644 tests/unittests/test_datasource/test_vmware.py + +diff --git a/README.md b/README.md +index 435405da..aa4fad63 100644 +--- a/README.md ++++ b/README.md +@@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! + + | Supported OSes | Supported Public Clouds | Supported Private Clouds | + | --- | --- | --- | +-| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />| ++| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br />VMware<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />| + + ## To start developing cloud-init + +diff --git a/cloudinit/settings.py b/cloudinit/settings.py +index 2acf2615..d5f32dbb 100644 +--- a/cloudinit/settings.py ++++ b/cloudinit/settings.py +@@ -42,6 +42,7 @@ CFG_BUILTIN = { + 'Exoscale', + 'RbxCloud', + 'UpCloud', ++ 'VMware', + # At the end to act as a 'catch' when none of the above work... + 'None', + ], +diff --git a/cloudinit/sources/DataSourceVMware.py b/cloudinit/sources/DataSourceVMware.py +new file mode 100644 +index 00000000..22ca63de +--- /dev/null ++++ b/cloudinit/sources/DataSourceVMware.py +@@ -0,0 +1,871 @@ ++# Cloud-Init DataSource for VMware ++# ++# Copyright (c) 2018-2021 VMware, Inc. All Rights Reserved. ++# ++# Authors: Anish Swaminathan <anishs@vmware.com> ++# Andrew Kutz <akutz@vmware.com> ++# ++# This file is part of cloud-init. See LICENSE file for license information. ++ ++"""Cloud-Init DataSource for VMware ++ ++This module provides a cloud-init datasource for VMware systems and supports ++multiple transports types, including: ++ ++ * EnvVars ++ * GuestInfo ++ ++Netifaces (https://github.com/al45tair/netifaces) ++ ++ Please note this module relies on the netifaces project to introspect the ++ runtime, network configuration of the host on which this datasource is ++ running. This is in contrast to the rest of cloud-init which uses the ++ cloudinit/netinfo module. ++ ++ The reasons for using netifaces include: ++ ++ * Netifaces is built in C and is more portable across multiple systems ++ and more deterministic than shell exec'ing local network commands and ++ parsing their output. ++ ++ * Netifaces provides a stable way to determine the view of the host's ++ network after DHCP has brought the network online. Unlike most other ++ datasources, this datasource still provides support for JINJA queries ++ based on networking information even when the network is based on a ++ DHCP lease. While this does not tie this datasource directly to ++ netifaces, it does mean the ability to consistently obtain the ++ correct information is paramount. ++ ++ * It is currently possible to execute this datasource on macOS ++ (which many developers use today) to print the output of the ++ get_host_info function. This function calls netifaces to obtain ++ the same runtime network configuration that the datasource would ++ persist to the local system's instance data. ++ ++ However, the netinfo module fails on macOS. The result is either a ++ hung operation that requires a SIGINT to return control to the user, ++ or, if brew is used to install iproute2mac, the ip commands are used ++ but produce output the netinfo module is unable to parse. ++ ++ While macOS is not a target of cloud-init, this feature is quite ++ useful when working on this datasource. ++ ++ For more information about this behavior, please see the following ++ PR comment, https://bit.ly/3fG7OVh. ++ ++ The authors of this datasource are not opposed to moving away from ++ netifaces. The goal may be to eventually do just that. This proviso was ++ added to the top of this module as a way to remind future-us and others ++ 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. ++""" ++ ++import collections ++import copy ++from distutils.spawn import find_executable ++import ipaddress ++import json ++import os ++import socket ++import time ++ ++from cloudinit import dmi, log as logging ++from cloudinit import sources ++from cloudinit import util ++from cloudinit.subp import subp, ProcessExecutionError ++ ++import netifaces ++ ++ ++PRODUCT_UUID_FILE_PATH = "/sys/class/dmi/id/product_uuid" ++ ++LOG = logging.getLogger(__name__) ++NOVAL = "No value found" ++ ++DATA_ACCESS_METHOD_ENVVAR = "envvar" ++DATA_ACCESS_METHOD_GUESTINFO = "guestinfo" ++ ++VMWARE_RPCTOOL = find_executable("vmware-rpctool") ++REDACT = "redact" ++CLEANUP_GUESTINFO = "cleanup-guestinfo" ++VMX_GUESTINFO = "VMX_GUESTINFO" ++GUESTINFO_EMPTY_YAML_VAL = "---" ++ ++LOCAL_IPV4 = "local-ipv4" ++LOCAL_IPV6 = "local-ipv6" ++WAIT_ON_NETWORK = "wait-on-network" ++WAIT_ON_NETWORK_IPV4 = "ipv4" ++WAIT_ON_NETWORK_IPV6 = "ipv6" ++ ++ ++class DataSourceVMware(sources.DataSource): ++ """ ++ Setting the hostname: ++ The hostname is set by way of the metadata key "local-hostname". ++ ++ Setting the instance ID: ++ The instance ID may be set by way of the metadata key "instance-id". ++ However, if this value is absent then the instance ID is read ++ from the file /sys/class/dmi/id/product_uuid. ++ ++ Configuring the network: ++ The network is configured by setting the metadata key "network" ++ with a value consistent with Network Config Versions 1 or 2, ++ depending on the Linux distro's version of cloud-init: ++ ++ Network Config Version 1 - http://bit.ly/cloudinit-net-conf-v1 ++ 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. ++ ++ 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" ++ ++ def __init__(self, sys_cfg, distro, paths, ud_proc=None): ++ sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc) ++ ++ self.data_access_method = None ++ self.vmware_rpctool = VMWARE_RPCTOOL ++ ++ def _get_data(self): ++ """ ++ _get_data loads the metadata, userdata, and vendordata from one of ++ the following locations in the given order: ++ ++ * envvars ++ * guestinfo ++ ++ Please note when updating this function with support for new data ++ transports, the order should match the order in the dscheck_VMware ++ function from the file ds-identify. ++ """ ++ ++ # Initialize the locally scoped metadata, userdata, and vendordata ++ # variables. They are assigned below depending on the detected data ++ # 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") ++ ++ 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 ++ ++ if not self.data_access_method: ++ LOG.error("failed to find a valid data access method") ++ return False ++ ++ LOG.info("using data access method %s", self._get_subplatform()) ++ ++ # Get the metadata. ++ self.metadata = process_metadata(load_json_or_yaml(md)) ++ ++ # Get the user data. ++ self.userdata_raw = ud ++ ++ # Get the vendor data. ++ self.vendordata_raw = vd ++ ++ # Redact any sensitive information. ++ self.redact_keys() ++ ++ # get_data returns true if there is any available metadata, ++ # userdata, or vendordata. ++ if self.metadata or self.userdata_raw or self.vendordata_raw: ++ return True ++ else: ++ return False ++ ++ def setup(self, is_new_instance): ++ """setup(is_new_instance) ++ ++ This is called before user-data and vendor-data have been processed. ++ ++ Unless the datasource has set mode to 'local', then networking ++ per 'fallback' or per 'network_config' will have been written and ++ brought up the OS at this point. ++ """ ++ ++ host_info = wait_on_network(self.metadata) ++ LOG.info("got host-info: %s", host_info) ++ ++ # Reflect any possible local IPv4 or IPv6 addresses in the guest ++ # info. ++ advertise_local_ip_addrs(host_info) ++ ++ # Ensure the metadata gets updated with information about the ++ # host, including the network interfaces, default IP addresses, ++ # etc. ++ self.metadata = util.mergemanydict([self.metadata, host_info]) ++ ++ # Persist the instance data for versions of cloud-init that support ++ # doing so. This occurs here rather than in the get_data call in ++ # order to ensure that the network interfaces are up and can be ++ # persisted with the metadata. ++ self.persist_instance_data() ++ ++ def _get_subplatform(self): ++ get_key_name_fn = None ++ if self.data_access_method == DATA_ACCESS_METHOD_ENVVAR: ++ 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 ++ else: ++ return sources.METADATA_UNKNOWN ++ ++ return "%s (%s)" % ( ++ self.data_access_method, ++ get_key_name_fn("metadata"), ++ ) ++ ++ @property ++ def network_config(self): ++ if "network" in self.metadata: ++ LOG.debug("using metadata network config") ++ else: ++ LOG.debug("using fallback network config") ++ self.metadata["network"] = { ++ "config": self.distro.generate_fallback_config(), ++ } ++ return self.metadata["network"]["config"] ++ ++ def get_instance_id(self): ++ # Pull the instance ID out of the metadata if present. Otherwise ++ # read the file /sys/class/dmi/id/product_uuid for the instance ID. ++ if self.metadata and "instance-id" in self.metadata: ++ return self.metadata["instance-id"] ++ with open(PRODUCT_UUID_FILE_PATH, "r") as id_file: ++ self.metadata["instance-id"] = str(id_file.read()).rstrip().lower() ++ return self.metadata["instance-id"] ++ ++ def get_public_ssh_keys(self): ++ for key_name in ( ++ "public-keys-data", ++ "public_keys_data", ++ "public-keys", ++ "public_keys", ++ ): ++ if key_name in self.metadata: ++ return sources.normalize_pubkey_data(self.metadata[key_name]) ++ return [] ++ ++ def redact_keys(self): ++ # Determine if there are any keys to redact. ++ keys_to_redact = None ++ if REDACT in self.metadata: ++ keys_to_redact = self.metadata[REDACT] ++ elif CLEANUP_GUESTINFO in self.metadata: ++ # This is for backwards compatibility. ++ 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) ++ ++ ++def decode(key, enc_type, data): ++ """ ++ decode returns the decoded string value of data ++ key is a string used to identify the data being decoded in log messages ++ """ ++ LOG.debug("Getting encoded data for key=%s, enc=%s", key, enc_type) ++ ++ raw_data = None ++ if enc_type in ["gzip+base64", "gz+b64"]: ++ LOG.debug("Decoding %s format %s", enc_type, key) ++ raw_data = util.decomp_gzip(util.b64d(data)) ++ elif enc_type in ["base64", "b64"]: ++ LOG.debug("Decoding %s format %s", enc_type, key) ++ raw_data = util.b64d(data) ++ else: ++ LOG.debug("Plain-text data %s", key) ++ raw_data = data ++ ++ return util.decode_binary(raw_data) ++ ++ ++def get_none_if_empty_val(val): ++ """ ++ get_none_if_empty_val returns None if the provided value, once stripped ++ of its trailing whitespace, is empty or equal to GUESTINFO_EMPTY_YAML_VAL. ++ ++ The return value is always a string, regardless of whether the input is ++ a bytes class or a string. ++ """ ++ ++ # If the provided value is a bytes class, convert it to a string to ++ # simplify the rest of this function's logic. ++ val = util.decode_binary(val) ++ val = val.rstrip() ++ if len(val) == 0 or val == GUESTINFO_EMPTY_YAML_VAL: ++ return None ++ return val ++ ++ ++def advertise_local_ip_addrs(host_info): ++ """ ++ advertise_local_ip_addrs gets the local IP address information from ++ the provided host_info map and sets the addresses in the guestinfo ++ namespace ++ """ ++ if not host_info: ++ return ++ ++ # Reflect any possible local IPv4 or IPv6 addresses in the guest ++ # info. ++ local_ipv4 = host_info.get(LOCAL_IPV4) ++ if local_ipv4: ++ guestinfo_set_value(LOCAL_IPV4, local_ipv4) ++ 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) ++ LOG.info("advertised local ipv6 address %s in guestinfo", local_ipv6) ++ ++ ++def handle_returned_guestinfo_val(key, val): ++ """ ++ handle_returned_guestinfo_val returns the provided value if it is ++ not empty or set to GUESTINFO_EMPTY_YAML_VAL, otherwise None is ++ returned ++ """ ++ val = get_none_if_empty_val(val) ++ if val: ++ return val ++ LOG.debug("No value found for key %s", key) ++ return None ++ ++ ++def get_guestinfo_key_name(key): ++ return "guestinfo." + key ++ ++ ++def get_guestinfo_envvar_key_name(key): ++ return ("vmx." + get_guestinfo_key_name(key)).upper().replace(".", "_", -1) ++ ++ ++def guestinfo_envvar(key): ++ val = guestinfo_envvar_get_value(key) ++ if not val: ++ return None ++ enc_type = guestinfo_envvar_get_value(key + ".encoding") ++ return decode(get_guestinfo_envvar_key_name(key), enc_type, val) ++ ++ ++def guestinfo_envvar_get_value(key): ++ env_key = get_guestinfo_envvar_key_name(key) ++ return handle_returned_guestinfo_val(key, os.environ.get(env_key, "")) ++ ++ ++def guestinfo(key, vmware_rpctool=VMWARE_RPCTOOL): ++ """ ++ guestinfo returns the guestinfo value for the provided key, decoding ++ the value when required ++ """ ++ val = guestinfo_get_value(key, vmware_rpctool) ++ if not val: ++ return None ++ enc_type = guestinfo_get_value(key + ".encoding", vmware_rpctool) ++ return decode(get_guestinfo_key_name(key), enc_type, val) ++ ++ ++def guestinfo_get_value(key, vmware_rpctool=VMWARE_RPCTOOL): ++ """ ++ Returns a guestinfo value for the specified key. ++ """ ++ LOG.debug("Getting guestinfo value for key %s", key) ++ ++ try: ++ (stdout, stderr) = subp( ++ [ ++ vmware_rpctool, ++ "info-get " + get_guestinfo_key_name(key), ++ ] ++ ) ++ 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: ++ if error.stderr == NOVAL: ++ LOG.debug("No value found for key %s", key) ++ else: ++ util.logexc( ++ LOG, ++ "Failed to get guestinfo value for key %s: %s", ++ key, ++ error, ++ ) ++ except Exception: ++ util.logexc( ++ LOG, ++ "Unexpected error while trying to get " ++ + "guestinfo value for key %s", ++ key, ++ ) ++ ++ return None ++ ++ ++def guestinfo_set_value(key, value, vmware_rpctool=VMWARE_RPCTOOL): ++ """ ++ Sets a guestinfo value for the specified key. Set value to an empty string ++ to clear an existing guestinfo key. ++ """ ++ ++ # If value is an empty string then set it to a single space as it is not ++ # possible to set a guestinfo key to an empty string. Setting a guestinfo ++ # key to a single space is as close as it gets to clearing an existing ++ # guestinfo key. ++ if value == "": ++ value = " " ++ ++ LOG.debug("Setting guestinfo key=%s to value=%s", key, value) ++ ++ try: ++ subp( ++ [ ++ vmware_rpctool, ++ ("info-set %s %s" % (get_guestinfo_key_name(key), value)), ++ ] ++ ) ++ return True ++ except ProcessExecutionError as error: ++ util.logexc( ++ LOG, ++ "Failed to set guestinfo key=%s to value=%s: %s", ++ key, ++ value, ++ error, ++ ) ++ except Exception: ++ util.logexc( ++ LOG, ++ "Unexpected error while trying to set " ++ + "guestinfo key=%s to value=%s", ++ key, ++ value, ++ ) ++ ++ return None ++ ++ ++def guestinfo_redact_keys(keys, vmware_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 ++ YAML, cloud-init can still read it if it tries. ++ """ ++ if not keys: ++ return ++ if not type(keys) in (list, tuple): ++ keys = [keys] ++ 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 ++ ): ++ LOG.error("failed to clear %s", key_name) ++ LOG.info("clearing %s.encoding", key_name) ++ if not guestinfo_set_value(key + ".encoding", "", vmware_rpctool): ++ LOG.error("failed to clear %s.encoding", key_name) ++ ++ ++def load_json_or_yaml(data): ++ """ ++ load first attempts to unmarshal the provided data as JSON, and if ++ that fails then attempts to unmarshal the data as YAML. If data is ++ None then a new dictionary is returned. ++ """ ++ if not data: ++ return {} ++ try: ++ return util.load_json(data) ++ except (json.JSONDecodeError, TypeError): ++ return util.load_yaml(data) ++ ++ ++def process_metadata(data): ++ """ ++ process_metadata processes metadata and loads the optional network ++ configuration. ++ """ ++ network = None ++ if "network" in data: ++ network = data["network"] ++ del data["network"] ++ ++ network_enc = None ++ if "network.encoding" in data: ++ network_enc = data["network.encoding"] ++ del data["network.encoding"] ++ ++ if network: ++ if isinstance(network, collections.abc.Mapping): ++ LOG.debug("network data copied to 'config' key") ++ network = {"config": copy.deepcopy(network)} ++ else: ++ LOG.debug("network data to be decoded %s", network) ++ dec_net = decode("metadata.network", network_enc, network) ++ network = { ++ "config": load_json_or_yaml(dec_net), ++ } ++ ++ LOG.debug("network data %s", network) ++ data["network"] = network ++ ++ return data ++ ++ ++# Used to match classes to dependencies ++datasources = [ ++ (DataSourceVMware, (sources.DEP_FILESYSTEM,)), # Run at init-local ++ (DataSourceVMware, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), ++] ++ ++ ++def get_datasource_list(depends): ++ """ ++ Return a list of data sources that match this set of dependencies ++ """ ++ return sources.list_from_depends(depends, datasources) ++ ++ ++def get_default_ip_addrs(): ++ """ ++ Returns the default IPv4 and IPv6 addresses based on the device(s) used for ++ the default route. Please note that None may be returned for either address ++ family if that family has no default route or if there are multiple ++ addresses associated with the device used by the default route for a given ++ address. ++ """ ++ # TODO(promote and use netifaces in cloudinit.net* modules) ++ gateways = netifaces.gateways() ++ if "default" not in gateways: ++ return None, None ++ ++ default_gw = gateways["default"] ++ if ( ++ netifaces.AF_INET not in default_gw ++ and netifaces.AF_INET6 not in default_gw ++ ): ++ return None, None ++ ++ ipv4 = None ++ ipv6 = None ++ ++ gw4 = default_gw.get(netifaces.AF_INET) ++ if gw4: ++ _, dev4 = gw4 ++ addr4_fams = netifaces.ifaddresses(dev4) ++ if addr4_fams: ++ af_inet4 = addr4_fams.get(netifaces.AF_INET) ++ if af_inet4: ++ if len(af_inet4) > 1: ++ LOG.warning( ++ "device %s has more than one ipv4 address: %s", ++ dev4, ++ af_inet4, ++ ) ++ elif "addr" in af_inet4[0]: ++ ipv4 = af_inet4[0]["addr"] ++ ++ # Try to get the default IPv6 address by first seeing if there is a default ++ # IPv6 route. ++ gw6 = default_gw.get(netifaces.AF_INET6) ++ if gw6: ++ _, dev6 = gw6 ++ addr6_fams = netifaces.ifaddresses(dev6) ++ if addr6_fams: ++ af_inet6 = addr6_fams.get(netifaces.AF_INET6) ++ if af_inet6: ++ if len(af_inet6) > 1: ++ LOG.warning( ++ "device %s has more than one ipv6 address: %s", ++ dev6, ++ af_inet6, ++ ) ++ elif "addr" in af_inet6[0]: ++ ipv6 = af_inet6[0]["addr"] ++ ++ # If there is a default IPv4 address but not IPv6, then see if there is a ++ # single IPv6 address associated with the same device associated with the ++ # default IPv4 address. ++ if ipv4 and not ipv6: ++ af_inet6 = addr4_fams.get(netifaces.AF_INET6) ++ if af_inet6: ++ if len(af_inet6) > 1: ++ LOG.warning( ++ "device %s has more than one ipv6 address: %s", ++ dev4, ++ af_inet6, ++ ) ++ elif "addr" in af_inet6[0]: ++ ipv6 = af_inet6[0]["addr"] ++ ++ # If there is a default IPv6 address but not IPv4, then see if there is a ++ # single IPv4 address associated with the same device associated with the ++ # default IPv6 address. ++ if not ipv4 and ipv6: ++ af_inet4 = addr6_fams.get(netifaces.AF_INET) ++ if af_inet4: ++ if len(af_inet4) > 1: ++ LOG.warning( ++ "device %s has more than one ipv4 address: %s", ++ dev6, ++ af_inet4, ++ ) ++ elif "addr" in af_inet4[0]: ++ ipv4 = af_inet4[0]["addr"] ++ ++ return ipv4, ipv6 ++ ++ ++# patched socket.getfqdn() - see https://bugs.python.org/issue5004 ++ ++ ++def getfqdn(name=""): ++ """Get fully qualified domain name from name. ++ An empty argument is interpreted as meaning the local host. ++ """ ++ # TODO(may want to promote this function to util.getfqdn) ++ # TODO(may want to extend util.get_hostname to accept fqdn=True param) ++ name = name.strip() ++ if not name or name == "0.0.0.0": ++ name = util.get_hostname() ++ try: ++ addrs = socket.getaddrinfo( ++ name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME ++ ) ++ except socket.error: ++ pass ++ else: ++ for addr in addrs: ++ if addr[3]: ++ name = addr[3] ++ break ++ return name ++ ++ ++def is_valid_ip_addr(val): ++ """ ++ Returns false if the address is loopback, link local or unspecified; ++ otherwise true is returned. ++ """ ++ # TODO(extend cloudinit.net.is_ip_addr exclude link_local/loopback etc) ++ # TODO(migrate to use cloudinit.net.is_ip_addr)# ++ ++ addr = None ++ try: ++ addr = ipaddress.ip_address(val) ++ except ipaddress.AddressValueError: ++ addr = ipaddress.ip_address(str(val)) ++ except Exception: ++ return None ++ ++ if addr.is_link_local or addr.is_loopback or addr.is_unspecified: ++ return False ++ return True ++ ++ ++def get_host_info(): ++ """ ++ Returns host information such as the host name and network interfaces. ++ """ ++ # TODO(look to promote netifices use up in cloud-init netinfo funcs) ++ host_info = { ++ "network": { ++ "interfaces": { ++ "by-mac": collections.OrderedDict(), ++ "by-ipv4": collections.OrderedDict(), ++ "by-ipv6": collections.OrderedDict(), ++ }, ++ }, ++ } ++ hostname = getfqdn(util.get_hostname()) ++ if hostname: ++ host_info["hostname"] = hostname ++ host_info["local-hostname"] = hostname ++ host_info["local_hostname"] = hostname ++ ++ default_ipv4, default_ipv6 = get_default_ip_addrs() ++ if default_ipv4: ++ host_info[LOCAL_IPV4] = default_ipv4 ++ if default_ipv6: ++ host_info[LOCAL_IPV6] = default_ipv6 ++ ++ by_mac = host_info["network"]["interfaces"]["by-mac"] ++ by_ipv4 = host_info["network"]["interfaces"]["by-ipv4"] ++ by_ipv6 = host_info["network"]["interfaces"]["by-ipv6"] ++ ++ ifaces = netifaces.interfaces() ++ for dev_name in ifaces: ++ addr_fams = netifaces.ifaddresses(dev_name) ++ af_link = addr_fams.get(netifaces.AF_LINK) ++ af_inet4 = addr_fams.get(netifaces.AF_INET) ++ af_inet6 = addr_fams.get(netifaces.AF_INET6) ++ ++ mac = None ++ if af_link and "addr" in af_link[0]: ++ mac = af_link[0]["addr"] ++ ++ # Do not bother recording localhost ++ if mac == "00:00:00:00:00:00": ++ continue ++ ++ if mac and (af_inet4 or af_inet6): ++ key = mac ++ val = {} ++ if af_inet4: ++ af_inet4_vals = [] ++ for ip_info in af_inet4: ++ if not is_valid_ip_addr(ip_info["addr"]): ++ continue ++ af_inet4_vals.append(ip_info) ++ val["ipv4"] = af_inet4_vals ++ if af_inet6: ++ af_inet6_vals = [] ++ for ip_info in af_inet6: ++ if not is_valid_ip_addr(ip_info["addr"]): ++ continue ++ af_inet6_vals.append(ip_info) ++ val["ipv6"] = af_inet6_vals ++ by_mac[key] = val ++ ++ if af_inet4: ++ for ip_info in af_inet4: ++ key = ip_info["addr"] ++ if not is_valid_ip_addr(key): ++ continue ++ val = copy.deepcopy(ip_info) ++ del val["addr"] ++ if mac: ++ val["mac"] = mac ++ by_ipv4[key] = val ++ ++ if af_inet6: ++ for ip_info in af_inet6: ++ key = ip_info["addr"] ++ if not is_valid_ip_addr(key): ++ continue ++ val = copy.deepcopy(ip_info) ++ del val["addr"] ++ if mac: ++ val["mac"] = mac ++ by_ipv6[key] = val ++ ++ return host_info ++ ++ ++def wait_on_network(metadata): ++ # Determine whether we need to wait on the network coming online. ++ wait_on_ipv4 = False ++ wait_on_ipv6 = False ++ if WAIT_ON_NETWORK in metadata: ++ wait_on_network = metadata[WAIT_ON_NETWORK] ++ if WAIT_ON_NETWORK_IPV4 in wait_on_network: ++ wait_on_ipv4_val = wait_on_network[WAIT_ON_NETWORK_IPV4] ++ if isinstance(wait_on_ipv4_val, bool): ++ wait_on_ipv4 = wait_on_ipv4_val ++ else: ++ wait_on_ipv4 = util.translate_bool(wait_on_ipv4_val) ++ if WAIT_ON_NETWORK_IPV6 in wait_on_network: ++ wait_on_ipv6_val = wait_on_network[WAIT_ON_NETWORK_IPV6] ++ if isinstance(wait_on_ipv6_val, bool): ++ wait_on_ipv6 = wait_on_ipv6_val ++ else: ++ wait_on_ipv6 = util.translate_bool(wait_on_ipv6_val) ++ ++ # Get information about the host. ++ host_info = None ++ while host_info is None: ++ # This loop + sleep results in two logs every second while waiting ++ # for either ipv4 or ipv6 up. Do we really need to log each iteration ++ # or can we log once and log on successful exit? ++ host_info = get_host_info() ++ ++ network = host_info.get("network") or {} ++ interfaces = network.get("interfaces") or {} ++ by_ipv4 = interfaces.get("by-ipv4") or {} ++ by_ipv6 = interfaces.get("by-ipv6") or {} ++ ++ if wait_on_ipv4: ++ ipv4_ready = len(by_ipv4) > 0 if by_ipv4 else False ++ if not ipv4_ready: ++ host_info = None ++ ++ if wait_on_ipv6: ++ ipv6_ready = len(by_ipv6) > 0 if by_ipv6 else False ++ if not ipv6_ready: ++ host_info = None ++ ++ if host_info is None: ++ LOG.debug( ++ "waiting on network: wait4=%s, ready4=%s, wait6=%s, ready6=%s", ++ wait_on_ipv4, ++ ipv4_ready, ++ wait_on_ipv6, ++ ipv6_ready, ++ ) ++ time.sleep(1) ++ ++ LOG.debug("waiting on network complete") ++ return host_info ++ ++ ++def main(): ++ """ ++ Executed when this file is used as a program. ++ """ ++ try: ++ logging.setupBasicLogging() ++ except Exception: ++ pass ++ metadata = { ++ "wait-on-network": {"ipv4": True, "ipv6": "false"}, ++ "network": {"config": {"dhcp": True}}, ++ } ++ host_info = wait_on_network(metadata) ++ metadata = util.mergemanydict([metadata, host_info]) ++ print(util.json_dumps(metadata)) ++ ++ ++if __name__ == "__main__": ++ main() ++ ++# vi: ts=4 expandtab +diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst +index f58b2b38..6606367c 100644 +--- a/doc/rtd/topics/availability.rst ++++ b/doc/rtd/topics/availability.rst +@@ -64,5 +64,6 @@ Additionally, cloud-init is supported on these private clouds: + - LXD + - KVM + - Metal-as-a-Service (MAAS) ++- VMware + + .. vi: textwidth=79 +diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst +index 228173d2..8afed470 100644 +--- a/doc/rtd/topics/datasources.rst ++++ b/doc/rtd/topics/datasources.rst +@@ -49,7 +49,7 @@ The following is a list of documents for each supported datasource: + datasources/smartos.rst + datasources/upcloud.rst + datasources/zstack.rst +- ++ datasources/vmware.rst + + Creation + ======== +diff --git a/doc/rtd/topics/datasources/vmware.rst b/doc/rtd/topics/datasources/vmware.rst +new file mode 100644 +index 00000000..996eb61f +--- /dev/null ++++ b/doc/rtd/topics/datasources/vmware.rst +@@ -0,0 +1,359 @@ ++.. _datasource_vmware: ++ ++VMware ++====== ++ ++This datasource is for use with systems running on a VMware platform such as ++vSphere and currently supports the following data transports: ++ ++ ++* `GuestInfo <https://github.com/vmware/govmomi/blob/master/govc/USAGE.md#vmchange>`_ keys ++ ++Configuration ++------------- ++ ++The configuration method is dependent upon the transport: ++ ++GuestInfo Keys ++^^^^^^^^^^^^^^ ++ ++One method of providing meta, user, and vendor data is by setting the following ++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>`_ : ++ ++.. list-table:: ++ :header-rows: 1 ++ ++ * - Property ++ - Description ++ * - ``guestinfo.metadata`` ++ - A YAML or JSON document containing the cloud-init metadata. ++ * - ``guestinfo.metadata.encoding`` ++ - The encoding type for ``guestinfo.metadata``. ++ * - ``guestinfo.userdata`` ++ - A YAML document containing the cloud-init user data. ++ * - ``guestinfo.userdata.encoding`` ++ - The encoding type for ``guestinfo.userdata``. ++ * - ``guestinfo.vendordata`` ++ - A YAML document containing the cloud-init vendor data. ++ * - ``guestinfo.vendordata.encoding`` ++ - The encoding type for ``guestinfo.vendordata``. ++ ++ ++All ``guestinfo.*.encoding`` values may be set to ``base64`` or ++``gzip+base64``. ++ ++Features ++-------- ++ ++This section reviews several features available in this datasource, regardless ++of how the meta, user, and vendor data was discovered. ++ ++Instance data and lazy networks ++^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ++ ++One of the hallmarks of cloud-init is `its use of instance-data and JINJA ++queries <../instancedata.html#using-instance-data>`_ ++-- the ability to write queries in user and vendor data that reference runtime ++information present in ``/run/cloud-init/instance-data.json``. This works well ++when the metadata provides all of the information up front, such as the network ++configuration. For systems that rely on DHCP, however, this information may not ++be available when the metadata is persisted to disk. ++ ++This datasource ensures that even if the instance is using DHCP to configure ++networking, the same details about the configured network are available in ++``/run/cloud-init/instance-data.json`` as if static networking was used. This ++information collected at runtime is easy to demonstrate by executing the ++datasource on the command line. From the root of this repository, run the ++following command: ++ ++.. code-block:: bash ++ ++ PYTHONPATH="$(pwd)" python3 cloudinit/sources/DataSourceVMware.py ++ ++The above command will result in output similar to the below JSON: ++ ++.. code-block:: json ++ ++ { ++ "hostname": "akutz.localhost", ++ "local-hostname": "akutz.localhost", ++ "local-ipv4": "192.168.0.188", ++ "local_hostname": "akutz.localhost", ++ "network": { ++ "config": { ++ "dhcp": true ++ }, ++ "interfaces": { ++ "by-ipv4": { ++ "172.0.0.2": { ++ "netmask": "255.255.255.255", ++ "peer": "172.0.0.2" ++ }, ++ "192.168.0.188": { ++ "broadcast": "192.168.0.255", ++ "mac": "64:4b:f0:18:9a:21", ++ "netmask": "255.255.255.0" ++ } ++ }, ++ "by-ipv6": { ++ "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2": { ++ "flags": 208, ++ "mac": "64:4b:f0:18:9a:21", ++ "netmask": "ffff:ffff:ffff:ffff::/64" ++ } ++ }, ++ "by-mac": { ++ "64:4b:f0:18:9a:21": { ++ "ipv4": [ ++ { ++ "addr": "192.168.0.188", ++ "broadcast": "192.168.0.255", ++ "netmask": "255.255.255.0" ++ } ++ ], ++ "ipv6": [ ++ { ++ "addr": "fd8e:d25e:c5b6:1:1f5:b2fd:8973:22f2", ++ "flags": 208, ++ "netmask": "ffff:ffff:ffff:ffff::/64" ++ } ++ ] ++ }, ++ "ac:de:48:00:11:22": { ++ "ipv6": [] ++ } ++ } ++ } ++ }, ++ "wait-on-network": { ++ "ipv4": true, ++ "ipv6": "false" ++ } ++ } ++ ++ ++Redacting sensitive information ++^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ++ ++Sometimes the cloud-init userdata might contain sensitive information, and it ++may be desirable to have the ``guestinfo.userdata`` key (or other guestinfo ++keys) redacted as soon as its data is read by the datasource. This is possible ++by adding the following to the metadata: ++ ++.. code-block:: yaml ++ ++ redact: # formerly named cleanup-guestinfo, which will also work ++ - userdata ++ - vendordata ++ ++When the above snippet is added to the metadata, the datasource will iterate ++over the elements in the ``redact`` array and clear each of the keys. For ++example, when the guestinfo transport is used, the above snippet will cause ++the following commands to be executed: ++ ++.. code-block:: shell ++ ++ vmware-rpctool "info-set guestinfo.userdata ---" ++ vmware-rpctool "info-set guestinfo.userdata.encoding " ++ vmware-rpctool "info-set guestinfo.vendordata ---" ++ vmware-rpctool "info-set guestinfo.vendordata.encoding " ++ ++Please note that keys are set to the valid YAML string ``---`` as it is not ++possible remove an existing key from the guestinfo key-space. A key's analogous ++encoding property will be set to a single white-space character, causing the ++datasource to treat the actual key value as plain-text, thereby loading it as ++an empty YAML doc (hence the aforementioned ``---``\ ). ++ ++Reading the local IP addresses ++^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ++ ++This datasource automatically discovers the local IPv4 and IPv6 addresses for ++a guest operating system based on the default routes. However, when inspecting ++a VM externally, it's not possible to know what the *default* IP address is for ++the guest OS. That's why this datasource sets the discovered, local IPv4 and ++IPv6 addresses back in the guestinfo namespace as the following keys: ++ ++ ++* ``guestinfo.local-ipv4`` ++* ``guestinfo.local-ipv6`` ++ ++It is possible that a host may not have any default, local IP addresses. It's ++also possible the reported, local addresses are link-local addresses. But these ++two keys may be used to discover what this datasource determined were the local ++IPv4 and IPv6 addresses for a host. ++ ++Waiting on the network ++^^^^^^^^^^^^^^^^^^^^^^ ++ ++Sometimes cloud-init may bring up the network, but it will not finish coming ++online before the datasource's ``setup`` function is called, resulting in an ++``/var/run/cloud-init/instance-data.json`` file that does not have the correct ++network information. It is possible to instruct the datasource to wait until an ++IPv4 or IPv6 address is available before writing the instance data with the ++following metadata properties: ++ ++.. code-block:: yaml ++ ++ wait-on-network: ++ ipv4: true ++ ipv6: true ++ ++If either of the above values are true, then the datasource will sleep for a ++second, check the network status, and repeat until one or both addresses from ++the specified families are available. ++ ++Walkthrough ++----------- ++ ++The following series of steps is a demonstration on how to configure a VM with ++this datasource: ++ ++ ++#. Create the metadata file for the VM. Save the following YAML to a file named ++ ``metadata.yaml``\ : ++ ++ .. code-block:: yaml ++ ++ instance-id: cloud-vm ++ local-hostname: cloud-vm ++ network: ++ version: 2 ++ ethernets: ++ nics: ++ match: ++ name: ens* ++ dhcp4: yes ++ ++#. Create the userdata file ``userdata.yaml``\ : ++ ++ .. code-block:: yaml ++ ++ #cloud-config ++ ++ users: ++ - default ++ - name: akutz ++ primary_group: akutz ++ sudo: ALL=(ALL) NOPASSWD:ALL ++ groups: sudo, wheel ++ ssh_import_id: None ++ lock_passwd: true ++ ssh_authorized_keys: ++ - 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 ++ ++#. Please note this step requires that the VM be powered off. All of the ++ commands below use the VMware CLI tool, `govc <https://github.com/vmware/govmomi/blob/master/govc>`_. ++ ++ Go ahead and assign the path to the VM to the environment variable ``VM``\ : ++ ++ .. code-block:: shell ++ ++ export VM="/inventory/path/to/the/vm" ++ ++#. Power off the VM: ++ ++ .. raw:: html ++ ++ <hr /> ++ ++ ⚠️ <strong>First Boot Mode</strong> ++ ++ To ensure the next power-on operation results in a first-boot scenario for ++ cloud-init, it may be necessary to run the following command just before ++ powering off the VM: ++ ++ .. code-block:: bash ++ ++ cloud-init clean ++ ++ Otherwise cloud-init may not run in first-boot mode. For more information ++ on how the boot mode is determined, please see the ++ `First Boot Documentation <../boot.html#first-boot-determination>`_. ++ ++ .. raw:: html ++ ++ <hr /> ++ ++ .. code-block:: shell ++ ++ govc vm.power -off "${VM}" ++ ++#. ++ Export the environment variables that contain the cloud-init metadata and ++ userdata: ++ ++ .. code-block:: shell ++ ++ export METADATA=$(gzip -c9 <metadata.yaml | { base64 -w0 2>/dev/null || base64; }) \ ++ USERDATA=$(gzip -c9 <userdata.yaml | { base64 -w0 2>/dev/null || base64; }) ++ ++#. ++ Assign the metadata and userdata to the VM: ++ ++ .. code-block:: shell ++ ++ govc vm.change -vm "${VM}" \ ++ -e guestinfo.metadata="${METADATA}" \ ++ -e guestinfo.metadata.encoding="gzip+base64" \ ++ -e guestinfo.userdata="${USERDATA}" \ ++ -e guestinfo.userdata.encoding="gzip+base64" ++ ++ Please note the above commands include specifying the encoding for the ++ properties. This is important as it informs the datasource how to decode ++ the data for cloud-init. Valid values for ``metadata.encoding`` and ++ ``userdata.encoding`` include: ++ ++ ++ * ``base64`` ++ * ``gzip+base64`` ++ ++#. ++ Power on the VM: ++ ++ .. code-block:: shell ++ ++ govc vm.power -vm "${VM}" -on ++ ++If all went according to plan, the CentOS box is: ++ ++* Locked down, allowing SSH access only for the user in the userdata ++* Configured for a dynamic IP address via DHCP ++* Has a hostname of ``cloud-vm`` ++ ++Examples ++-------- ++ ++This section reviews common configurations: ++ ++Setting the hostname ++^^^^^^^^^^^^^^^^^^^^ ++ ++The hostname is set by way of the metadata key ``local-hostname``. ++ ++Setting the instance ID ++^^^^^^^^^^^^^^^^^^^^^^^ ++ ++The instance ID may be set by way of the metadata key ``instance-id``. However, ++if this value is absent then then the instance ID is read from the file ++``/sys/class/dmi/id/product_uuid``. ++ ++Providing public SSH keys ++^^^^^^^^^^^^^^^^^^^^^^^^^ ++ ++The public SSH keys may be set by way of the metadata key ``public-keys-data``. ++Each newline-terminated string will be interpreted as a separate SSH public ++key, which will be placed in distro's default user's ++``~/.ssh/authorized_keys``. If the value is empty or absent, then nothing will ++be written to ``~/.ssh/authorized_keys``. ++ ++Configuring the network ++^^^^^^^^^^^^^^^^^^^^^^^ ++ ++The network is configured by setting the metadata key ``network`` with a value ++consistent with Network Config Versions ++`1 <../network-config-format-v1.html>`_ or ++`2 <../network-config-format-v2.html>`_\ , depending on the Linux ++distro's version of cloud-init. ++ ++The metadata key ``network.encoding`` may be used to indicate the format of ++the metadata key "network". Valid encodings are ``base64`` and ``gzip+base64``. +diff --git a/requirements.txt b/requirements.txt +index 5b8becd7..41d01d62 100644 +--- a/requirements.txt ++++ b/requirements.txt +@@ -29,3 +29,15 @@ requests + + # For patching pieces of cloud-config together + jsonpatch ++ ++# For validating cloud-config sections per schema definitions ++jsonschema ++ ++# Used by DataSourceVMware to inspect the host's network configuration during ++# the "setup()" function. ++# ++# This allows a host that uses DHCP to bring up the network during BootLocal ++# and still participate in instance-data by gathering the network in detail at ++# runtime and merge that information into the metadata and repersist that to ++# disk. ++netifaces>=0.10.9 +diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py +index 5912f7ee..475a2cf8 100644 +--- a/tests/unittests/test_datasource/test_common.py ++++ b/tests/unittests/test_datasource/test_common.py +@@ -28,6 +28,7 @@ from cloudinit.sources import ( + DataSourceScaleway as Scaleway, + DataSourceSmartOS as SmartOS, + DataSourceUpCloud as UpCloud, ++ DataSourceVMware as VMware, + ) + from cloudinit.sources import DataSourceNone as DSNone + +@@ -50,6 +51,7 @@ DEFAULT_LOCAL = [ + RbxCloud.DataSourceRbxCloud, + Scaleway.DataSourceScaleway, + UpCloud.DataSourceUpCloudLocal, ++ VMware.DataSourceVMware, + ] + + DEFAULT_NETWORK = [ +@@ -66,6 +68,7 @@ DEFAULT_NETWORK = [ + OpenStack.DataSourceOpenStack, + OVF.DataSourceOVFNet, + UpCloud.DataSourceUpCloud, ++ VMware.DataSourceVMware, + ] + + +diff --git a/tests/unittests/test_datasource/test_vmware.py b/tests/unittests/test_datasource/test_vmware.py +new file mode 100644 +index 00000000..597db7c8 +--- /dev/null ++++ b/tests/unittests/test_datasource/test_vmware.py +@@ -0,0 +1,377 @@ ++# Copyright (c) 2021 VMware, Inc. All Rights Reserved. ++# ++# Authors: Andrew Kutz <akutz@vmware.com> ++# ++# This file is part of cloud-init. See LICENSE file for license information. ++ ++import base64 ++import gzip ++from cloudinit import dmi, helpers, safeyaml ++from cloudinit import settings ++from cloudinit.sources import DataSourceVMware ++from cloudinit.tests.helpers import ( ++ mock, ++ CiTestCase, ++ FilesystemMockingTestCase, ++ populate_dir, ++) ++ ++import os ++ ++PRODUCT_NAME_FILE_PATH = "/sys/class/dmi/id/product_name" ++PRODUCT_NAME = "VMware7,1" ++PRODUCT_UUID = "82343CED-E4C7-423B-8F6B-0D34D19067AB" ++REROOT_FILES = { ++ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, ++ PRODUCT_NAME_FILE_PATH: PRODUCT_NAME, ++} ++ ++VMW_MULTIPLE_KEYS = [ ++ "ssh-rsa AAAAB3NzaC1yc2EAAAA... test1@vmw.com", ++ "ssh-rsa AAAAB3NzaC1yc2EAAAA... test2@vmw.com", ++] ++VMW_SINGLE_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAA... test@vmw.com" ++ ++VMW_METADATA_YAML = """instance-id: cloud-vm ++local-hostname: cloud-vm ++network: ++ version: 2 ++ ethernets: ++ nics: ++ match: ++ name: ens* ++ dhcp4: yes ++""" ++ ++VMW_USERDATA_YAML = """## template: jinja ++#cloud-config ++users: ++- default ++""" ++ ++VMW_VENDORDATA_YAML = """## template: jinja ++#cloud-config ++runcmd: ++- echo "Hello, world." ++""" ++ ++ ++class TestDataSourceVMware(CiTestCase): ++ """ ++ Test common functionality that is not transport specific. ++ """ ++ ++ def setUp(self): ++ super(TestDataSourceVMware, self).setUp() ++ self.tmp = self.tmp_dir() ++ ++ def test_no_data_access_method(self): ++ ds = get_ds(self.tmp) ++ ds.vmware_rpctool = None ++ ret = ds.get_data() ++ self.assertFalse(ret) ++ ++ def test_get_host_info(self): ++ host_info = DataSourceVMware.get_host_info() ++ self.assertTrue(host_info) ++ self.assertTrue(host_info["hostname"]) ++ self.assertTrue(host_info["local-hostname"]) ++ self.assertTrue(host_info["local_hostname"]) ++ self.assertTrue(host_info[DataSourceVMware.LOCAL_IPV4]) ++ ++ ++class TestDataSourceVMwareEnvVars(FilesystemMockingTestCase): ++ """ ++ Test the envvar transport. ++ """ ++ ++ def setUp(self): ++ super(TestDataSourceVMwareEnvVars, self).setUp() ++ self.tmp = self.tmp_dir() ++ os.environ[DataSourceVMware.VMX_GUESTINFO] = "1" ++ self.create_system_files() ++ ++ def tearDown(self): ++ del os.environ[DataSourceVMware.VMX_GUESTINFO] ++ return super(TestDataSourceVMwareEnvVars, self).tearDown() ++ ++ def create_system_files(self): ++ rootd = self.tmp_dir() ++ populate_dir( ++ rootd, ++ { ++ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, ++ }, ++ ) ++ self.assertTrue(self.reRoot(rootd)) ++ ++ def assert_get_data_ok(self, m_fn, m_fn_call_count=6): ++ ds = get_ds(self.tmp) ++ ds.vmware_rpctool = None ++ ret = ds.get_data() ++ self.assertTrue(ret) ++ self.assertEqual(m_fn_call_count, m_fn.call_count) ++ self.assertEqual( ++ ds.data_access_method, DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR ++ ) ++ return ds ++ ++ def assert_metadata(self, metadata, m_fn, m_fn_call_count=6): ++ ds = self.assert_get_data_ok(m_fn, m_fn_call_count) ++ assert_metadata(self, ds, metadata) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_subplatform(self, m_fn): ++ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] ++ ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ self.assertEqual( ++ ds.subplatform, ++ "%s (%s)" ++ % ( ++ DataSourceVMware.DATA_ACCESS_METHOD_ENVVAR, ++ DataSourceVMware.get_guestinfo_envvar_key_name("metadata"), ++ ), ++ ) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_data_metadata_only(self, m_fn): ++ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_data_userdata_only(self, m_fn): ++ m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_data_vendordata_only(self, m_fn): ++ m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_data_metadata_base64(self, m_fn): ++ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) ++ m_fn.side_effect = [data, "base64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_data_metadata_b64(self, m_fn): ++ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) ++ m_fn.side_effect = [data, "b64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_data_metadata_gzip_base64(self, m_fn): ++ data = VMW_METADATA_YAML.encode("utf-8") ++ data = gzip.compress(data) ++ data = base64.b64encode(data) ++ m_fn.side_effect = [data, "gzip+base64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_get_data_metadata_gz_b64(self, m_fn): ++ data = VMW_METADATA_YAML.encode("utf-8") ++ data = gzip.compress(data) ++ data = base64.b64encode(data) ++ m_fn.side_effect = [data, "gz+b64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_metadata_single_ssh_key(self, m_fn): ++ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) ++ metadata["public_keys"] = VMW_SINGLE_KEY ++ metadata_yaml = safeyaml.dumps(metadata) ++ m_fn.side_effect = [metadata_yaml, "", "", ""] ++ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) ++ ++ @mock.patch( ++ "cloudinit.sources.DataSourceVMware.guestinfo_envvar_get_value" ++ ) ++ def test_metadata_multiple_ssh_keys(self, m_fn): ++ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) ++ metadata["public_keys"] = VMW_MULTIPLE_KEYS ++ metadata_yaml = safeyaml.dumps(metadata) ++ m_fn.side_effect = [metadata_yaml, "", "", ""] ++ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) ++ ++ ++class TestDataSourceVMwareGuestInfo(FilesystemMockingTestCase): ++ """ ++ Test the guestinfo transport on a VMware platform. ++ """ ++ ++ def setUp(self): ++ super(TestDataSourceVMwareGuestInfo, self).setUp() ++ self.tmp = self.tmp_dir() ++ self.create_system_files() ++ ++ def create_system_files(self): ++ rootd = self.tmp_dir() ++ populate_dir( ++ rootd, ++ { ++ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, ++ PRODUCT_NAME_FILE_PATH: PRODUCT_NAME, ++ }, ++ ) ++ self.assertTrue(self.reRoot(rootd)) ++ ++ def assert_get_data_ok(self, m_fn, m_fn_call_count=6): ++ ds = get_ds(self.tmp) ++ ds.vmware_rpctool = "vmware-rpctool" ++ ret = ds.get_data() ++ self.assertTrue(ret) ++ self.assertEqual(m_fn_call_count, m_fn.call_count) ++ self.assertEqual( ++ ds.data_access_method, ++ DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO, ++ ) ++ return ds ++ ++ def assert_metadata(self, metadata, m_fn, m_fn_call_count=6): ++ ds = self.assert_get_data_ok(m_fn, m_fn_call_count) ++ assert_metadata(self, ds, metadata) ++ ++ def test_ds_valid_on_vmware_platform(self): ++ system_type = dmi.read_dmi_data("system-product-name") ++ self.assertEqual(system_type, PRODUCT_NAME) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_get_subplatform(self, m_fn): ++ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] ++ ds = self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ self.assertEqual( ++ ds.subplatform, ++ "%s (%s)" ++ % ( ++ DataSourceVMware.DATA_ACCESS_METHOD_GUESTINFO, ++ DataSourceVMware.get_guestinfo_key_name("metadata"), ++ ), ++ ) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_get_data_userdata_only(self, m_fn): ++ m_fn.side_effect = ["", VMW_USERDATA_YAML, "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_get_data_vendordata_only(self, m_fn): ++ m_fn.side_effect = ["", "", VMW_VENDORDATA_YAML, ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_metadata_single_ssh_key(self, m_fn): ++ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) ++ metadata["public_keys"] = VMW_SINGLE_KEY ++ metadata_yaml = safeyaml.dumps(metadata) ++ m_fn.side_effect = [metadata_yaml, "", "", ""] ++ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_metadata_multiple_ssh_keys(self, m_fn): ++ metadata = DataSourceVMware.load_json_or_yaml(VMW_METADATA_YAML) ++ metadata["public_keys"] = VMW_MULTIPLE_KEYS ++ metadata_yaml = safeyaml.dumps(metadata) ++ m_fn.side_effect = [metadata_yaml, "", "", ""] ++ self.assert_metadata(metadata, m_fn, m_fn_call_count=4) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_get_data_metadata_base64(self, m_fn): ++ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) ++ m_fn.side_effect = [data, "base64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_get_data_metadata_b64(self, m_fn): ++ data = base64.b64encode(VMW_METADATA_YAML.encode("utf-8")) ++ m_fn.side_effect = [data, "b64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_get_data_metadata_gzip_base64(self, m_fn): ++ data = VMW_METADATA_YAML.encode("utf-8") ++ data = gzip.compress(data) ++ data = base64.b64encode(data) ++ m_fn.side_effect = [data, "gzip+base64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_get_data_metadata_gz_b64(self, m_fn): ++ data = VMW_METADATA_YAML.encode("utf-8") ++ data = gzip.compress(data) ++ data = base64.b64encode(data) ++ m_fn.side_effect = [data, "gz+b64", "", ""] ++ self.assert_get_data_ok(m_fn, m_fn_call_count=4) ++ ++ ++class TestDataSourceVMwareGuestInfo_InvalidPlatform(FilesystemMockingTestCase): ++ """ ++ Test the guestinfo transport on a non-VMware platform. ++ """ ++ ++ def setUp(self): ++ super(TestDataSourceVMwareGuestInfo_InvalidPlatform, self).setUp() ++ self.tmp = self.tmp_dir() ++ self.create_system_files() ++ ++ def create_system_files(self): ++ rootd = self.tmp_dir() ++ populate_dir( ++ rootd, ++ { ++ DataSourceVMware.PRODUCT_UUID_FILE_PATH: PRODUCT_UUID, ++ }, ++ ) ++ self.assertTrue(self.reRoot(rootd)) ++ ++ @mock.patch("cloudinit.sources.DataSourceVMware.guestinfo_get_value") ++ def test_ds_invalid_on_non_vmware_platform(self, m_fn): ++ system_type = dmi.read_dmi_data("system-product-name") ++ self.assertEqual(system_type, None) ++ ++ m_fn.side_effect = [VMW_METADATA_YAML, "", "", "", "", ""] ++ ds = get_ds(self.tmp) ++ ds.vmware_rpctool = "vmware-rpctool" ++ ret = ds.get_data() ++ self.assertFalse(ret) ++ ++ ++def assert_metadata(test_obj, ds, metadata): ++ test_obj.assertEqual(metadata.get("instance-id"), ds.get_instance_id()) ++ test_obj.assertEqual(metadata.get("local-hostname"), ds.get_hostname()) ++ ++ expected_public_keys = metadata.get("public_keys") ++ if not isinstance(expected_public_keys, list): ++ expected_public_keys = [expected_public_keys] ++ ++ test_obj.assertEqual(expected_public_keys, ds.get_public_ssh_keys()) ++ test_obj.assertIsInstance(ds.get_public_ssh_keys(), list) ++ ++ ++def get_ds(temp_dir): ++ ds = DataSourceVMware.DataSourceVMware( ++ settings.CFG_BUILTIN, None, helpers.Paths({"run_dir": temp_dir}) ++ ) ++ ds.vmware_rpctool = "vmware-rpctool" ++ return ds ++ ++ ++# vi: ts=4 expandtab +diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py +index 1d8aaf18..8617d7bd 100644 +--- a/tests/unittests/test_ds_identify.py ++++ b/tests/unittests/test_ds_identify.py +@@ -649,6 +649,50 @@ class TestDsIdentify(DsIdentifyBase): + """EC2: bobrightbox.com in product_serial is not brightbox'""" + self._test_ds_not_found('Ec2-E24Cloud-negative') + ++ def test_vmware_no_valid_transports(self): ++ """VMware: no valid transports""" ++ self._test_ds_not_found('VMware-NoValidTransports') ++ ++ def test_vmware_envvar_no_data(self): ++ """VMware: envvar transport no data""" ++ self._test_ds_not_found('VMware-EnvVar-NoData') ++ ++ def test_vmware_envvar_no_virt_id(self): ++ """VMware: envvar transport success if no virt id""" ++ self._test_ds_found('VMware-EnvVar-NoVirtID') ++ ++ def test_vmware_envvar_activated_by_metadata(self): ++ """VMware: envvar transport activated by metadata""" ++ self._test_ds_found('VMware-EnvVar-Metadata') ++ ++ def test_vmware_envvar_activated_by_userdata(self): ++ """VMware: envvar transport activated by userdata""" ++ self._test_ds_found('VMware-EnvVar-Userdata') ++ ++ def test_vmware_envvar_activated_by_vendordata(self): ++ """VMware: envvar transport activated by vendordata""" ++ self._test_ds_found('VMware-EnvVar-Vendordata') ++ ++ def test_vmware_guestinfo_no_data(self): ++ """VMware: guestinfo transport no data""" ++ self._test_ds_not_found('VMware-GuestInfo-NoData') ++ ++ def test_vmware_guestinfo_no_virt_id(self): ++ """VMware: guestinfo transport fails if no virt id""" ++ self._test_ds_not_found('VMware-GuestInfo-NoVirtID') ++ ++ def test_vmware_guestinfo_activated_by_metadata(self): ++ """VMware: guestinfo transport activated by metadata""" ++ self._test_ds_found('VMware-GuestInfo-Metadata') ++ ++ def test_vmware_guestinfo_activated_by_userdata(self): ++ """VMware: guestinfo transport activated by userdata""" ++ self._test_ds_found('VMware-GuestInfo-Userdata') ++ ++ def test_vmware_guestinfo_activated_by_vendordata(self): ++ """VMware: guestinfo transport activated by vendordata""" ++ self._test_ds_found('VMware-GuestInfo-Vendordata') ++ + + class TestBSDNoSys(DsIdentifyBase): + """Test *BSD code paths +@@ -1136,7 +1180,240 @@ VALID_CFG = { + 'Ec2-E24Cloud-negative': { + 'ds': 'Ec2', + 'files': {P_SYS_VENDOR: 'e24cloudyday\n'}, +- } ++ }, ++ 'VMware-NoValidTransports': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-EnvVar-NoData': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-EnvVar-NoVirtID': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ ], ++ }, ++ 'VMware-EnvVar-Metadata': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-EnvVar-Userdata': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-EnvVar-Vendordata': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo', ++ 'ret': 0, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_metadata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_has_envvar_vmx_guestinfo_vendordata', ++ 'ret': 0, ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-GuestInfo-NoData': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_rpctool', ++ 'ret': 0, ++ 'out': '/usr/bin/vmware-rpctool', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_metadata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-GuestInfo-NoVirtID': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_rpctool', ++ 'ret': 0, ++ 'out': '/usr/bin/vmware-rpctool', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_metadata', ++ 'ret': 0, ++ 'out': '---', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ ], ++ }, ++ 'VMware-GuestInfo-Metadata': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_rpctool', ++ 'ret': 0, ++ 'out': '/usr/bin/vmware-rpctool', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_metadata', ++ 'ret': 0, ++ 'out': '---', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-GuestInfo-Userdata': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_rpctool', ++ 'ret': 0, ++ 'out': '/usr/bin/vmware-rpctool', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_metadata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_userdata', ++ 'ret': 0, ++ 'out': '---', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_vendordata', ++ 'ret': 1, ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, ++ 'VMware-GuestInfo-Vendordata': { ++ 'ds': 'VMware', ++ 'mocks': [ ++ { ++ 'name': 'vmware_has_rpctool', ++ 'ret': 0, ++ 'out': '/usr/bin/vmware-rpctool', ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_metadata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_userdata', ++ 'ret': 1, ++ }, ++ { ++ 'name': 'vmware_rpctool_guestinfo_vendordata', ++ 'ret': 0, ++ 'out': '---', ++ }, ++ MOCK_VIRT_IS_VMWARE, ++ ], ++ }, + } + + # vi: ts=4 expandtab +diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers +index 689d7902..cbfa883c 100644 +--- a/tools/.github-cla-signers ++++ b/tools/.github-cla-signers +@@ -1,5 +1,6 @@ + ader1990 + ajmyyra ++akutz + AlexBaranowski + Aman306 + andrewbogott +diff --git a/tools/ds-identify b/tools/ds-identify +index 2f2486f7..c01eae3d 100755 +--- a/tools/ds-identify ++++ b/tools/ds-identify +@@ -125,7 +125,7 @@ DI_DSNAME="" + # be searched if there is no setting found in config. + DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ + CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ +-OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud" ++OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud VMware" + DI_DSLIST="" + DI_MODE="" + DI_ON_FOUND="" +@@ -1350,6 +1350,80 @@ dscheck_IBMCloud() { + return ${DS_NOT_FOUND} + } + ++vmware_has_envvar_vmx_guestinfo() { ++ [ -n "${VMX_GUESTINFO:-}" ] ++} ++ ++vmware_has_envvar_vmx_guestinfo_metadata() { ++ [ -n "${VMX_GUESTINFO_METADATA:-}" ] ++} ++ ++vmware_has_envvar_vmx_guestinfo_userdata() { ++ [ -n "${VMX_GUESTINFO_USERDATA:-}" ] ++} ++ ++vmware_has_envvar_vmx_guestinfo_vendordata() { ++ [ -n "${VMX_GUESTINFO_VENDORDATA:-}" ] ++} ++ ++vmware_has_rpctool() { ++ command -v vmware-rpctool >/dev/null 2>&1 ++} ++ ++vmware_rpctool_guestinfo_metadata() { ++ vmware-rpctool "info-get guestinfo.metadata" ++} ++ ++vmware_rpctool_guestinfo_userdata() { ++ vmware-rpctool "info-get guestinfo.userdata" ++} ++ ++vmware_rpctool_guestinfo_vendordata() { ++ vmware-rpctool "info-get guestinfo.vendordata" ++} ++ ++dscheck_VMware() { ++ # Checks to see if there is valid data for the VMware datasource. ++ # The data transports are checked in the following order: ++ # ++ # * envvars ++ # * guestinfo ++ # ++ # Please note when updating this function with support for new data ++ # transports, the order should match the order in the _get_data ++ # function from the file DataSourceVMware.py. ++ ++ # Check to see if running in a container and the VMware ++ # datasource is configured via environment variables. ++ if vmware_has_envvar_vmx_guestinfo; then ++ if vmware_has_envvar_vmx_guestinfo_metadata || \ ++ vmware_has_envvar_vmx_guestinfo_userdata || \ ++ vmware_has_envvar_vmx_guestinfo_vendordata; then ++ return "${DS_FOUND}" ++ fi ++ fi ++ ++ # Do not proceed unless the detected platform is VMware. ++ if [ ! "${DI_VIRT}" = "vmware" ]; then ++ return "${DS_NOT_FOUND}" ++ fi ++ ++ # Do not proceed if the vmware-rpctool command is not present. ++ if ! vmware_has_rpctool; then ++ return "${DS_NOT_FOUND}" ++ fi ++ ++ # Activate the VMware datasource only if any of the fields used ++ # by the datasource are present in the guestinfo table. ++ if { vmware_rpctool_guestinfo_metadata || \ ++ vmware_rpctool_guestinfo_userdata || \ ++ vmware_rpctool_guestinfo_vendordata; } >/dev/null 2>&1; then ++ return "${DS_FOUND}" ++ fi ++ ++ return "${DS_NOT_FOUND}" ++} ++ + collect_info() { + read_uname_info + read_virt +-- +2.27.0 + diff --git a/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch b/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch new file mode 100644 index 0000000..8efdfa4 --- /dev/null +++ b/SOURCES/ci-Revert-unnecesary-lcase-in-ds-identify-978.patch @@ -0,0 +1,47 @@ +From 0aba80bf749458960945acf106833b098c3c5c97 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Fri, 14 Jan 2022 16:50:44 +0100 +Subject: [PATCH 4/5] Revert unnecesary lcase in ds-identify (#978) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 17: Datasource for VMware +RH-Commit: [4/5] 334aae223b966173238a905150cf7bc07829c255 (eesposit/cloud-init-centos-) +RH-Bugzilla: 2040090 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> + +commit f516a7d37c1654addc02485e681b4358d7e7c0db +Author: Andrew Kutz <101085+akutz@users.noreply.github.com> +Date: Fri Aug 13 14:30:55 2021 -0500 + + Revert unnecesary lcase in ds-identify (#978) + + This patch reverts an unnecessary lcase optimization in the + ds-identify script. SystemD documents the values produced by + the systemd-detect-virt command are lower case, and the mapping + table used by the FreeBSD check is also lower-case. + + The optimization added two new forked processes, needlessly + causing overhead. + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + tools/ds-identify | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tools/ds-identify b/tools/ds-identify +index 0e12298f..7b782462 100755 +--- a/tools/ds-identify ++++ b/tools/ds-identify +@@ -449,7 +449,7 @@ detect_virt() { + read_virt() { + cached "$DI_VIRT" && return 0 + detect_virt +- DI_VIRT="$(echo "${_RET}" | tr '[:upper:]' '[:lower:]')" ++ DI_VIRT="${_RET}" + } + + is_container() { +-- +2.27.0 + diff --git a/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch b/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch new file mode 100644 index 0000000..81deba6 --- /dev/null +++ b/SOURCES/ci-Update-dscheck_VMware-s-rpctool-check-970.patch @@ -0,0 +1,97 @@ +From f284c2925b7076b81afb9207161f01718ba70951 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito <eesposit@redhat.com> +Date: Fri, 14 Jan 2022 16:50:18 +0100 +Subject: [PATCH 3/5] Update dscheck_VMware's rpctool check (#970) + +RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com> +RH-MergeRequest: 17: Datasource for VMware +RH-Commit: [3/5] 0739bc18b46b8877fb3825d13f7cda57acda2dde (eesposit/cloud-init-centos-) +RH-Bugzilla: 2040090 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Eduardo Otubo <otubo@redhat.com> + +commit 7781dec3306e9467f216cfcb36b7e10a8b38547a +Author: Shreenidhi Shedi <53473811+sshedi@users.noreply.github.com> +Date: Fri Aug 13 00:40:39 2021 +0530 + + Update dscheck_VMware's rpctool check (#970) + + This patch updates the dscheck_VMware function's use of "vmware-rpctool". + + When checking to see if a "guestinfo" property is set. + Because a successful exit code can occur even if there is an empty + string returned, it is possible that the VMware datasource will be + loaded as a false-positive. This patch ensures that in addition to + validating the exit code, the emitted output is also examined to ensure + a non-empty value is returned by rpctool before returning "${DS_FOUND}" + from "dscheck_VMware()". + +Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> +--- + tools/ds-identify | 15 +++++++++------ + 1 file changed, 9 insertions(+), 6 deletions(-) + +diff --git a/tools/ds-identify b/tools/ds-identify +index c01eae3d..0e12298f 100755 +--- a/tools/ds-identify ++++ b/tools/ds-identify +@@ -141,6 +141,7 @@ error() { + debug 0 "$@" + stderr "$@" + } ++ + warn() { + set -- "WARN:" "$@" + debug 0 "$@" +@@ -344,7 +345,6 @@ geom_label_status_as() { + return $ret + } + +- + read_fs_info_freebsd() { + local oifs="$IFS" line="" delim="," + local ret=0 labels="" dev="" label="" ftype="" isodevs="" +@@ -404,7 +404,6 @@ cached() { + [ -n "$1" ] && _RET="$1" && return || return 1 + } + +- + detect_virt() { + local virt="${UNAVAILABLE}" r="" out="" + if [ -d /run/systemd ]; then +@@ -450,7 +449,7 @@ detect_virt() { + read_virt() { + cached "$DI_VIRT" && return 0 + detect_virt +- DI_VIRT=${_RET} ++ DI_VIRT="$(echo "${_RET}" | tr '[:upper:]' '[:lower:]')" + } + + is_container() { +@@ -1370,16 +1369,20 @@ vmware_has_rpctool() { + command -v vmware-rpctool >/dev/null 2>&1 + } + ++vmware_rpctool_guestinfo() { ++ vmware-rpctool "info-get guestinfo.${1}" 2>/dev/null | grep "[[:alnum:]]" ++} ++ + vmware_rpctool_guestinfo_metadata() { +- vmware-rpctool "info-get guestinfo.metadata" ++ vmware_rpctool_guestinfo "metadata" + } + + vmware_rpctool_guestinfo_userdata() { +- vmware-rpctool "info-get guestinfo.userdata" ++ vmware_rpctool_guestinfo "userdata" + } + + vmware_rpctool_guestinfo_vendordata() { +- vmware-rpctool "info-get guestinfo.vendordata" ++ vmware_rpctool_guestinfo "vendordata" + } + + dscheck_VMware() { +-- +2.27.0 + diff --git a/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch b/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch new file mode 100644 index 0000000..17b2187 --- /dev/null +++ b/SOURCES/ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch @@ -0,0 +1,470 @@ +From 9ccb738cf078555b68122b1fc745a45fe952c439 Mon Sep 17 00:00:00 2001 +From: Anh Vo <anhvo@microsoft.com> +Date: Tue, 13 Apr 2021 17:39:39 -0400 +Subject: [PATCH 3/7] azure: Removing ability to invoke walinuxagent (#799) + +RH-Author: Eduardo Otubo <otubo@redhat.com> +RH-MergeRequest: 18: Add support for userdata on Azure from IMDS +RH-Commit: [3/7] 7431b912e3df7ea384820f45e0230b47ab54643c (otubo/cloud-init-src) +RH-Bugzilla: 2042351 +RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com> +RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com> + +Invoking walinuxagent from within cloud-init is no longer +supported/necessary +--- + cloudinit/sources/DataSourceAzure.py | 137 ++++-------------- + doc/rtd/topics/datasources/azure.rst | 62 ++------ + tests/unittests/test_datasource/test_azure.py | 97 ------------- + 3 files changed, 35 insertions(+), 261 deletions(-) + +diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py +index de1452ce..020b7006 100755 +--- a/cloudinit/sources/DataSourceAzure.py ++++ b/cloudinit/sources/DataSourceAzure.py +@@ -381,53 +381,6 @@ class DataSourceAzure(sources.DataSource): + util.logexc(LOG, "handling set_hostname failed") + return False + +- @azure_ds_telemetry_reporter +- def get_metadata_from_agent(self): +- temp_hostname = self.metadata.get('local-hostname') +- agent_cmd = self.ds_cfg['agent_command'] +- LOG.debug("Getting metadata via agent. hostname=%s cmd=%s", +- temp_hostname, agent_cmd) +- +- self.bounce_network_with_azure_hostname() +- +- try: +- invoke_agent(agent_cmd) +- except subp.ProcessExecutionError: +- # claim the datasource even if the command failed +- util.logexc(LOG, "agent command '%s' failed.", +- self.ds_cfg['agent_command']) +- +- ddir = self.ds_cfg['data_dir'] +- +- fp_files = [] +- key_value = None +- for pk in self.cfg.get('_pubkeys', []): +- if pk.get('value', None): +- key_value = pk['value'] +- LOG.debug("SSH authentication: using value from fabric") +- else: +- bname = str(pk['fingerprint'] + ".crt") +- fp_files += [os.path.join(ddir, bname)] +- LOG.debug("SSH authentication: " +- "using fingerprint from fabric") +- +- with events.ReportEventStack( +- name="waiting-for-ssh-public-key", +- description="wait for agents to retrieve SSH keys", +- parent=azure_ds_reporter): +- # wait very long for public SSH keys to arrive +- # https://bugs.launchpad.net/cloud-init/+bug/1717611 +- missing = util.log_time(logfunc=LOG.debug, +- msg="waiting for SSH public key files", +- func=util.wait_for_files, +- args=(fp_files, 900)) +- if len(missing): +- LOG.warning("Did not find files, but going on: %s", missing) +- +- metadata = {} +- metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files) +- return metadata +- + def _get_subplatform(self): + """Return the subplatform metadata source details.""" + if self.seed.startswith('/dev'): +@@ -1354,35 +1307,32 @@ class DataSourceAzure(sources.DataSource): + On failure, returns False. + """ + +- if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN: +- self.bounce_network_with_azure_hostname() ++ self.bounce_network_with_azure_hostname() + +- pubkey_info = None +- try: +- raise KeyError( +- "Not using public SSH keys from IMDS" +- ) +- # pylint:disable=unreachable +- public_keys = self.metadata['imds']['compute']['publicKeys'] +- LOG.debug( +- 'Successfully retrieved %s key(s) from IMDS', +- len(public_keys) +- if public_keys is not None +- else 0 +- ) +- except KeyError: +- LOG.debug( +- 'Unable to retrieve SSH keys from IMDS during ' +- 'negotiation, falling back to OVF' +- ) +- pubkey_info = self.cfg.get('_pubkeys', None) +- +- metadata_func = partial(get_metadata_from_fabric, +- fallback_lease_file=self. +- dhclient_lease_file, +- pubkey_info=pubkey_info) +- else: +- metadata_func = self.get_metadata_from_agent ++ pubkey_info = None ++ try: ++ raise KeyError( ++ "Not using public SSH keys from IMDS" ++ ) ++ # pylint:disable=unreachable ++ public_keys = self.metadata['imds']['compute']['publicKeys'] ++ LOG.debug( ++ 'Successfully retrieved %s key(s) from IMDS', ++ len(public_keys) ++ if public_keys is not None ++ else 0 ++ ) ++ except KeyError: ++ LOG.debug( ++ 'Unable to retrieve SSH keys from IMDS during ' ++ 'negotiation, falling back to OVF' ++ ) ++ pubkey_info = self.cfg.get('_pubkeys', None) ++ ++ metadata_func = partial(get_metadata_from_fabric, ++ fallback_lease_file=self. ++ dhclient_lease_file, ++ pubkey_info=pubkey_info) + + LOG.debug("negotiating with fabric via agent command %s", + self.ds_cfg['agent_command']) +@@ -1617,33 +1567,6 @@ def perform_hostname_bounce(hostname, cfg, prev_hostname): + return True + + +-@azure_ds_telemetry_reporter +-def crtfile_to_pubkey(fname, data=None): +- pipeline = ('openssl x509 -noout -pubkey < "$0" |' +- 'ssh-keygen -i -m PKCS8 -f /dev/stdin') +- (out, _err) = subp.subp(['sh', '-c', pipeline, fname], +- capture=True, data=data) +- return out.rstrip() +- +- +-@azure_ds_telemetry_reporter +-def pubkeys_from_crt_files(flist): +- pubkeys = [] +- errors = [] +- for fname in flist: +- try: +- pubkeys.append(crtfile_to_pubkey(fname)) +- except subp.ProcessExecutionError: +- errors.append(fname) +- +- if errors: +- report_diagnostic_event( +- "failed to convert the crt files to pubkey: %s" % errors, +- logger_func=LOG.warning) +- +- return pubkeys +- +- + @azure_ds_telemetry_reporter + def write_files(datadir, files, dirmode=None): + +@@ -1672,16 +1595,6 @@ def write_files(datadir, files, dirmode=None): + util.write_file(filename=fname, content=content, mode=0o600) + + +-@azure_ds_telemetry_reporter +-def invoke_agent(cmd): +- # this is a function itself to simplify patching it for test +- if cmd: +- LOG.debug("invoking agent: %s", cmd) +- subp.subp(cmd, shell=(not isinstance(cmd, list))) +- else: +- LOG.debug("not invoking agent") +- +- + def find_child(node, filter_func): + ret = [] + if not node.hasChildNodes(): +diff --git a/doc/rtd/topics/datasources/azure.rst b/doc/rtd/topics/datasources/azure.rst +index e04c3a33..ad9f2236 100644 +--- a/doc/rtd/topics/datasources/azure.rst ++++ b/doc/rtd/topics/datasources/azure.rst +@@ -5,28 +5,6 @@ Azure + + This datasource finds metadata and user-data from the Azure cloud platform. + +-walinuxagent +------------- +-walinuxagent has several functions within images. For cloud-init +-specifically, the relevant functionality it performs is to register the +-instance with the Azure cloud platform at boot so networking will be +-permitted. For more information about the other functionality of +-walinuxagent, see `Azure's documentation +-<https://github.com/Azure/WALinuxAgent#introduction>`_ for more details. +-(Note, however, that only one of walinuxagent's provisioning and cloud-init +-should be used to perform instance customisation.) +- +-If you are configuring walinuxagent yourself, you will want to ensure that you +-have `Provisioning.UseCloudInit +-<https://github.com/Azure/WALinuxAgent#provisioningusecloudinit>`_ set to +-``y``. +- +- +-Builtin Agent +-------------- +-An alternative to using walinuxagent to register to the Azure cloud platform +-is to use the ``__builtin__`` agent command. This section contains more +-background on what that code path does, and how to enable it. + + The Azure cloud platform provides initial data to an instance via an attached + CD formatted in UDF. That CD contains a 'ovf-env.xml' file that provides some +@@ -41,16 +19,6 @@ by calling a script in /etc/dhcp/dhclient-exit-hooks or a file in + 'dhclient_hook' of cloud-init itself. This sub-command will write the client + information in json format to /run/cloud-init/dhclient.hook/<interface>.json. + +-In order for cloud-init to leverage this method to find the endpoint, the +-cloud.cfg file must contain: +- +-.. sourcecode:: yaml +- +- datasource: +- Azure: +- set_hostname: False +- agent_command: __builtin__ +- + If those files are not available, the fallback is to check the leases file + for the endpoint server (again option 245). + +@@ -83,9 +51,6 @@ configuration (in ``/etc/cloud/cloud.cfg`` or ``/etc/cloud/cloud.cfg.d/``). + + The settings that may be configured are: + +- * **agent_command**: Either __builtin__ (default) or a command to run to getcw +- metadata. If __builtin__, get metadata from walinuxagent. Otherwise run the +- provided command to obtain metadata. + * **apply_network_config**: Boolean set to True to use network configuration + described by Azure's IMDS endpoint instead of fallback network config of + dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is +@@ -121,7 +86,6 @@ An example configuration with the default values is provided below: + + datasource: + Azure: +- agent_command: __builtin__ + apply_network_config: true + data_dir: /var/lib/waagent + dhclient_lease_file: /var/lib/dhcp/dhclient.eth0.leases +@@ -144,9 +108,7 @@ child of the ``LinuxProvisioningConfigurationSet`` (a sibling to ``UserName``) + If both ``UserData`` and ``CustomData`` are provided behavior is undefined on + which will be selected. + +-In the example below, user-data provided is 'this is my userdata', and the +-datasource config provided is ``{"agent_command": ["start", "walinuxagent"]}``. +-That agent command will take affect as if it were specified in system config. ++In the example below, user-data provided is 'this is my userdata' + + Example: + +@@ -184,20 +146,16 @@ The hostname is provided to the instance in the ovf-env.xml file as + Whatever value the instance provides in its dhcp request will resolve in the + domain returned in the 'search' request. + +-The interesting issue is that a generic image will already have a hostname +-configured. The ubuntu cloud images have 'ubuntu' as the hostname of the +-system, and the initial dhcp request on eth0 is not guaranteed to occur after +-the datasource code has been run. So, on first boot, that initial value will +-be sent in the dhcp request and *that* value will resolve. +- +-In order to make the ``HostName`` provided in the ovf-env.xml resolve, a +-dhcp request must be made with the new value. Walinuxagent (in its current +-version) handles this by polling the state of hostname and bouncing ('``ifdown +-eth0; ifup eth0``' the network interface if it sees that a change has been +-made. ++A generic image will already have a hostname configured. The ubuntu ++cloud images have 'ubuntu' as the hostname of the system, and the ++initial dhcp request on eth0 is not guaranteed to occur after the ++datasource code has been run. So, on first boot, that initial value ++will be sent in the dhcp request and *that* value will resolve. + +-cloud-init handles this by setting the hostname in the DataSource's 'get_data' +-method via '``hostname $HostName``', and then bouncing the interface. This ++In order to make the ``HostName`` provided in the ovf-env.xml resolve, ++a dhcp request must be made with the new value. cloud-init handles ++this by setting the hostname in the DataSource's 'get_data' method via ++'``hostname $HostName``', and then bouncing the interface. This + behavior can be configured or disabled in the datasource config. See + 'Configuration' above. + +diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py +index dedebeb1..320fa857 100644 +--- a/tests/unittests/test_datasource/test_azure.py ++++ b/tests/unittests/test_datasource/test_azure.py +@@ -638,17 +638,10 @@ scbus-1 on xpt0 bus 0 + def dsdevs(): + return data.get('dsdevs', []) + +- def _invoke_agent(cmd): +- data['agent_invoked'] = cmd +- + def _wait_for_files(flist, _maxwait=None, _naplen=None): + data['waited'] = flist + return [] + +- def _pubkeys_from_crt_files(flist): +- data['pubkey_files'] = flist +- return ["pubkey_from: %s" % f for f in flist] +- + if data.get('ovfcontent') is not None: + populate_dir(os.path.join(self.paths.seed_dir, "azure"), + {'ovf-env.xml': data['ovfcontent']}) +@@ -675,8 +668,6 @@ scbus-1 on xpt0 bus 0 + + self.apply_patches([ + (dsaz, 'list_possible_azure_ds_devs', dsdevs), +- (dsaz, 'invoke_agent', _invoke_agent), +- (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files), + (dsaz, 'perform_hostname_bounce', mock.MagicMock()), + (dsaz, 'get_hostname', mock.MagicMock()), + (dsaz, 'set_hostname', mock.MagicMock()), +@@ -765,7 +756,6 @@ scbus-1 on xpt0 bus 0 + ret = dsrc.get_data() + self.m_is_platform_viable.assert_called_with(dsrc.seed_dir) + self.assertFalse(ret) +- self.assertNotIn('agent_invoked', data) + # Assert that for non viable platforms, + # there is no communication with the Azure datasource. + self.assertEqual( +@@ -789,7 +779,6 @@ scbus-1 on xpt0 bus 0 + ret = dsrc.get_data() + self.m_is_platform_viable.assert_called_with(dsrc.seed_dir) + self.assertFalse(ret) +- self.assertNotIn('agent_invoked', data) + self.assertEqual( + 1, + m_report_failure.call_count) +@@ -806,7 +795,6 @@ scbus-1 on xpt0 bus 0 + 1, + m_crawl_metadata.call_count) + self.assertFalse(ret) +- self.assertNotIn('agent_invoked', data) + + def test_crawl_metadata_exception_should_report_failure_with_msg(self): + data = {} +@@ -1086,21 +1074,6 @@ scbus-1 on xpt0 bus 0 + self.assertTrue(os.path.isdir(self.waagent_d)) + self.assertEqual(stat.S_IMODE(os.stat(self.waagent_d).st_mode), 0o700) + +- def test_user_cfg_set_agent_command_plain(self): +- # set dscfg in via plaintext +- # we must have friendly-to-xml formatted plaintext in yaml_cfg +- # not all plaintext is expected to work. +- yaml_cfg = "{agent_command: my_command}\n" +- cfg = yaml.safe_load(yaml_cfg) +- odata = {'HostName': "myhost", 'UserName': "myuser", +- 'dscfg': {'text': yaml_cfg, 'encoding': 'plain'}} +- data = {'ovfcontent': construct_valid_ovf_env(data=odata)} +- +- dsrc = self._get_ds(data) +- ret = self._get_and_setup(dsrc) +- self.assertTrue(ret) +- self.assertEqual(data['agent_invoked'], cfg['agent_command']) +- + @mock.patch('cloudinit.sources.DataSourceAzure.device_driver', + return_value=None) + def test_network_config_set_from_imds(self, m_driver): +@@ -1205,29 +1178,6 @@ scbus-1 on xpt0 bus 0 + dsrc.get_data() + self.assertEqual('eastus2', dsrc.region) + +- def test_user_cfg_set_agent_command(self): +- # set dscfg in via base64 encoded yaml +- cfg = {'agent_command': "my_command"} +- odata = {'HostName': "myhost", 'UserName': "myuser", +- 'dscfg': {'text': b64e(yaml.dump(cfg)), +- 'encoding': 'base64'}} +- data = {'ovfcontent': construct_valid_ovf_env(data=odata)} +- +- dsrc = self._get_ds(data) +- ret = self._get_and_setup(dsrc) +- self.assertTrue(ret) +- self.assertEqual(data['agent_invoked'], cfg['agent_command']) +- +- def test_sys_cfg_set_agent_command(self): +- sys_cfg = {'datasource': {'Azure': {'agent_command': '_COMMAND'}}} +- data = {'ovfcontent': construct_valid_ovf_env(data={}), +- 'sys_cfg': sys_cfg} +- +- dsrc = self._get_ds(data) +- ret = self._get_and_setup(dsrc) +- self.assertTrue(ret) +- self.assertEqual(data['agent_invoked'], '_COMMAND') +- + def test_sys_cfg_set_never_destroy_ntfs(self): + sys_cfg = {'datasource': {'Azure': { + 'never_destroy_ntfs': 'user-supplied-value'}}} +@@ -1311,51 +1261,6 @@ scbus-1 on xpt0 bus 0 + self.assertTrue(ret) + self.assertEqual(dsrc.userdata_raw, mydata.encode('utf-8')) + +- def test_cfg_has_pubkeys_fingerprint(self): +- odata = {'HostName': "myhost", 'UserName': "myuser"} +- mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': ''}] +- pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist] +- data = {'ovfcontent': construct_valid_ovf_env(data=odata, +- pubkeys=pubkeys)} +- +- dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) +- ret = self._get_and_setup(dsrc) +- self.assertTrue(ret) +- for mypk in mypklist: +- self.assertIn(mypk, dsrc.cfg['_pubkeys']) +- self.assertIn('pubkey_from', dsrc.metadata['public-keys'][-1]) +- +- def test_cfg_has_pubkeys_value(self): +- # make sure that provided key is used over fingerprint +- odata = {'HostName': "myhost", 'UserName': "myuser"} +- mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': 'value1'}] +- pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist] +- data = {'ovfcontent': construct_valid_ovf_env(data=odata, +- pubkeys=pubkeys)} +- +- dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) +- ret = self._get_and_setup(dsrc) +- self.assertTrue(ret) +- +- for mypk in mypklist: +- self.assertIn(mypk, dsrc.cfg['_pubkeys']) +- self.assertIn(mypk['value'], dsrc.metadata['public-keys']) +- +- def test_cfg_has_no_fingerprint_has_value(self): +- # test value is used when fingerprint not provided +- odata = {'HostName': "myhost", 'UserName': "myuser"} +- mypklist = [{'fingerprint': None, 'path': 'path1', 'value': 'value1'}] +- pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist] +- data = {'ovfcontent': construct_valid_ovf_env(data=odata, +- pubkeys=pubkeys)} +- +- dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) +- ret = self._get_and_setup(dsrc) +- self.assertTrue(ret) +- +- for mypk in mypklist: +- self.assertIn(mypk['value'], dsrc.metadata['public-keys']) +- + def test_default_ephemeral_configs_ephemeral_exists(self): + # make sure the ephemeral configs are correct if disk present + odata = {} +@@ -1919,8 +1824,6 @@ class TestAzureBounce(CiTestCase): + with_logs = True + + def mock_out_azure_moving_parts(self): +- self.patches.enter_context( +- mock.patch.object(dsaz, 'invoke_agent')) + self.patches.enter_context( + mock.patch.object(dsaz.util, 'wait_for_files')) + self.patches.enter_context( +-- +2.27.0 + diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index c2d87d9..c16a07c 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -1,6 +1,6 @@ Name: cloud-init Version: 21.1 -Release: 15%{?dist} +Release: 17%{?dist} Summary: Cloud instance init scripts License: ASL 2.0 or GPLv3 URL: http://launchpad.net/cloud-init @@ -34,6 +34,28 @@ Patch13: ci-remove-unnecessary-EOF-string-in-disable-sshd-keygen.patch Patch14: ci-fix-error-on-upgrade-caused-by-new-vendordata2-attri.patch # For bz#2028031 - [RHEL-9] Above 19.2 of cloud-init fails to configure routes when configuring static and default routes to the same destination IP Patch15: ci-cloudinit-net-handle-two-different-routes-for-the-sa.patch +# For bz#2040090 - [cloud-init][RHEL9] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' +Patch16: ci-Datasource-for-VMware-953.patch +# For bz#2040090 - [cloud-init][RHEL9] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' +Patch17: ci-Change-netifaces-dependency-to-0.10.4-965.patch +# For bz#2040090 - [cloud-init][RHEL9] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' +Patch18: ci-Update-dscheck_VMware-s-rpctool-check-970.patch +# For bz#2040090 - [cloud-init][RHEL9] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' +Patch19: ci-Revert-unnecesary-lcase-in-ds-identify-978.patch +# For bz#2042351 - [RHEL-9] Support for provisioning Azure VM with userdata +Patch20: ci-Add-flexibility-to-IMDS-api-version-793.patch +# For bz#2042351 - [RHEL-9] Support for provisioning Azure VM with userdata +Patch21: ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch +# For bz#2042351 - [RHEL-9] Support for provisioning Azure VM with userdata +Patch22: ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch +# For bz#2042351 - [RHEL-9] Support for provisioning Azure VM with userdata +Patch23: ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch +# For bz#2042351 - [RHEL-9] Support for provisioning Azure VM with userdata +Patch24: ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch +# For bz#2042351 - [RHEL-9] Support for provisioning Azure VM with userdata +Patch25: ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch +# For bz#2042351 - [RHEL-9] Support for provisioning Azure VM with userdata +Patch26: ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch # Source-git patches @@ -86,6 +108,7 @@ Requires: dhcp-client # https://bugzilla.redhat.com/show_bug.cgi?id=2032524 Requires: gdisk Requires: openssl +Requires: python3-netifaces %{?systemd_requires} @@ -234,6 +257,26 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog +* Mon Feb 07 2022 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-17 +- ci-Add-flexibility-to-IMDS-api-version-793.patch [bz#2042351] +- ci-Azure-helper-Ensure-Azure-http-handler-sleeps-betwee.patch [bz#2042351] +- ci-azure-Removing-ability-to-invoke-walinuxagent-799.patch [bz#2042351] +- ci-Azure-eject-the-provisioning-iso-before-reporting-re.patch [bz#2042351] +- ci-Azure-Retrieve-username-and-hostname-from-IMDS-865.patch [bz#2042351] +- ci-Azure-Retry-net-metadata-during-nic-attach-for-non-t.patch [bz#2042351] +- ci-Azure-adding-support-for-consuming-userdata-from-IMD.patch [bz#2042351] +- Resolves: bz#2042351 + ([RHEL-9] Support for provisioning Azure VM with userdata) + +* Fri Jan 21 2022 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-16 +- ci-Datasource-for-VMware-953.patch [bz#2040090] +- ci-Change-netifaces-dependency-to-0.10.4-965.patch [bz#2040090] +- ci-Update-dscheck_VMware-s-rpctool-check-970.patch [bz#2040090] +- ci-Revert-unnecesary-lcase-in-ds-identify-978.patch [bz#2040090] +- ci-Add-netifaces-package-as-a-Requires-in-cloud-init.sp.patch [bz#2040090] +- Resolves: bz#2040090 + ([cloud-init][RHEL9] Support for cloud-init datasource 'cloud-init-vmware-guestinfo') + * Thu Jan 13 2022 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-15 - ci-Add-gdisk-and-openssl-as-deps-to-fix-UEFI-Azure-init.patch [bz#2032524] - Resolves: bz#2032524