Repository: Azure/WALinuxAgent
Pull request #707 (2017-05-10 17:54:13)

Emit event if reprovisioning without a deprovision (#706)
Emit even if reprovisioning without a deprovision (#706)
Correct enabling test agents
Blacklist agent due to any failure

Signed-off-by: Brendan Dixon <brendandixon@me.com>


Patch:
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index bbda0178..20dc1f30 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -30,13 +30,15 @@ import fcntl
 import base64
 import glob
 import datetime
+
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.conf as conf
-from azurelinuxagent.common.exception import OSUtilError
-from azurelinuxagent.common.future import ustr
 import azurelinuxagent.common.utils.fileutil as fileutil
 import azurelinuxagent.common.utils.shellutil as shellutil
 import azurelinuxagent.common.utils.textutil as textutil
+
+from azurelinuxagent.common.exception import OSUtilError
+from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.utils.cryptutil import CryptUtil
 
 __RULES_FILES__ = [ "/lib/udev/rules.d/75-persistent-net-generator.rules",
@@ -48,6 +50,12 @@ for all distros. Each concrete distro classes could overwrite default behavior
 if needed.
 """
 
+DMIDECODE_CMD = 'dmidecode --string system-uuid'
+PRODUCT_ID_FILE = '/sys/class/dmi/id/product_uuid'
+UUID_PATTERN = re.compile(
+    '^\s*[A-F0-9]{8}(?:\-[A-F0-9]{4}){3}\-[A-F0-9]{12}\s*$',
+    re.IGNORECASE)
+
 class DefaultOSUtil(object):
 
     def __init__(self):
@@ -58,6 +66,22 @@ class DefaultOSUtil(object):
     def get_agent_conf_file_path(self):
         return self.agent_conf_file_path
 
+    def get_instance_id(self):
+        '''
+        Azure records a UUID as the instance ID
+        First check /sys/class/dmi/id/product_uuid.
+        If that is missing, then extracts from dmidecode
+        If nothing works (for old VMs), return the empty string
+        '''
+        if os.path.isfile(PRODUCT_ID_FILE):
+            return fileutil.read_file(PRODUCT_ID_FILE).strip()
+
+        rc, s = shellutil.run_get_output(DMIDECODE_CMD)
+        if rc != 0 or UUID_PATTERN.match(s) is None:
+            return ""
+
+        return s.strip()
+
     def get_userentry(self, username):
         try:
             return pwd.getpwnam(username)
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index 735279ed..a8b47432 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -201,7 +201,7 @@ class UpdateHandler(object):
                     ret)
                 logger.warn(msg)
                 if latest_agent is not None:
-                    latest_agent.mark_failure()
+                    latest_agent.mark_failure(is_fatal=True)
 
         except Exception as e:
             msg = u"Agent {0} launched with command '{1}' failed with exception: {2}".format(
@@ -415,6 +415,7 @@ class UpdateHandler(object):
         host = None
         if protocol and protocol.client:
             host = protocol.client.get_host_plugin()
+
         self._set_agents([GuestAgent(pkg=pkg, host=host) for pkg in pkg_list.versions])
         self._purge_agents()
         self._filter_blacklisted_agents()
@@ -675,6 +676,7 @@ class GuestAgent(object):
     def enable(self):
         if self.error.is_sentinel:
             self.error.clear()
+            self.error.save()
         return
 
     @property
@@ -714,7 +716,7 @@ class GuestAgent(object):
             logger.verbose(u"Ensuring Agent {0} is downloaded", self.name)
 
             if self.is_blacklisted:
-                logger.warn(u"Agent {0} is blacklisted - skipping download", self.name)
+                logger.info(u"Agent {0} is blacklisted - skipping download", self.name)
                 return
 
             if self.is_downloaded:
diff --git a/azurelinuxagent/pa/provision/cloudinit.py b/azurelinuxagent/pa/provision/cloudinit.py
index b325f64d..48ec1b02 100644
--- a/azurelinuxagent/pa/provision/cloudinit.py
+++ b/azurelinuxagent/pa/provision/cloudinit.py
@@ -18,6 +18,7 @@
 #
 
 import os
+import os.path
 import time
 
 from datetime import datetime
@@ -25,6 +26,7 @@ from datetime import datetime
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.utils.fileutil as fileutil
+import azurelinuxagent.common.utils.shellutil as shellutil
 
 from azurelinuxagent.common.event import elapsed_milliseconds
 from azurelinuxagent.common.exception import ProvisionError, ProtocolError
@@ -46,31 +48,32 @@ class CloudInitProvisionHandler(ProvisionHandler):
             super(CloudInitProvisionHandler, self).run()
             return
 
-        provisioned = os.path.join(conf.get_lib_dir(), "provisioned")
-        if os.path.isfile(provisioned):
-            logger.info("Provisioning already completed, skipping.")
-            return
-
-        utc_start = datetime.utcnow()
-        logger.info("Running CloudInit provisioning handler")
-        self.wait_for_ovfenv()
-        self.protocol_util.get_protocol()
-        self.report_not_ready("Provisioning", "Starting")
         try:
+            if is_provisioned():
+                logger.info("Provisioning already completed, skipping.")
+                return
+
+            utc_start = datetime.utcnow()
+            logger.info("Running CloudInit provisioning handler")
+            self.wait_for_ovfenv()
+            self.protocol_util.get_protocol()
+            self.report_not_ready("Provisioning", "Starting")
+
             thumbprint = self.wait_for_ssh_host_key()
-            fileutil.write_file(provisioned, "")
+            self.write_provisioned()
             logger.info("Finished provisioning")
+
+            self.report_ready(thumbprint)
+            self.report_event("Provision succeed",
+                is_success=True,
+                duration=elapsed_milliseconds(utc_start))
+
         except ProvisionError as e:
             logger.error("Provisioning failed: {0}", ustr(e))
             self.report_not_ready("ProvisioningFailed", ustr(e))
             self.report_event(ustr(e))
             return
 
-        self.report_ready(thumbprint)
-        self.report_event("Provision succeed",
-            is_success=True,
-            duration=elapsed_milliseconds(utc_start))
-
     def wait_for_ovfenv(self, max_retry=360, sleep_time=5):
         """
         Wait for cloud-init to copy ovf-env.xml file from provision ISO
diff --git a/azurelinuxagent/pa/provision/default.py b/azurelinuxagent/pa/provision/default.py
index 36eab136..f6492be8 100644
--- a/azurelinuxagent/pa/provision/default.py
+++ b/azurelinuxagent/pa/provision/default.py
@@ -44,6 +44,8 @@ CUSTOM_DATA_FILE = "CustomData"
 CLOUD_INIT_PATTERN = b".*/bin/cloud-init.*"
 CLOUD_INIT_REGEX = re.compile(CLOUD_INIT_PATTERN)
 
+PROVISIONED_FILE = 'provisioned'
+
 
 class ProvisionHandler(object):
     def __init__(self):
@@ -51,44 +53,53 @@ class ProvisionHandler(object):
         self.protocol_util = get_protocol_util()
 
     def run(self):
-        # if provisioning is already done, return
-        provisioned = os.path.join(conf.get_lib_dir(), "provisioned")
-        if os.path.isfile(provisioned):
-            logger.info("Provisioning already completed, skipping.")
-            return
-
-        utc_start = datetime.utcnow()
-        thumbprint = None
         # If provision is not enabled, report ready and then return
         if not conf.get_provision_enabled():
             logger.info("Provisioning is disabled, skipping.")
-        else:
-            logger.info("Running default provisioning handler")
-            try:
-                if not self.validate_cloud_init(is_expected=False):
-                    raise ProvisionError("cloud-init appears to be running, "
-                                         "this is not expected, cannot continue")
-                logger.info("Copying ovf-env.xml")
-                ovf_env = self.protocol_util.copy_ovf_env()
-                self.protocol_util.get_protocol_by_file()
-                self.report_not_ready("Provisioning", "Starting")
-                logger.info("Starting provisioning")
-                self.provision(ovf_env)
-                thumbprint = self.reg_ssh_host_key()
-                self.osutil.restart_ssh_service()
-                self.report_event("Provision succeed",
-                    is_success=True,
-                    duration=elapsed_milliseconds(utc_start))
-            except (ProtocolError, ProvisionError) as e:
-                self.report_not_ready("ProvisioningFailed", ustr(e))
-                self.report_event(ustr(e))
-                logger.error("Provisioning failed: {0}", ustr(e))
+            return
+
+        try:
+            utc_start = datetime.utcnow()
+            thumbprint = None
+
+            # if provisioning is already done, return
+            if self.is_provisioned():
+                logger.info("Provisioning already completed, skipping.")
                 return
 
-        # write out provisioned file and report Ready
-        fileutil.write_file(provisioned, "")
-        self.report_ready(thumbprint)
-        logger.info("Provisioning complete")
+            logger.info("Running default provisioning handler")
+
+            if not self.validate_cloud_init(is_expected=False):
+                raise ProvisionError("cloud-init appears to be running, "
+                                        "this is not expected, cannot continue")
+
+            logger.info("Copying ovf-env.xml")
+            ovf_env = self.protocol_util.copy_ovf_env()
+
+            self.protocol_util.get_protocol_by_file()
+            self.report_not_ready("Provisioning", "Starting")
+            logger.info("Starting provisioning")
+
+            self.provision(ovf_env)
+
+            thumbprint = self.reg_ssh_host_key()
+            self.osutil.restart_ssh_service()
+
+            # write out provisioned file and report Ready
+            self.write_provisioned()
+
+            self.report_event("Provision succeed",
+                is_success=True,
+                duration=elapsed_milliseconds(utc_start))
+
+            self.report_ready(thumbprint)
+            logger.info("Provisioning complete")
+
+        except (ProtocolError, ProvisionError) as e:
+            self.report_not_ready("ProvisioningFailed", ustr(e))
+            self.report_event(ustr(e))
+            logger.error("Provisioning failed: {0}", ustr(e))
+            return
 
     @staticmethod
     def validate_cloud_init(is_expected=True):
@@ -128,6 +139,25 @@ class ProvisionHandler(object):
             raise ProvisionError(("Failed to generate ssh host key: "
                                   "ret={0}, out= {1}").format(ret[0], ret[1]))
 
+    def provisioned_file_path(self):
+        return os.path.join(conf.get_lib_dir(), PROVISIONED_FILE)
+
+    def is_provisioned(self):
+        if not os.path.isfile(self.provisioned_file_path()):
+            return False
+
+        s = fileutil.read_file(self.provisioned_file_path())
+        util = get_osutil()
+        if s == util.get_instance_id():
+            return True
+
+        raise ProtocolError("Provisioning new instance without a deprovision")
+
+    def write_provisioned(self):
+        fileutil.write_file(
+            self.provisioned_file_path(),
+            get_osutil().get_instance_id())
+
     def provision(self, ovfenv):
         logger.info("Handle ovf-env.xml.")
         try:


Test patch:
diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py
index 447b6fb3..87acc603 100644
--- a/tests/common/osutil/test_default.py
+++ b/tests/common/osutil/test_default.py
@@ -362,5 +362,37 @@ Match host 192.168.1.2\n\
                     conf.get_sshd_conf_file_path(),
                     expected_output)
 
+    @patch('os.path.isfile', return_value=True)
+    @patch('azurelinuxagent.common.utils.fileutil.read_file',
+            return_value="B9F3C233-9913-9F42-8EB3-BA656DF32502")
+    def test_get_instance_id_from_file(self, mock_read, mock_isfile):
+        util = osutil.DefaultOSUtil()
+        self.assertEqual(
+            "B9F3C233-9913-9F42-8EB3-BA656DF32502",
+            util.get_instance_id())
+
+    @patch('os.path.isfile', return_value=False)
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output',
+            return_value=[0, 'B9F3C233-9913-9F42-8EB3-BA656DF32502'])
+    def test_get_instance_id_from_dmidecode(self, mock_shell, mock_isfile):
+        util = osutil.DefaultOSUtil()
+        self.assertEqual(
+            "B9F3C233-9913-9F42-8EB3-BA656DF32502",
+            util.get_instance_id())
+
+    @patch('os.path.isfile', return_value=False)
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output',
+            return_value=[1, 'Error Value'])
+    def test_get_instance_id_missing(self, mock_shell, mock_isfile):
+        util = osutil.DefaultOSUtil()
+        self.assertEqual("", util.get_instance_id())
+
+    @patch('os.path.isfile', return_value=False)
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output',
+            return_value=[0, 'Unexpected Value'])
+    def test_get_instance_id_unexpected(self, mock_shell, mock_isfile):
+        util = osutil.DefaultOSUtil()
+        self.assertEqual("", util.get_instance_id())
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index cc9e7a89..13125108 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -528,6 +528,11 @@ class TestGuestAgent(UpdateTestCase):
         self.assertFalse(agent.is_blacklisted)
         self.assertFalse(agent.is_test)
 
