sailesh1993 / rpms / cloud-init

Forked from rpms/cloud-init a year ago
Clone
4eb3b8
From 2a2a5cdec0de0b96d503f9357c1641043574f90a Mon Sep 17 00:00:00 2001
4eb3b8
From: Thomas Stringer <thstring@microsoft.com>
4eb3b8
Date: Wed, 3 Mar 2021 11:07:43 -0500
4eb3b8
Subject: [PATCH 1/7] Add flexibility to IMDS api-version (#793)
4eb3b8
4eb3b8
RH-Author: Eduardo Otubo <otubo@redhat.com>
4eb3b8
RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
4eb3b8
RH-Commit: [1/7] 9aa42581c4ff175fb6f8f4a78d94cac9c9971062
4eb3b8
RH-Bugzilla: 2023940
4eb3b8
RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
4eb3b8
RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
4eb3b8
4eb3b8
Add flexibility to IMDS api-version by having both a desired IMDS
4eb3b8
api-version and a minimum api-version. The desired api-version will
4eb3b8
be used first, and if that fails it will fall back to the minimum
4eb3b8
api-version.
4eb3b8
---
4eb3b8
 cloudinit/sources/DataSourceAzure.py          | 113 ++++++++++++++----
4eb3b8
 tests/unittests/test_datasource/test_azure.py |  42 ++++++-
4eb3b8
 2 files changed, 129 insertions(+), 26 deletions(-)
4eb3b8
4eb3b8
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
4eb3b8
index 553b5a7e..de1452ce 100755
4eb3b8
--- a/cloudinit/sources/DataSourceAzure.py
4eb3b8
+++ b/cloudinit/sources/DataSourceAzure.py
4eb3b8
@@ -78,17 +78,15 @@ AGENT_SEED_DIR = '/var/lib/waagent'
4eb3b8
 # In the event where the IMDS primary server is not
4eb3b8
 # available, it takes 1s to fallback to the secondary one
4eb3b8
 IMDS_TIMEOUT_IN_SECONDS = 2
4eb3b8
-IMDS_URL = "http://169.254.169.254/metadata/"
4eb3b8
-IMDS_VER = "2019-06-01"
4eb3b8
-IMDS_VER_PARAM = "api-version={}".format(IMDS_VER)
4eb3b8
+IMDS_URL = "http://169.254.169.254/metadata"
4eb3b8
+IMDS_VER_MIN = "2019-06-01"
4eb3b8
+IMDS_VER_WANT = "2020-09-01"
4eb3b8
 
4eb3b8
 
4eb3b8
 class metadata_type(Enum):
4eb3b8
-    compute = "{}instance?{}".format(IMDS_URL, IMDS_VER_PARAM)
4eb3b8
-    network = "{}instance/network?{}".format(IMDS_URL,
4eb3b8
-                                             IMDS_VER_PARAM)
4eb3b8
-    reprovisiondata = "{}reprovisiondata?{}".format(IMDS_URL,
4eb3b8
-                                                    IMDS_VER_PARAM)
4eb3b8
+    compute = "{}/instance".format(IMDS_URL)
4eb3b8
+    network = "{}/instance/network".format(IMDS_URL)
4eb3b8
+    reprovisiondata = "{}/reprovisiondata".format(IMDS_URL)
4eb3b8
 
4eb3b8
 
4eb3b8
 PLATFORM_ENTROPY_SOURCE = "/sys/firmware/acpi/tables/OEM0"
4eb3b8
@@ -349,6 +347,8 @@ class DataSourceAzure(sources.DataSource):
4eb3b8
         self.update_events['network'].add(EventType.BOOT)
4eb3b8
         self._ephemeral_dhcp_ctx = None
4eb3b8
 
4eb3b8
+        self.failed_desired_api_version = False
4eb3b8
+
4eb3b8
     def __str__(self):
4eb3b8
         root = sources.DataSource.__str__(self)
4eb3b8
         return "%s [seed=%s]" % (root, self.seed)
4eb3b8
@@ -520,8 +520,10 @@ class DataSourceAzure(sources.DataSource):
4eb3b8
                     self._wait_for_all_nics_ready()
4eb3b8
                 ret = self._reprovision()
4eb3b8
 
4eb3b8
-            imds_md = get_metadata_from_imds(
4eb3b8
-                self.fallback_interface, retries=10)
4eb3b8
+            imds_md = self.get_imds_data_with_api_fallback(
4eb3b8
+                self.fallback_interface,
4eb3b8
+                retries=10
4eb3b8
+            )
4eb3b8
             (md, userdata_raw, cfg, files) = ret
4eb3b8
             self.seed = cdev
4eb3b8
             crawled_data.update({
4eb3b8
@@ -652,6 +654,57 @@ class DataSourceAzure(sources.DataSource):
4eb3b8
             self.ds_cfg['data_dir'], crawled_data['files'], dirmode=0o700)
4eb3b8
         return True
4eb3b8
 
4eb3b8
+    @azure_ds_telemetry_reporter
4eb3b8
+    def get_imds_data_with_api_fallback(
4eb3b8
+            self,
4eb3b8
+            fallback_nic,
4eb3b8
+            retries,
4eb3b8
+            md_type=metadata_type.compute):
4eb3b8
+        """
4eb3b8
+        Wrapper for get_metadata_from_imds so that we can have flexibility
4eb3b8
+        in which IMDS api-version we use. If a particular instance of IMDS
4eb3b8
+        does not have the api version that is desired, we want to make
4eb3b8
+        this fault tolerant and fall back to a good known minimum api
4eb3b8
+        version.
4eb3b8
+        """
4eb3b8
+
4eb3b8
+        if not self.failed_desired_api_version:
4eb3b8
+            for _ in range(retries):
4eb3b8
+                try:
4eb3b8
+                    LOG.info(
4eb3b8
+                        "Attempting IMDS api-version: %s",
4eb3b8
+                        IMDS_VER_WANT
4eb3b8
+                    )
4eb3b8
+                    return get_metadata_from_imds(
4eb3b8
+                        fallback_nic=fallback_nic,
4eb3b8
+                        retries=0,
4eb3b8
+                        md_type=md_type,
4eb3b8
+                        api_version=IMDS_VER_WANT
4eb3b8
+                    )
4eb3b8
+                except UrlError as err:
4eb3b8
+                    LOG.info(
4eb3b8
+                        "UrlError with IMDS api-version: %s",
4eb3b8
+                        IMDS_VER_WANT
4eb3b8
+                    )
4eb3b8
+                    if err.code == 400:
4eb3b8
+                        log_msg = "Fall back to IMDS api-version: {}".format(
4eb3b8
+                            IMDS_VER_MIN
4eb3b8
+                        )
4eb3b8
+                        report_diagnostic_event(
4eb3b8
+                            log_msg,
4eb3b8
+                            logger_func=LOG.info
4eb3b8
+                        )
4eb3b8
+                        self.failed_desired_api_version = True
4eb3b8
+                        break
4eb3b8
+
4eb3b8
+        LOG.info("Using IMDS api-version: %s", IMDS_VER_MIN)
4eb3b8
+        return get_metadata_from_imds(
4eb3b8
+            fallback_nic=fallback_nic,
4eb3b8
+            retries=retries,
4eb3b8
+            md_type=md_type,
4eb3b8
+            api_version=IMDS_VER_MIN
4eb3b8
+        )
4eb3b8
+
4eb3b8
     def device_name_to_device(self, name):
4eb3b8
         return self.ds_cfg['disk_aliases'].get(name)
4eb3b8
 
4eb3b8
@@ -880,10 +933,11 @@ class DataSourceAzure(sources.DataSource):
4eb3b8
         # primary nic is being attached first helps here. Otherwise each nic
4eb3b8
         # could add several seconds of delay.
4eb3b8
         try:
4eb3b8
-            imds_md = get_metadata_from_imds(
4eb3b8
+            imds_md = self.get_imds_data_with_api_fallback(
4eb3b8
                 ifname,
4eb3b8
                 5,
4eb3b8
-                metadata_type.network)
4eb3b8
+                metadata_type.network
4eb3b8
+            )
4eb3b8
         except Exception as e:
4eb3b8
             LOG.warning(
4eb3b8
                 "Failed to get network metadata using nic %s. Attempt to "
4eb3b8
@@ -1017,7 +1071,10 @@ class DataSourceAzure(sources.DataSource):
4eb3b8
     def _poll_imds(self):
4eb3b8
         """Poll IMDS for the new provisioning data until we get a valid
4eb3b8
         response. Then return the returned JSON object."""
4eb3b8
-        url = metadata_type.reprovisiondata.value
4eb3b8
+        url = "{}?api-version={}".format(
4eb3b8
+            metadata_type.reprovisiondata.value,
4eb3b8
+            IMDS_VER_MIN
4eb3b8
+        )
4eb3b8
         headers = {"Metadata": "true"}
4eb3b8
         nl_sock = None
4eb3b8
         report_ready = bool(not os.path.isfile(REPORTED_READY_MARKER_FILE))
4eb3b8
@@ -2059,7 +2116,8 @@ def _generate_network_config_from_fallback_config() -> dict:
4eb3b8
 @azure_ds_telemetry_reporter
4eb3b8
 def get_metadata_from_imds(fallback_nic,
4eb3b8
                            retries,
4eb3b8
-                           md_type=metadata_type.compute):
4eb3b8
+                           md_type=metadata_type.compute,
4eb3b8
+                           api_version=IMDS_VER_MIN):
4eb3b8
     """Query Azure's instance metadata service, returning a dictionary.
4eb3b8
 
4eb3b8
     If network is not up, setup ephemeral dhcp on fallback_nic to talk to the
4eb3b8
@@ -2069,13 +2127,16 @@ def get_metadata_from_imds(fallback_nic,
4eb3b8
     @param fallback_nic: String. The name of the nic which requires active
4eb3b8
         network in order to query IMDS.
4eb3b8
     @param retries: The number of retries of the IMDS_URL.
4eb3b8
+    @param md_type: Metadata type for IMDS request.
4eb3b8
+    @param api_version: IMDS api-version to use in the request.
4eb3b8
 
4eb3b8
     @return: A dict of instance metadata containing compute and network
4eb3b8
         info.
4eb3b8
     """
4eb3b8
     kwargs = {'logfunc': LOG.debug,
4eb3b8
               'msg': 'Crawl of Azure Instance Metadata Service (IMDS)',
4eb3b8
-              'func': _get_metadata_from_imds, 'args': (retries, md_type,)}
4eb3b8
+              'func': _get_metadata_from_imds,
4eb3b8
+              'args': (retries, md_type, api_version,)}
4eb3b8
     if net.is_up(fallback_nic):
4eb3b8
         return util.log_time(**kwargs)
4eb3b8
     else:
4eb3b8
@@ -2091,20 +2152,26 @@ def get_metadata_from_imds(fallback_nic,
4eb3b8
 
4eb3b8
 
4eb3b8
 @azure_ds_telemetry_reporter
4eb3b8
-def _get_metadata_from_imds(retries, md_type=metadata_type.compute):
4eb3b8
-
4eb3b8
-    url = md_type.value
4eb3b8
+def _get_metadata_from_imds(
4eb3b8
+        retries,
4eb3b8
+        md_type=metadata_type.compute,
4eb3b8
+        api_version=IMDS_VER_MIN):
4eb3b8
+    url = "{}?api-version={}".format(md_type.value, api_version)
4eb3b8
     headers = {"Metadata": "true"}
4eb3b8
     try:
4eb3b8
         response = readurl(
4eb3b8
             url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers,
4eb3b8
             retries=retries, exception_cb=retry_on_url_exc)
4eb3b8
     except Exception as e:
4eb3b8
-        report_diagnostic_event(
4eb3b8
-            'Ignoring IMDS instance metadata. '
4eb3b8
-            'Get metadata from IMDS failed: %s' % e,
4eb3b8
-            logger_func=LOG.warning)
4eb3b8
-        return {}
4eb3b8
+        # pylint:disable=no-member
4eb3b8
+        if isinstance(e, UrlError) and e.code == 400:
4eb3b8
+            raise
4eb3b8
+        else:
4eb3b8
+            report_diagnostic_event(
4eb3b8
+                'Ignoring IMDS instance metadata. '
4eb3b8
+                'Get metadata from IMDS failed: %s' % e,
4eb3b8
+                logger_func=LOG.warning)
4eb3b8
+            return {}
4eb3b8
     try:
4eb3b8
         from json.decoder import JSONDecodeError
4eb3b8
         json_decode_error = JSONDecodeError
4eb3b8
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
4eb3b8
index f597c723..dedebeb1 100644
4eb3b8
--- a/tests/unittests/test_datasource/test_azure.py
4eb3b8
+++ b/tests/unittests/test_datasource/test_azure.py
4eb3b8
@@ -408,7 +408,9 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
4eb3b8
 
4eb3b8
     def setUp(self):
4eb3b8
         super(TestGetMetadataFromIMDS, self).setUp()
4eb3b8
-        self.network_md_url = dsaz.IMDS_URL + "instance?api-version=2019-06-01"
4eb3b8
+        self.network_md_url = "{}/instance?api-version=2019-06-01".format(
4eb3b8
+            dsaz.IMDS_URL
4eb3b8
+        )
4eb3b8
 
4eb3b8
     @mock.patch(MOCKPATH + 'readurl')
4eb3b8
     @mock.patch(MOCKPATH + 'EphemeralDHCPv4', autospec=True)
4eb3b8
@@ -518,7 +520,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
4eb3b8
         """Return empty dict when IMDS network metadata is absent."""
4eb3b8
         httpretty.register_uri(
4eb3b8
             httpretty.GET,
4eb3b8
-            dsaz.IMDS_URL + 'instance?api-version=2017-12-01',
4eb3b8
+            dsaz.IMDS_URL + '/instance?api-version=2017-12-01',
4eb3b8
             body={}, status=404)
4eb3b8
 
4eb3b8
         m_net_is_up.return_value = True  # skips dhcp
4eb3b8
@@ -1877,6 +1879,40 @@ scbus-1 on xpt0 bus 0
4eb3b8
         ssh_keys = dsrc.get_public_ssh_keys()
4eb3b8
         self.assertEqual(ssh_keys, ['key2'])
4eb3b8
 
4eb3b8
+    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
4eb3b8
+    def test_imds_api_version_wanted_nonexistent(
4eb3b8
+            self,
4eb3b8
+            m_get_metadata_from_imds):
4eb3b8
+        def get_metadata_from_imds_side_eff(*args, **kwargs):
4eb3b8
+            if kwargs['api_version'] == dsaz.IMDS_VER_WANT:
4eb3b8
+                raise url_helper.UrlError("No IMDS version", code=400)
4eb3b8
+            return NETWORK_METADATA
4eb3b8
+        m_get_metadata_from_imds.side_effect = get_metadata_from_imds_side_eff
4eb3b8
+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
4eb3b8
+        odata = {'HostName': "myhost", 'UserName': "myuser"}
4eb3b8
+        data = {
4eb3b8
+            'ovfcontent': construct_valid_ovf_env(data=odata),
4eb3b8
+            'sys_cfg': sys_cfg
4eb3b8
+        }
4eb3b8
+        dsrc = self._get_ds(data)
4eb3b8
+        dsrc.get_data()
4eb3b8
+        self.assertIsNotNone(dsrc.metadata)
4eb3b8
+        self.assertTrue(dsrc.failed_desired_api_version)
4eb3b8
+
4eb3b8
+    @mock.patch(
4eb3b8
+        MOCKPATH + 'get_metadata_from_imds', return_value=NETWORK_METADATA)
4eb3b8
+    def test_imds_api_version_wanted_exists(self, m_get_metadata_from_imds):
4eb3b8
+        sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
4eb3b8
+        odata = {'HostName': "myhost", 'UserName': "myuser"}
4eb3b8
+        data = {
4eb3b8
+            'ovfcontent': construct_valid_ovf_env(data=odata),
4eb3b8
+            'sys_cfg': sys_cfg
4eb3b8
+        }
4eb3b8
+        dsrc = self._get_ds(data)
4eb3b8
+        dsrc.get_data()
4eb3b8
+        self.assertIsNotNone(dsrc.metadata)
4eb3b8
+        self.assertFalse(dsrc.failed_desired_api_version)
4eb3b8
+
4eb3b8
 
4eb3b8
 class TestAzureBounce(CiTestCase):
4eb3b8
 
4eb3b8
@@ -2657,7 +2693,7 @@ class TestPreprovisioningHotAttachNics(CiTestCase):
4eb3b8
     @mock.patch(MOCKPATH + 'DataSourceAzure.wait_for_link_up')
4eb3b8
     @mock.patch('cloudinit.sources.helpers.netlink.wait_for_nic_attach_event')
4eb3b8
     @mock.patch('cloudinit.sources.net.find_fallback_nic')
4eb3b8
-    @mock.patch(MOCKPATH + 'get_metadata_from_imds')
4eb3b8
+    @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback')
4eb3b8
     @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
4eb3b8
     @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach')
4eb3b8
     @mock.patch('os.path.isfile')
4eb3b8
-- 
4eb3b8
2.27.0
4eb3b8