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