+        # Ensure the new state is preserved to disk
+        agent = GuestAgent(path=self.agent_path)
+        self.assertFalse(agent.is_blacklisted)
+        self.assertFalse(agent.is_test)
+
     @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded")
     def test_mark_failure(self, mock_ensure):
         agent = GuestAgent(path=self.agent_path)
@@ -1280,7 +1285,8 @@ class TestUpdate(UpdateTestCase):
         with patch('azurelinuxagent.ga.update.UpdateHandler.get_latest_agent', return_value=latest_agent):
             self._test_run_latest(mock_child=ChildMock(return_value=1))
 
-        self.assertTrue(latest_agent.is_available)
+        self.assertTrue(latest_agent.is_blacklisted)
+        self.assertFalse(latest_agent.is_available)
         self.assertNotEqual(0.0, latest_agent.error.last_failure)
         self.assertEqual(1, latest_agent.error.failure_count)
         return
diff --git a/tests/pa/test_provision.py b/tests/pa/test_provision.py
index a98eacd0..72038959 100644
--- a/tests/pa/test_provision.py
+++ b/tests/pa/test_provision.py
@@ -16,17 +16,22 @@
 #
 
 import azurelinuxagent.common.utils.fileutil as fileutil
+
+from azurelinuxagent.common.exception import ProtocolError
 from azurelinuxagent.common.osutil.default import DefaultOSUtil
 from azurelinuxagent.common.protocol import OVF_FILE_NAME
 from azurelinuxagent.pa.provision import get_provision_handler
