222d62
From b03bfae6c032a2590e094e9bceeedd47525ca057 Mon Sep 17 00:00:00 2001
222d62
From: Eduardo Otubo <otubo@redhat.com>
222d62
Date: Fri, 5 Oct 2018 09:53:02 +0200
222d62
Subject: [PATCH 3/4] azure: Add reported ready marker file.
222d62
222d62
RH-Author: Eduardo Otubo <otubo@redhat.com>
222d62
Message-id: <20181005095303.20597-4-otubo@redhat.com>
222d62
Patchwork-id: 82386
222d62
O-Subject: [RHEL-8.0 cloud-init PATCH 3/4] azure: Add reported ready marker file.
222d62
Bugzilla: 1615599
222d62
RH-Acked-by: Cathy Avery <cavery@redhat.com>
222d62
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
222d62
RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
222d62
222d62
commit aae494c39f4c6f625e7409ca262e657d085dd5d1
222d62
Author: Joshua Chan <jocha@microsoft.com>
222d62
Date:   Thu May 3 14:50:16 2018 -0600
222d62
222d62
    azure: Add reported ready marker file.
222d62
222d62
    This change is for Azure VM Preprovisioning. A bug was found when after
222d62
    azure VMs report ready the first time, during the time when VM is polling
222d62
    indefinitely for the new ovf-env.xml from Instance Metadata Service
222d62
    (IMDS), if a reboot happens, we send another report ready signal to the
222d62
    fabric, which deletes the reprovisioning data on the node.
222d62
222d62
    This marker file is used to fix this issue so that we will only send a
222d62
    report ready signal to the fabric when no marker file is present. Then,
222d62
    create a marker file so that when a reboot does occur, we check if a
222d62
    marker file has been created and decide whether we would like to send the
222d62
    repot ready signal.
222d62
222d62
    LP: #1765214
222d62
222d62
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
222d62
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
222d62
---
222d62
 cloudinit/sources/DataSourceAzure.py          |  21 +++-
222d62
 tests/unittests/test_datasource/test_azure.py | 170 ++++++++++++++++++--------
222d62
 2 files changed, 134 insertions(+), 57 deletions(-)
222d62
222d62
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
222d62
index 7e49455..46d5744 100644
222d62
--- a/cloudinit/sources/DataSourceAzure.py
222d62
+++ b/cloudinit/sources/DataSourceAzure.py
222d62
@@ -48,6 +48,7 @@ DEFAULT_FS = 'ext4'
222d62
 # DMI chassis-asset-tag is set static for all azure instances
222d62
 AZURE_CHASSIS_ASSET_TAG = '7783-7084-3265-9085-8269-3286-77'
222d62
 REPROVISION_MARKER_FILE = "/var/lib/cloud/data/poll_imds"
222d62
+REPORTED_READY_MARKER_FILE = "/var/lib/cloud/data/reported_ready"
222d62
 IMDS_URL = "http://169.254.169.254/metadata/reprovisiondata"
222d62
 
222d62
 
222d62
@@ -439,11 +440,12 @@ class DataSourceAzure(sources.DataSource):
222d62
             LOG.debug("negotiating already done for %s",
222d62
                       self.get_instance_id())
222d62
 
222d62
-    def _poll_imds(self, report_ready=True):
222d62
+    def _poll_imds(self):
222d62
         """Poll IMDS for the new provisioning data until we get a valid
222d62
         response. Then return the returned JSON object."""
222d62
         url = IMDS_URL + "?api-version=2017-04-02"
222d62
         headers = {"Metadata": "true"}
222d62
+        report_ready = bool(not os.path.isfile(REPORTED_READY_MARKER_FILE))
222d62
         LOG.debug("Start polling IMDS")
222d62
 
222d62
         def exc_cb(msg, exception):
222d62
@@ -453,13 +455,17 @@ class DataSourceAzure(sources.DataSource):
222d62
             # call DHCP and setup the ephemeral network to acquire the new IP.
222d62
             return False
222d62
 
222d62
-        need_report = report_ready
222d62
         while True:
222d62
             try:
222d62
                 with EphemeralDHCPv4() as lease:
222d62
-                    if need_report:
222d62
+                    if report_ready:
222d62
+                        path = REPORTED_READY_MARKER_FILE
222d62
+                        LOG.info(
222d62
+                            "Creating a marker file to report ready: %s", path)
222d62
+                        util.write_file(path, "{pid}: {time}\n".format(
222d62
+                            pid=os.getpid(), time=time()))
222d62
                         self._report_ready(lease=lease)
