42024e
From 816fe5c2e6d5dcc68f292092b00b2acfbc4c8e88 Mon Sep 17 00:00:00 2001
42024e
From: aswinrajamannar <39812128+aswinrajamannar@users.noreply.github.com>
42024e
Date: Mon, 26 Apr 2021 07:28:39 -0700
42024e
Subject: [PATCH 6/7] Azure: Retry net metadata during nic attach for
42024e
 non-timeout errs (#878)
42024e
42024e
RH-Author: Eduardo Otubo <otubo@redhat.com>
42024e
RH-MergeRequest: 18: Add support for userdata on Azure from IMDS
42024e
RH-Commit: [6/7] 794cd340644260bb43a7c8582a8067f403b9842d (otubo/cloud-init-src)
42024e
RH-Bugzilla: 2042351
42024e
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
42024e
RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
42024e
42024e
When network interfaces are hot-attached to the VM, attempting to get
42024e
network metadata might return 410 (or 500, 503 etc) because the info
42024e
is not yet available. In those cases, we retry getting the metadata
42024e
before giving up. The only case where we can move on to wait for more
42024e
nic attach events is if the call times out despite retries, which
42024e
means the interface is not likely a primary interface, and we should
42024e
try for more nic attach events.
42024e
---
42024e
 cloudinit/sources/DataSourceAzure.py          | 65 +++++++++++--
42024e
 tests/unittests/test_datasource/test_azure.py | 95 ++++++++++++++++---
42024e
 2 files changed, 140 insertions(+), 20 deletions(-)
42024e
42024e
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
42024e
index 6d7954ee..d0be6d84 100755
42024e
--- a/cloudinit/sources/DataSourceAzure.py
42024e
+++ b/cloudinit/sources/DataSourceAzure.py
42024e
@@ -17,6 +17,7 @@ from time import sleep
42024e
 from xml.dom import minidom
42024e
 import xml.etree.ElementTree as ET
42024e
 from enum import Enum
42024e
+import requests
42024e
 
42024e
 from cloudinit import dmi
42024e
 from cloudinit import log as logging
42024e
@@ -665,7 +666,9 @@ class DataSourceAzure(sources.DataSource):
42024e
             self,
42024e
             fallback_nic,
42024e
             retries,
42024e
-            md_type=metadata_type.compute):
42024e
+            md_type=metadata_type.compute,
42024e
+            exc_cb=retry_on_url_exc,
42024e
+            infinite=False):
42024e
         """
42024e
         Wrapper for get_metadata_from_imds so that we can have flexibility
42024e
         in which IMDS api-version we use. If a particular instance of IMDS
42024e
@@ -685,7 +688,8 @@ class DataSourceAzure(sources.DataSource):
42024e
                         fallback_nic=fallback_nic,
42024e
                         retries=0,
42024e
                         md_type=md_type,
42024e
-                        api_version=IMDS_VER_WANT
42024e
+                        api_version=IMDS_VER_WANT,
42024e
+                        exc_cb=exc_cb
42024e
                     )
42024e
                 except UrlError as err:
42024e
                     LOG.info(
42024e
@@ -708,7 +712,9 @@ class DataSourceAzure(sources.DataSource):
42024e
             fallback_nic=fallback_nic,
42024e
             retries=retries,
42024e
             md_type=md_type,
42024e
-            api_version=IMDS_VER_MIN
42024e
+            api_version=IMDS_VER_MIN,
42024e
+            exc_cb=exc_cb,
42024e
+            infinite=infinite
42024e
         )
42024e
 
42024e
     def device_name_to_device(self, name):
42024e
@@ -938,6 +944,9 @@ class DataSourceAzure(sources.DataSource):
42024e
         is_primary = False
42024e
         expected_nic_count = -1
42024e
         imds_md = None
42024e
+        metadata_poll_count = 0
42024e
+        metadata_logging_threshold = 1
42024e
+        metadata_timeout_count = 0
42024e
 
42024e
         # For now, only a VM's primary NIC can contact IMDS and WireServer. If
42024e
         # DHCP fails for a NIC, we have no mechanism to determine if the NIC is
42024e
@@ -962,14 +971,48 @@ class DataSourceAzure(sources.DataSource):
42024e
                                     % (ifname, e), logger_func=LOG.error)
42024e
             raise
42024e
 
42024e
+        # Retry polling network metadata for a limited duration only when the
42024e
+        # calls fail due to timeout. This is because the platform drops packets
42024e
+        # going towards IMDS when it is not a primary nic. If the calls fail
42024e
+        # due to other issues like 410, 503 etc, then it means we are primary
42024e
+        # but IMDS service is unavailable at the moment. Retry indefinitely in
42024e
+        # those cases since we cannot move on without the network metadata.
42024e
+        def network_metadata_exc_cb(msg, exc):
42024e
+            nonlocal metadata_timeout_count, metadata_poll_count
42024e
+            nonlocal metadata_logging_threshold
42024e
+
42024e
+            metadata_poll_count = metadata_poll_count + 1
42024e
+
42024e
+            # Log when needed but back off exponentially to avoid exploding
42024e
+            # the log file.
42024e
+            if metadata_poll_count >= metadata_logging_threshold:
42024e
+                metadata_logging_threshold *= 2
42024e
+                report_diagnostic_event(
42024e
+                    "Ran into exception when attempting to reach %s "
42024e
+                    "after %d polls." % (msg, metadata_poll_count),
42024e
+                    logger_func=LOG.error)
42024e
+
42024e
+                if isinstance(exc, UrlError):
42024e
+                    report_diagnostic_event("poll IMDS with %s failed. "
42024e
+                                            "Exception: %s and code: %s" %
42024e
+                                            (msg, exc.cause, exc.code),
42024e
+                                            logger_func=LOG.error)
42024e
+
42024e
+            if exc.cause and isinstance(exc.cause, requests.Timeout):
42024e
+                metadata_timeout_count = metadata_timeout_count + 1
42024e
+                return (metadata_timeout_count <= 10)
42024e
+            return True
42024e
+
42024e
         # Primary nic detection will be optimized in the future. The fact that
42024e
         # primary nic is being attached first helps here. Otherwise each nic
42024e
         # could add several seconds of delay.
42024e
         try:
42024e
             imds_md = self.get_imds_data_with_api_fallback(
42024e
                 ifname,
42024e
-                5,
42024e
-                metadata_type.network
42024e
+                0,
42024e
+                metadata_type.network,
42024e
+                network_metadata_exc_cb,
42024e
+                True
42024e
             )
42024e
         except Exception as e:
42024e
             LOG.warning(
42024e
@@ -2139,7 +2182,9 @@ def _generate_network_config_from_fallback_config() -> dict:
42024e
 def get_metadata_from_imds(fallback_nic,
42024e
                            retries,
42024e
                            md_type=metadata_type.compute,
42024e
-                           api_version=IMDS_VER_MIN):
42024e
+                           api_version=IMDS_VER_MIN,
42024e
+                           exc_cb=retry_on_url_exc,
42024e
+                           infinite=False):
42024e
     """Query Azure's instance metadata service, returning a dictionary.