+from azurelinuxagent.pa.provision.default import ProvisionHandler
 from tests.tools import *
 
 
 class TestProvision(AgentTestCase):
  
     @distros("redhat")
-    def test_provision(self, distro_name, distro_version, distro_full_name):
-        provision_handler = get_provision_handler(distro_name, distro_version, 
+    @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.get_instance_id',
+        return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502')
+    def test_provision(self, mock_util, distro_name, distro_version, distro_full_name):
+        provision_handler = get_provision_handler(distro_name, distro_version,
                                                   distro_full_name)
         mock_osutil = MagicMock()
         mock_osutil.decode_customdata = Mock(return_value="")
@@ -48,6 +53,24 @@ class TestProvision(AgentTestCase):
         data = DefaultOSUtil().decode_customdata(base64data)
         fileutil.write_file(tempfile.mktemp(), data)
 
+    @patch('os.path.isfile', return_value=False)
+    def test_is_provisioned_not_provisioned(self, mock_isfile):
+        ph = ProvisionHandler()
+        self.assertFalse(ph.is_provisioned())
+
+    @patch('os.path.isfile', return_value=True)
+    @patch('azurelinuxagent.common.utils.fileutil.read_file',
+            return_value="B9F3C233-9913-9F42-8EB3-BA656DF32502")
+    def test_is_provisioned_is_provisioned(self, mock_read, mock_isfile):
+        ph = ProvisionHandler()
+        self.assertTrue(ph.is_provisioned())
+
+    @patch('os.path.isfile', return_value=True)
+    @patch('azurelinuxagent.common.utils.fileutil.read_file',
+            side_effect=["Value1", "Value2"])
+    def test_is_provisioned_not_deprovisioned(self, mock_read, mock_isfile):
+        ph = ProvisionHandler()
+        self.assertRaises(ProtocolError, ph.is_provisioned)
 
 if __name__ == '__main__':
     unittest.main()