222d62
-                        need_report = False
222d62
+                        report_ready = False
222d62
                     return readurl(url, timeout=1, headers=headers,
222d62
                                    exception_cb=exc_cb, infinite=True).contents
222d62
             except UrlError:
222d62
@@ -493,8 +499,10 @@ class DataSourceAzure(sources.DataSource):
222d62
         if (cfg.get('PreprovisionedVm') is True or
222d62
                 os.path.isfile(path)):
222d62
             if not os.path.isfile(path):
222d62
-                LOG.info("Creating a marker file to poll imds")
222d62
-                util.write_file(path, "%s: %s\n" % (os.getpid(), time()))
222d62
+                LOG.info("Creating a marker file to poll imds: %s",
222d62
+                         path)
222d62
+                util.write_file(path, "{pid}: {time}\n".format(
222d62
+                    pid=os.getpid(), time=time()))
222d62
             return True
222d62
         return False
222d62
 
222d62
@@ -529,6 +537,7 @@ class DataSourceAzure(sources.DataSource):
222d62
                 "Error communicating with Azure fabric; You may experience."
222d62
                 "connectivity issues.", exc_info=True)
222d62
             return False
222d62
+        util.del_file(REPORTED_READY_MARKER_FILE)
222d62
         util.del_file(REPROVISION_MARKER_FILE)
222d62
         return fabric_data
222d62
 
222d62
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
222d62
index af2c93a..ed810d2 100644
222d62
--- a/tests/unittests/test_datasource/test_azure.py
222d62
+++ b/tests/unittests/test_datasource/test_azure.py
222d62
@@ -1196,19 +1196,9 @@ class TestAzureNetExists(CiTestCase):
222d62
         self.assertTrue(hasattr(dsaz, "DataSourceAzureNet"))
222d62
 
222d62
 
222d62
-@mock.patch('cloudinit.sources.DataSourceAzure.util.subp')
222d62
-@mock.patch.object(dsaz, 'get_hostname')
222d62
-@mock.patch.object(dsaz, 'set_hostname')
222d62
-class TestAzureDataSourcePreprovisioning(CiTestCase):
222d62
-
222d62
-    def setUp(self):
222d62
-        super(TestAzureDataSourcePreprovisioning, self).setUp()
222d62
-        tmp = self.tmp_dir()
222d62
-        self.waagent_d = self.tmp_path('/var/lib/waagent', tmp)
222d62
-        self.paths = helpers.Paths({'cloud_dir': tmp})
222d62
-        dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
222d62
+class TestPreprovisioningReadAzureOvfFlag(CiTestCase):
222d62
 
222d62
-    def test_read_azure_ovf_with_true_flag(self, *args):
222d62
+    def test_read_azure_ovf_with_true_flag(self):
222d62
         """The read_azure_ovf method should set the PreprovisionedVM
222d62
            cfg flag if the proper setting is present."""