42024e
 
42024e
     If network is not up, setup ephemeral dhcp on fallback_nic to talk to the
42024e
@@ -2158,7 +2203,7 @@ def get_metadata_from_imds(fallback_nic,
42024e
     kwargs = {'logfunc': LOG.debug,
42024e
               'msg': 'Crawl of Azure Instance Metadata Service (IMDS)',
42024e
               'func': _get_metadata_from_imds,
42024e
-              'args': (retries, md_type, api_version,)}
42024e
+              'args': (retries, exc_cb, md_type, api_version, infinite)}
42024e
     if net.is_up(fallback_nic):
42024e
         return util.log_time(**kwargs)
42024e
     else:
42024e
@@ -2176,14 +2221,16 @@ def get_metadata_from_imds(fallback_nic,
42024e
 @azure_ds_telemetry_reporter
42024e
 def _get_metadata_from_imds(
42024e
         retries,
42024e
+        exc_cb,
42024e
         md_type=metadata_type.compute,
42024e
-        api_version=IMDS_VER_MIN):
42024e
+        api_version=IMDS_VER_MIN,
42024e
+        infinite=False):
42024e
     url = "{}?api-version={}".format(md_type.value, api_version)
42024e
     headers = {"Metadata": "true"}
42024e
     try:
42024e
         response = readurl(
42024e
             url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers,
42024e
-            retries=retries, exception_cb=retry_on_url_exc)
42024e
+            retries=retries, exception_cb=exc_cb, infinite=infinite)
42024e
     except Exception as e:
42024e
         # pylint:disable=no-member
42024e
         if isinstance(e, UrlError) and e.code == 400:
42024e
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
42024e
index d9817d84..c4a8e08d 100644
42024e
--- a/tests/unittests/test_datasource/test_azure.py
42024e
+++ b/tests/unittests/test_datasource/test_azure.py
42024e
@@ -448,7 +448,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
42024e
             "http://169.254.169.254/metadata/instance?api-version="
42024e
             "2019-06-01", exception_cb=mock.ANY,
42024e
             headers=mock.ANY, retries=mock.ANY,
42024e
-            timeout=mock.ANY)
42024e
+            timeout=mock.ANY, infinite=False)
42024e
 
42024e
     @mock.patch(MOCKPATH + 'readurl', autospec=True)
42024e
     @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
42024e
@@ -467,7 +467,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
42024e
             "http://169.254.169.254/metadata/instance/network?api-version="
42024e
             "2019-06-01", exception_cb=mock.ANY,
42024e
             headers=mock.ANY, retries=mock.ANY,
42024e
-            timeout=mock.ANY)
42024e
+            timeout=mock.ANY, infinite=False)
42024e
 
42024e
     @mock.patch(MOCKPATH + 'readurl', autospec=True)
42024e
     @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
42024e
@@ -486,7 +486,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
42024e
             "http://169.254.169.254/metadata/instance?api-version="
42024e
             "2019-06-01", exception_cb=mock.ANY,
42024e
             headers=mock.ANY, retries=mock.ANY,
42024e
-            timeout=mock.ANY)
42024e
+            timeout=mock.ANY, infinite=False)
42024e
 
42024e
     @mock.patch(MOCKPATH + 'readurl', autospec=True)
42024e
     @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting', autospec=True)
42024e
@@ -511,7 +511,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
42024e
         m_readurl.assert_called_with(
42024e
             self.network_md_url, exception_cb=mock.ANY,
42024e
             headers={'Metadata': 'true'}, retries=2,
42024e
-            timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS)
42024e
+            timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS, infinite=False)
42024e
 
42024e
     @mock.patch('cloudinit.url_helper.time.sleep')
42024e
     @mock.patch(MOCKPATH + 'net.is_up', autospec=True)
42024e
@@ -2694,15 +2694,22 @@ class TestPreprovisioningHotAttachNics(CiTestCase):
42024e
 
42024e
         def nic_attach_ret(nl_sock, nics_found):
42024e
             nonlocal m_attach_call_count
42024e
-            if m_attach_call_count == 0:
42024e
-                m_attach_call_count = m_attach_call_count + 1
42024e
+            m_attach_call_count = m_attach_call_count + 1
42024e
+            if m_attach_call_count == 1:
42024e
                 return "eth0"
42024e
-            return "eth1"
42024e
+            elif m_attach_call_count == 2:
42024e
+                return "eth1"
42024e
+            raise RuntimeError("Must have found primary nic by now.")
42024e
+
42024e
+        # Simulate two NICs by adding the same one twice.
42024e
+        md = {
42024e
+            "interface": [
42024e
+                IMDS_NETWORK_METADATA['interface'][0],
42024e
+                IMDS_NETWORK_METADATA['interface'][0]
42024e
+            ]
42024e
+        }
42024e
 