222d62
         content = construct_valid_ovf_env(
222d62
@@ -1217,7 +1207,7 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
222d62
         cfg = ret[2]
222d62
         self.assertTrue(cfg['PreprovisionedVm'])
222d62
 
222d62
-    def test_read_azure_ovf_with_false_flag(self, *args):
222d62
+    def test_read_azure_ovf_with_false_flag(self):
222d62
         """The read_azure_ovf method should set the PreprovisionedVM
222d62
            cfg flag to false if the proper setting is false."""
222d62
         content = construct_valid_ovf_env(
222d62
@@ -1226,7 +1216,7 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
222d62
         cfg = ret[2]
222d62
         self.assertFalse(cfg['PreprovisionedVm'])
222d62
 
222d62
-    def test_read_azure_ovf_without_flag(self, *args):
222d62
+    def test_read_azure_ovf_without_flag(self):
222d62
         """The read_azure_ovf method should not set the
222d62
            PreprovisionedVM cfg flag."""
222d62
         content = construct_valid_ovf_env()
222d62
@@ -1234,12 +1224,121 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
222d62
         cfg = ret[2]
222d62
         self.assertFalse(cfg['PreprovisionedVm'])
222d62
 
222d62
-    @mock.patch('cloudinit.sources.DataSourceAzure.util.is_FreeBSD')
222d62
-    @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
222d62
-    @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
222d62
-    @mock.patch('requests.Session.request')
222d62
+
222d62
+@mock.patch('os.path.isfile')
222d62
+class TestPreprovisioningShouldReprovision(CiTestCase):
222d62
+
222d62
+    def setUp(self):
222d62
+        super(TestPreprovisioningShouldReprovision, self).setUp()
222d62
+        tmp = self.tmp_dir()
222d62
+        self.waagent_d = self.tmp_path('/var/lib/waagent', tmp)
222d62
+        self.paths = helpers.Paths({'cloud_dir': tmp})
222d62
+        dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
222d62
+
222d62
+    @mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
222d62
+    def test__should_reprovision_with_true_cfg(self, isfile, write_f):
222d62
+        """The _should_reprovision method should return true with config
222d62
+           flag present."""
222d62
+        isfile.return_value = False
222d62
+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
+        self.assertTrue(dsa._should_reprovision(
222d62
+            (None, None, {'PreprovisionedVm': True}, None)))
222d62
+
222d62
+    def test__should_reprovision_with_file_existing(self, isfile):
222d62
+        """The _should_reprovision method should return True if the sentinal
222d62
+           exists."""
222d62
+        isfile.return_value = True
222d62
+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
+        self.assertTrue(dsa._should_reprovision(
222d62
+            (None, None, {'preprovisionedvm': False}, None)))
222d62
+
222d62
+    def test__should_reprovision_returns_false(self, isfile):
222d62
+        """The _should_reprovision method should return False
222d62
+           if config and sentinal are not present."""
222d62
+        isfile.return_value = False
222d62
+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
+        self.assertFalse(dsa._should_reprovision((None, None, {}, None)))
222d62
+
222d62
+    @mock.patch('cloudinit.sources.DataSourceAzure.DataSourceAzure._poll_imds')
222d62
+    def test_reprovision_calls__poll_imds(self, _poll_imds, isfile):
222d62
+        """_reprovision will poll IMDS."""
222d62
+        isfile.return_value = False
222d62
+        hostname = "myhost"
222d62
+        username = "myuser"
222d62
+        odata = {'HostName': hostname, 'UserName': username}
222d62
+        _poll_imds.return_value = construct_valid_ovf_env(data=odata)
222d62
+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
+        dsa._reprovision()
222d62
+        _poll_imds.assert_called_with()
222d62
+
222d62
+
222d62
+@mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
222d62
+@mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
222d62
+@mock.patch('requests.Session.request')
222d62
+@mock.patch(
222d62
+    'cloudinit.sources.DataSourceAzure.DataSourceAzure._report_ready')
222d62
+class TestPreprovisioningPollIMDS(CiTestCase):
222d62
+
222d62
+    def setUp(self):
222d62
+        super(TestPreprovisioningPollIMDS, self).setUp()
222d62
+        self.tmp = self.tmp_dir()
222d62
+        self.waagent_d = self.tmp_path('/var/lib/waagent', self.tmp)
222d62
+        self.paths = helpers.Paths({'cloud_dir': self.tmp})
222d62
+        dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
222d62
+
222d62
+    @mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
222d62
+    def test_poll_imds_calls_report_ready(self, write_f, report_ready_func,
222d62
+                                          fake_resp, m_dhcp, m_net):
222d62
+        """The poll_imds will call report_ready after creating marker file."""
222d62
+        report_marker = self.tmp_path('report_marker', self.tmp)
222d62
+        lease = {
222d62
+            'interface': 'eth9', 'fixed-address': '192.168.2.9',
222d62
+            'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
222d62
+            'unknown-245': '624c3620'}
222d62
+        m_dhcp.return_value = [lease]
222d62
+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
+        mock_path = (
222d62
+            'cloudinit.sources.DataSourceAzure.REPORTED_READY_MARKER_FILE')
222d62
+        with mock.patch(mock_path, report_marker):
222d62
+            dsa._poll_imds()
222d62
+        self.assertEqual(report_ready_func.call_count, 1)
222d62
+        report_ready_func.assert_called_with(lease=lease)
222d62
+
222d62
+    def test_poll_imds_report_ready_false(self, report_ready_func,
222d62
+                                          fake_resp, m_dhcp, m_net):
222d62
+        """The poll_imds should not call reporting ready
222d62
+           when flag is false"""
222d62
+        report_marker = self.tmp_path('report_marker', self.tmp)
222d62
+        write_file(report_marker, content='dont run report_ready :)')
222d62
+        m_dhcp.return_value = [{
222d62
+            'interface': 'eth9', 'fixed-address': '192.168.2.9',
222d62
+            'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
222d62
+            'unknown-245': '624c3620'}]
222d62
+        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
+        mock_path = (
222d62
+            'cloudinit.sources.DataSourceAzure.REPORTED_READY_MARKER_FILE')
222d62
+        with mock.patch(mock_path, report_marker):
222d62
+            dsa._poll_imds()
222d62
+        self.assertEqual(report_ready_func.call_count, 0)
222d62
+
222d62
+
222d62
+@mock.patch('cloudinit.sources.DataSourceAzure.util.subp')
222d62
+@mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
222d62
+@mock.patch('cloudinit.sources.DataSourceAzure.util.is_FreeBSD')
222d62
+@mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
222d62
+@mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
222d62
+@mock.patch('requests.Session.request')
222d62
+class TestAzureDataSourcePreprovisioning(CiTestCase):
222d62
+
222d62
+    def setUp(self):
222d62
+        super(TestAzureDataSourcePreprovisioning, self).setUp()
222d62
+        tmp = self.tmp_dir()
222d62
+        self.waagent_d = self.tmp_path('/var/lib/waagent', tmp)
222d62
+        self.paths = helpers.Paths({'cloud_dir': tmp})
222d62
+        dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
222d62
+
222d62
     def test_poll_imds_returns_ovf_env(self, fake_resp, m_dhcp, m_net,
222d62
-                                       m_is_bsd, *args):
222d62
+                                       m_is_bsd, write_f, subp):
222d62
         """The _poll_imds method should return the ovf_env.xml."""
222d62
         m_is_bsd.return_value = False
222d62
         m_dhcp.return_value = [{
222d62
@@ -1265,12 +1364,8 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
222d62
             prefix_or_mask='255.255.255.0', router='192.168.2.1')
222d62
         self.assertEqual(m_net.call_count, 1)
222d62
 
222d62
-    @mock.patch('cloudinit.sources.DataSourceAzure.util.is_FreeBSD')
222d62
-    @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
222d62
-    @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
222d62
-    @mock.patch('requests.Session.request')
222d62
     def test__reprovision_calls__poll_imds(self, fake_resp, m_dhcp, m_net,
222d62
-                                           m_is_bsd, *args):
222d62
+                                           m_is_bsd, write_f, subp):
222d62
         """The _reprovision method should call poll IMDS."""
222d62
         m_is_bsd.return_value = False
222d62
         m_dhcp.return_value = [{
222d62
@@ -1302,32 +1397,5 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
222d62
             prefix_or_mask='255.255.255.0', router='192.168.2.1')
222d62
         self.assertEqual(m_net.call_count, 1)
222d62
 
222d62
-    @mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
222d62
-    @mock.patch('os.path.isfile')
222d62
-    def test__should_reprovision_with_true_cfg(self, isfile, write_f, *args):
222d62
-        """The _should_reprovision method should return true with config
222d62
-           flag present."""
222d62
-        isfile.return_value = False
222d62
-        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
-        self.assertTrue(dsa._should_reprovision(
222d62
-            (None, None, {'PreprovisionedVm': True}, None)))
222d62
-
222d62
-    @mock.patch('os.path.isfile')
222d62
-    def test__should_reprovision_with_file_existing(self, isfile, *args):
222d62
-        """The _should_reprovision method should return True if the sentinal
222d62
-           exists."""
222d62
-        isfile.return_value = True
222d62
-        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
-        self.assertTrue(dsa._should_reprovision(
222d62
-            (None, None, {'preprovisionedvm': False}, None)))
222d62
-
222d62
-    @mock.patch('os.path.isfile')
222d62
-    def test__should_reprovision_returns_false(self, isfile, *args):
222d62
-        """The _should_reprovision method should return False
222d62
-           if config and sentinal are not present."""
222d62
-        isfile.return_value = False
222d62
-        dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
222d62
-        self.assertFalse(dsa._should_reprovision((None, None, {}, None)))
222d62
-
222d62
 
222d62
 # vi: ts=4 expandtab
222d62
-- 
222d62
1.8.3.1
222d62