42024e
-        def network_metadata_ret(ifname, retries, type):
42024e
-            # Simulate two NICs by adding the same one twice.
42024e
-            md = IMDS_NETWORK_METADATA
42024e
-            md['interface'].append(md['interface'][0])
42024e
+        def network_metadata_ret(ifname, retries, type, exc_cb, infinite):
42024e
             if ifname == "eth0":
42024e
                 return md
42024e
             raise requests.Timeout('Fake connection timeout')
42024e
@@ -2724,6 +2731,72 @@ class TestPreprovisioningHotAttachNics(CiTestCase):
42024e
         self.assertEqual(1, m_imds.call_count)
42024e
         self.assertEqual(2, m_link_up.call_count)
42024e
 
42024e
+    @mock.patch(MOCKPATH + 'DataSourceAzure.get_imds_data_with_api_fallback')
42024e
+    @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
42024e
+    def test_check_if_nic_is_primary_retries_on_failures(
42024e
+            self, m_dhcpv4, m_imds):
42024e
+        """Retry polling for network metadata on all failures except timeout"""
42024e
+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
42024e
+        lease = {
42024e
+            'interface': 'eth9', 'fixed-address': '192.168.2.9',
42024e
+            'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
42024e
+            'unknown-245': '624c3620'}
42024e
+
42024e
+        eth0Retries = []
42024e
+        eth1Retries = []
42024e
+        # Simulate two NICs by adding the same one twice.
42024e
+        md = {
42024e
+            "interface": [
42024e
+                IMDS_NETWORK_METADATA['interface'][0],
42024e
+                IMDS_NETWORK_METADATA['interface'][0]
42024e
+            ]
42024e
+        }
42024e
+
42024e
+        def network_metadata_ret(ifname, retries, type, exc_cb, infinite):
42024e
+            nonlocal eth0Retries, eth1Retries
42024e
+
42024e
+            # Simulate readurl functionality with retries and
42024e
+            # exception callbacks so that the callback logic can be
42024e
+            # validated.
42024e
+            if ifname == "eth0":
42024e
+                cause = requests.HTTPError()
42024e
+                for _ in range(0, 15):
42024e
+                    error = url_helper.UrlError(cause=cause, code=410)
42024e
+                    eth0Retries.append(exc_cb("No goal state.", error))
42024e
+            else:
42024e
+                cause = requests.Timeout('Fake connection timeout')
42024e
+                for _ in range(0, 10):
42024e
+                    error = url_helper.UrlError(cause=cause)
42024e
+                    eth1Retries.append(exc_cb("Connection timeout", error))
42024e
+                # Should stop retrying after 10 retries
42024e
+                eth1Retries.append(exc_cb("Connection timeout", error))
42024e
+                raise cause
42024e
+            return md
42024e
+
42024e
+        m_imds.side_effect = network_metadata_ret
42024e
+
42024e
+        dhcp_ctx = mock.MagicMock(lease=lease)
42024e
+        dhcp_ctx.obtain_lease.return_value = lease
42024e
+        m_dhcpv4.return_value = dhcp_ctx
42024e
+
42024e
+        is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth0")
42024e
+        self.assertEqual(True, is_primary)
42024e
+        self.assertEqual(2, expected_nic_count)
42024e
+
42024e
+        # All Eth0 errors are non-timeout errors. So we should have been
42024e
+        # retrying indefinitely until success.
42024e
+        for i in eth0Retries:
42024e
+            self.assertTrue(i)
42024e
+
42024e
+        is_primary, expected_nic_count = dsa._check_if_nic_is_primary("eth1")
42024e
+        self.assertEqual(False, is_primary)
42024e
+
42024e
+        # All Eth1 errors are timeout errors. Retry happens for a max of 10 and
42024e
+        # then we should have moved on assuming it is not the primary nic.
42024e
+        for i in range(0, 10):
42024e
+            self.assertTrue(eth1Retries[i])
42024e
+        self.assertFalse(eth1Retries[10])
42024e
+
42024e
     @mock.patch('cloudinit.distros.networking.LinuxNetworking.try_set_link_up')
42024e
     def test_wait_for_link_up_returns_if_already_up(
42024e
             self, m_is_link_up):
42024e
-- 
42024e
2.27.0
42024e