Repository: Azure/WALinuxAgent
Pull request #1246 (2018-07-10 13:49:38)

Use fileutil.makedir consistently to avoid "No such file or directory" errors
If `/var/lib/waagent` weren't created manually, `waagent` continues to fail with "No such file or directory" error, because it can't create `/var/lib/waagent/events`. I suppose officially supported images alrady have the directory. But anyway I'veencounterd this error on Gentoo Linux installation.

This PR is to avoid such errors by using fileutil.makedir, which creates directories recursively. Its use is consistent with other parts of the codes.

msftclas: [![CLA assistant check](https://cla.opensource.microsoft.com/pull/badge/not_signed)](https://cla.opensource.microsoft.com/Azure/WALinuxAgent?pullRequest=1246) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla.opensource.microsoft.com/Azure/WALinuxAgent?pullRequest=1246) before we can accept your contribution.<br/><br/>:x: grafi-tt [sign now](https://cla.opensource.microsoft.com/Azure/WALinuxAgent?pullRequest=1246)<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla.opensource.microsoft.com/check/Azure/WALinuxAgent?pullRequest=1246) it.</sub>

Patch:
diff --git a/azurelinuxagent/common/dhcp.py b/azurelinuxagent/common/dhcp.py
index 84afeccc..70a072e4 100644
--- a/azurelinuxagent/common/dhcp.py
+++ b/azurelinuxagent/common/dhcp.py
@@ -150,6 +150,15 @@ class DhcpHandler(object):
         return None
 
     def send_dhcp_req(self):
+        """
+        Check if DHCP is available
+        """
+        (dhcp_available, endpoint) =  self.osutil.is_dhcp_available()
+        if not dhcp_available:
+            logger.info("send_dhcp_req: DHCP not available")
+            self.endpoint = endpoint
+            return
+
         """
         Build dhcp request with mac addr
         Configure route to allow dhcp traffic
diff --git a/azurelinuxagent/common/event.py b/azurelinuxagent/common/event.py
index 09117acf..71723d45 100644
--- a/azurelinuxagent/common/event.py
+++ b/azurelinuxagent/common/event.py
@@ -33,7 +33,7 @@ from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.protocol.restapi import TelemetryEventParam, \
     TelemetryEvent, \
     get_properties
-from azurelinuxagent.common.utils import textutil
+from azurelinuxagent.common.utils import fileutil, textutil
 from azurelinuxagent.common.version import CURRENT_VERSION
 
 _EVENT_MSG = "Event: name={0}, op={1}, message={2}, duration={3}"
@@ -200,9 +200,7 @@ class EventLogger(object):
             logger.warn("Cannot save event -- Event reporter is not initialized.")
             return
 
-        if not os.path.exists(self.event_dir):
-            os.mkdir(self.event_dir)
-            os.chmod(self.event_dir, 0o700)
+        fileutil.mkdir(self.event_dir, mode=0o700)
 
         existing_events = os.listdir(self.event_dir)
         if len(existing_events) >= 1000:
diff --git a/azurelinuxagent/common/exception.py b/azurelinuxagent/common/exception.py
index ef026fb0..46846560 100644
--- a/azurelinuxagent/common/exception.py
+++ b/azurelinuxagent/common/exception.py
@@ -56,8 +56,9 @@ class ExtensionError(AgentError):
     When failed to execute an extension
     """
 
-    def __init__(self, msg=None, inner=None):
+    def __init__(self, msg=None, inner=None, code=-1):
         super(ExtensionError, self).__init__(msg, inner)
+        self.code = code
 
 
 class ProvisionError(AgentError):
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 9667cdf2..6611424a 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -308,12 +308,12 @@ class DefaultOSUtil(object):
         '''
         if os.path.isfile(PRODUCT_ID_FILE):
             s = fileutil.read_file(PRODUCT_ID_FILE).strip()
-
+            
         else:
             rc, s = shellutil.run_get_output(DMIDECODE_CMD)
             if rc != 0 or UUID_PATTERN.match(s) is None:
                 return ""
-
+              
         return self._correct_instance_id(s.strip())
 
     def get_userentry(self, username):
@@ -390,12 +390,12 @@ class DefaultOSUtil(object):
         if not remove:
             # for older distros create sudoers.d
             if not os.path.isdir(sudoers_dir):
-                sudoers_file = os.path.join(sudoers_dir, '../sudoers')
                 # create the sudoers.d directory
-                os.mkdir(sudoers_dir)
+                fileutil.mkdir(sudoers_dir)
                 # add the include of sudoers.d to the /etc/sudoers
-                sudoers = '\n#includedir ' + sudoers_dir + '\n'
-                fileutil.append_file(sudoers_file, sudoers)
+                sudoers_file = os.path.join(sudoers_dir, os.pardir, 'sudoers')
+                include_sudoers_dir = "\n#includedir {0}\n".format(sudoers_dir)
+                fileutil.append_file(sudoers_file, include_sudoers_dir)
             sudoer = None
             if nopasswd:
                 sudoer = "{0} ALL=(ALL) NOPASSWD: ALL".format(username)
@@ -1012,6 +1012,9 @@ class DefaultOSUtil(object):
         shellutil.run("route del 255.255.255.255 dev {0}".format(ifname),
                       chk_err=False)
 
+    def is_dhcp_available(self):
+        return (True, '')
+
     def is_dhcp_enabled(self):
         return False
 
diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py
index 223c3a2b..480f2a81 100644
--- a/azurelinuxagent/common/osutil/factory.py
+++ b/azurelinuxagent/common/osutil/factory.py
@@ -105,6 +105,9 @@ def get_osutil(distro_name=DISTRO_NAME,
     elif distro_name == "gaia":
         return GaiaOSUtil()
 
+    if distro_name == "iosxe":
+        return IosxeOSUtil()
+
     else:
         logger.warn("Unable to load distro implementation for {0}. Using "
                     "default distro implementation instead.",
diff --git a/azurelinuxagent/common/osutil/iosxe.py b/azurelinuxagent/common/osutil/iosxe.py
new file mode 100644
index 00000000..3152ada8
--- /dev/null
+++ b/azurelinuxagent/common/osutil/iosxe.py
@@ -0,0 +1,84 @@
+# Microsoft Azure Linux Agent
+#
+# Copyright 2018 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.6+ and Openssl 1.0+
+#
+
+import azurelinuxagent.common.logger as logger
+import azurelinuxagent.common.utils.shellutil as shellutil
+from azurelinuxagent.common.osutil.default import DefaultOSUtil
+from azurelinuxagent.common.osutil.redhat import Redhat6xOSUtil
+
+'''
+The IOSXE distribution is a variant of the Centos distribution, 
+version 7.1.
+The primary difference is that IOSXE makes some assumptions about
+the waagent environment:
+ - only the waagent daemon is executed
+ - no provisioning is performed
+ - no DHCP-based services are available
+'''
+class IosxeOSUtil(DefaultOSUtil):
+    def __init__(self):
+        super(IosxeOSUtil, self).__init__()
+
+    def set_hostname(self, hostname):
+        """
+        Unlike redhat 6.x, redhat 7.x will set hostname via hostnamectl
+        Due to a bug in systemd in Centos-7.0, if this call fails, fallback
+        to hostname.
+        """
+        hostnamectl_cmd = "hostnamectl set-hostname {0} --static".format(hostname)
+        if shellutil.run(hostnamectl_cmd, chk_err=False) != 0:
+            logger.warn("[{0}] failed, attempting fallback".format(hostnamectl_cmd))
+            DefaultOSUtil.set_hostname(self, hostname)
+
+    def publish_hostname(self, hostname):
+        """
+        Restart NetworkManager first before publishing hostname
+        """
+        shellutil.run("service NetworkManager restart")
+        super(RedhatOSUtil, self).publish_hostname(hostname)
+
+    def register_agent_service(self):
+        return shellutil.run("systemctl enable waagent", chk_err=False)
+
+    def unregister_agent_service(self):
+        return shellutil.run("systemctl disable waagent", chk_err=False)
+
+    def openssl_to_openssh(self, input_file, output_file):
+        DefaultOSUtil.openssl_to_openssh(self, input_file, output_file)
+
+    def is_dhcp_available(self):
+        return (False, '168.63.129.16')
+
+    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):
+            try:
+                s = fileutil.read_file(PRODUCT_ID_FILE).strip()
+                return self._correct_instance_id(s.strip())
+            except IOError:
+                pass
+        rc, s = shellutil.run_get_output(DMIDECODE_CMD)
+        if rc != 0 or UUID_PATTERN.match(s) is None:
+            return ""
+        return self._correct_instance_id(s.strip())
diff --git a/azurelinuxagent/common/protocol/imds.py b/azurelinuxagent/common/protocol/imds.py
index a40e39da..d10bd2b7 100644
--- a/azurelinuxagent/common/protocol/imds.py
+++ b/azurelinuxagent/common/protocol/imds.py
@@ -297,11 +297,7 @@ class ImdsClient(object):
 
         # ensure all expected fields are present and have a value
         try:
-            self.check_field(json_data, 'compute')
-            self.check_field(json_data['compute'], 'location')
-            self.check_field(json_data['compute'], 'name')
-            self.check_field(json_data['compute'], 'subscriptionId')
-            self.check_field(json_data['compute'], 'vmSize')
+            # TODO: compute fields cannot be verified yet since we need to exclude rdfe vms (#1249)
 
             self.check_field(json_data, 'network')
             self.check_field(json_data['network'], 'interface')
diff --git a/azurelinuxagent/common/protocol/restapi.py b/azurelinuxagent/common/protocol/restapi.py
index 771006d1..00da1cde 100644
--- a/azurelinuxagent/common/protocol/restapi.py
+++ b/azurelinuxagent/common/protocol/restapi.py
@@ -161,8 +161,6 @@ class Extension(DataContract):
 class ExtHandlerProperties(DataContract):
     def __init__(self):
         self.version = None
-        self.upgradePolicy = None
-        self.upgradeGuid = None
         self.dependencyLevel = None
         self.state = None
         self.extensions = DataContractList(Extension)
@@ -257,13 +255,11 @@ class ExtHandlerStatus(DataContract):
     def __init__(self,
                  name=None,
                  version=None,
-                 upgradeGuid=None,
                  status=None,
                  code=0,
                  message=None):
         self.name = name
         self.version = version
-        self.upgradeGuid = upgradeGuid
         self.status = status
         self.code = code
         self.message = message
diff --git a/azurelinuxagent/common/protocol/util.py b/azurelinuxagent/common/protocol/util.py
index a3e3176f..b642b13e 100644
--- a/azurelinuxagent/common/protocol/util.py
+++ b/azurelinuxagent/common/protocol/util.py
@@ -146,13 +146,26 @@ class ProtocolUtil(object):
     def _detect_wire_protocol(self):
         endpoint = self.dhcp_handler.endpoint
         if endpoint is None:
-            logger.info("WireServer endpoint is not found. Rerun dhcp handler")
-            try:
-                self.dhcp_handler.run()
-            except DhcpError as e:
-                raise ProtocolError(ustr(e))
-            endpoint = self.dhcp_handler.endpoint
-        
+            '''
+            Check if DHCP can be used to get the wire protocol endpoint
+            '''
+            (dhcp_available, conf_endpoint) =  self.osutil.is_dhcp_available()
+            if dhcp_available:
+                logger.info("WireServer endpoint is not found. Rerun dhcp handler")
+                try:
+                    self.dhcp_handler.run()
+                except DhcpError as e:
+                    raise ProtocolError(ustr(e))
+                endpoint = self.dhcp_handler.endpoint
+            else:
+                logger.info("_detect_wire_protocol: DHCP not available")
+                endpoint = self._get_wireserver_endpoint()
+                if endpoint == None:
+                    endpoint = conf_endpoint
+                    logger.info("Using hardcoded WireServer endpoint {0}", endpoint)
+                else:
+                    logger.info("WireServer endpoint {0} read from file", endpoint)
+
         try:
             protocol = WireProtocol(endpoint)
             protocol.detect()
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index 60e0ba2b..14fbadb1 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -328,7 +328,8 @@ def ext_handler_status_to_v1(handler_status, ext_statuses, timestamp):
         'handlerVersion': handler_status.version,
         'handlerName': handler_status.name,
         'status': handler_status.status,
-        'code': handler_status.code
+        'code': handler_status.code,
+        'useExactVersion': True
     }
     if handler_status.message is not None:
         v1_handler_status["formattedMessage"] = {
@@ -336,9 +337,6 @@ def ext_handler_status_to_v1(handler_status, ext_statuses, timestamp):
             "message": handler_status.message
         }
 
-    if handler_status.upgradeGuid is not None:
-        v1_handler_status["upgradeGuid"] = handler_status.upgradeGuid
-
     if len(handler_status.extensions) > 0:
         # Currently, no more than one extension per handler
         ext_name = handler_status.extensions[0]
@@ -981,52 +979,53 @@ class WireClient(object):
             raise ProtocolNotFoundError(error)
 
     def upload_status_blob(self):
-        try:
-            self.update_goal_state()
-            ext_conf = self.get_ext_conf()
+        self.update_goal_state()
+        ext_conf = self.get_ext_conf()
 
-            blob_uri = ext_conf.status_upload_blob
-            blob_type = ext_conf.status_upload_blob_type
+        if ext_conf.status_upload_blob is None:
+            self.update_goal_state(forced=True)
+            ext_conf = self.get_ext_conf()
 
-            if blob_uri is None:
-                raise ProtocolError("No blob uri found")
+        if ext_conf.status_upload_blob is None:
+            raise ProtocolNotFoundError("Status upload uri is missing")
 
-            if blob_type not in ["BlockBlob", "PageBlob"]:
-                blob_type = "BlockBlob"
-                logger.verbose("Status Blob type is unspecified, assuming BlockBlob")
+        blob_type = ext_conf.status_upload_blob_type
+        if blob_type not in ["BlockBlob", "PageBlob"]:
+            blob_type = "BlockBlob"
+            logger.verbose("Status Blob type is unspecified, assuming BlockBlob")
 
-            try:
-                self.status_blob.prepare(blob_type)
-            except Exception as e:
-                raise ProtocolError("Exception creating status blob: {0}", ustr(e))
-
-            # Swap the order of use for the HostPlugin vs. the "direct" route.
-            # Prefer the use of HostPlugin. If HostPlugin fails fall back to the
-            # direct route.
-            #
-            # The code previously preferred the "direct" route always, and only fell back
-            # to the HostPlugin *if* there was an error.  We would like to move to
-            # the HostPlugin for all traffic, but this is a big change.  We would like
-            # to see how this behaves at scale, and have a fallback should things go
-            # wrong.  This is why we try HostPlugin then direct.
-            try:
-                host = self.get_host_plugin()
-                host.put_vm_status(self.status_blob,
-                                   ext_conf.status_upload_blob,
-                                   ext_conf.status_upload_blob_type)
-                return
-            except ResourceGoneError:
-                # do not attempt direct, force goal state update and wait to try again
-                self.update_goal_state(forced=True)
-                return
-            except Exception as e:
-                # for all other errors, fall back to direct
-                msg = "Falling back to direct upload: {0}".format(ustr(e))
-                self.report_status_event(msg, is_success=True)
+        try:
+            self.status_blob.prepare(blob_type)
+        except Exception as e:
+            raise ProtocolError("Exception creating status blob: {0}", ustr(e))
+
+        # Swap the order of use for the HostPlugin vs. the "direct" route.
+        # Prefer the use of HostPlugin. If HostPlugin fails fall back to the
+        # direct route.
+        #
+        # The code previously preferred the "direct" route always, and only fell back
+        # to the HostPlugin *if* there was an error.  We would like to move to
+        # the HostPlugin for all traffic, but this is a big change.  We would like
+        # to see how this behaves at scale, and have a fallback should things go
+        # wrong.  This is why we try HostPlugin then direct.
+        try:
+            host = self.get_host_plugin()
+            host.put_vm_status(self.status_blob,
+                               ext_conf.status_upload_blob,
+                               ext_conf.status_upload_blob_type)
+            return
+        except ResourceGoneError:
+            # do not attempt direct, force goal state update and wait to try again
+            self.update_goal_state(forced=True)
+            return
+        except Exception as e:
+            # for all other errors, fall back to direct
+            msg = "Falling back to direct upload: {0}".format(ustr(e))
+            self.report_status_event(msg, is_success=True)
 
-            if self.status_blob.upload(blob_uri):
+        try:
+            if self.status_blob.upload(ext_conf.status_upload_blob):
                 return
-
         except Exception as e:
             msg = "Exception uploading status blob: {0}".format(ustr(e))
             self.report_status_event(msg, is_success=False)
@@ -1567,21 +1566,11 @@ class ExtensionsConfig(object):
         ext_handler.properties.version = getattrib(plugin, "version")
         ext_handler.properties.state = getattrib(plugin, "state")
 
-        ext_handler.properties.upgradeGuid = getattrib(plugin, "upgradeGuid")
-        if not ext_handler.properties.upgradeGuid:
-            ext_handler.properties.upgradeGuid = None
-
         try:
             ext_handler.properties.dependencyLevel = int(getattrib(plugin, "dependencyLevel"))
         except ValueError:
             ext_handler.properties.dependencyLevel = 0
 
-        auto_upgrade = getattrib(plugin, "autoUpgrade")
-        if auto_upgrade is not None and auto_upgrade.lower() == "true":
-            ext_handler.properties.upgradePolicy = "auto"
-        else:
-            ext_handler.properties.upgradePolicy = "manual"
-
         location = getattrib(plugin, "location")
         failover_location = getattrib(plugin, "failoverlocation")
         for uri in [location, failover_location]:
diff --git a/azurelinuxagent/common/rdma.py b/azurelinuxagent/common/rdma.py
index 3c01e776..5504a068 100644
--- a/azurelinuxagent/common/rdma.py
+++ b/azurelinuxagent/common/rdma.py
@@ -84,6 +84,8 @@ class RDMAHandler(object):
         """Retrieve the firmware version information from the system.
            This depends on information provided by the Linux kernel."""
 
+        kvp_key_size = 512
+        kvp_value_size = 2048
         driver_info_source = '/var/lib/hyperv/.kvp_pool_0'
         base_kernel_err_msg = 'Kernel does not provide the necessary '
         base_kernel_err_msg += 'information or the kvp daemon is not running.'
@@ -93,21 +95,23 @@ class RDMAHandler(object):
             logger.error(error_msg % driver_info_source)
             return
 
-        lines = open(driver_info_source).read()
-        if not lines:
-            error_msg = 'RDMA: Source file "%s" is empty. '
-            error_msg += base_kernel_err_msg
-            logger.error(error_msg % driver_info_source)
-            return
-
-        r = re.search("NdDriverVersion\0+(\d\d\d\.\d)", lines)
-        if r:
-            NdDriverVersion = r.groups()[0]
-            return NdDriverVersion
-        else:
-            error_msg = 'RDMA: NdDriverVersion not found in "%s"'
-            logger.error(error_msg % driver_info_source)
-            return
+        f = open(driver_info_source)
+        while True :
+            key = f.read(kvp_key_size)
+            value = f.read(kvp_value_size)
+            if key and value :
+                key_0 = key.split("\x00")[0]
+                value_0 = value.split("\x00")[0]
+                if key_0 == "NdDriverVersion" :
+                    f.close()
+                    return value_0
+            else :
+                break
+        f.close()
+
+        error_msg = 'RDMA: NdDriverVersion not found in "%s"'
+        logger.error(error_msg % driver_info_source)
+        return
 
     @staticmethod
     def is_kvp_daemon_running():
diff --git a/azurelinuxagent/common/utils/flexible_version.py b/azurelinuxagent/common/utils/flexible_version.py
index 14c2a733..81202ac9 100644
--- a/azurelinuxagent/common/utils/flexible_version.py
+++ b/azurelinuxagent/common/utils/flexible_version.py
@@ -147,6 +147,18 @@ class FlexibleVersion(version.Version):
 
         return True
 
+    def matches(self, that):
+        if self.sep != that.sep or len(self.version) > len(that.version):
+            return False
+
+        for i in range(len(self.version)):
+            if self.version[i] != that.version[i]:
+                return False
+        if self.prerel_tags:
+            return self.prerel_tags == that.prerel_tags
+
+        return True
+
     def _assemble(self, version, sep, prerel_sep, prerelease):
         s = sep.join(map(str, version))
         if prerelease is not None:
diff --git a/azurelinuxagent/common/utils/processutil.py b/azurelinuxagent/common/utils/processutil.py
index fe9dd4a5..9c0eb24b 100644
--- a/azurelinuxagent/common/utils/processutil.py
+++ b/azurelinuxagent/common/utils/processutil.py
@@ -16,7 +16,7 @@
 #
 # Requires Python 2.6+ and Openssl 1.0+
 #
-
+import multiprocessing
 import subprocess
 import sys
 import os
@@ -100,75 +100,57 @@ def _destroy_process(process, signal_to_send=signal.SIGKILL):
         pass    # If the process is already gone, that's fine
 
 
-def capture_from_process_modern(process, cmd, timeout):
-    try:
-        stdout, stderr = process.communicate(timeout=timeout)
-    except subprocess.TimeoutExpired:
-        # Just kill the process. The .communicate method will gather stdout/stderr, close those pipes, and reap
-        # the zombie process. That is, .communicate() does all the other stuff that _destroy_process does.
+def capture_from_process_poll(process, cmd, timeout):
+    """
+    If the process forks, we cannot capture anything we block until the process tree completes
+    """
+    retry = timeout
+    while retry > 0 and process.poll() is None:
+        time.sleep(1)
+        retry -= 1
+
+    # process did not fork, timeout expired
+    if retry == 0:
         os.killpg(os.getpgid(process.pid), signal.SIGKILL)
         stdout, stderr = process.communicate()
         msg = format_stdout_stderr(sanitize(stdout), sanitize(stderr))
         raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, cmd, msg))
-    except OSError as e:
-        _destroy_process(process, signal.SIGKILL)
-        raise ExtensionError("Error while running '{0}': {1}".format(cmd, e.strerror))
-    except ValueError:
-        _destroy_process(process, signal.SIGKILL)
-        raise ExtensionError("Invalid timeout ({0}) specified for '{1}'".format(timeout, cmd))
-    except Exception as e:
-        _destroy_process(process, signal.SIGKILL)
-        raise ExtensionError("Exception while running '{0}': {1}".format(cmd, e))
 
-    return stdout, stderr
+    # process completed or forked
+    return_code = process.wait()
+    if return_code != 0:
+        raise ExtensionError("Non-zero exit code: {0}, {1}".format(return_code, cmd))
 
+    stderr = b''
+    stdout = b'cannot collect stdout'
 
-def capture_from_process_pre_33(process, cmd, timeout):
-    """
-    Can't use process.communicate(timeout=), so do it the hard way.
-    """
-    watcher_process_exited = 0
-    watcher_process_timed_out = 1
-
-    def kill_on_timeout(pid, watcher_timeout):
-        """
-        Check for the continued existence of pid once per second. If pid no longer exists, exit with code 0.
-        If timeout (in seconds) elapses, kill pid and exit with code 1.
-        """
-        for iteration in range(watcher_timeout):
-            time.sleep(1)
-            try:
-                os.kill(pid, 0)
-            except OSError as ex:
-                if ESRCH == ex.errno:   # Process no longer exists
-                    exit(watcher_process_exited)
-        os.killpg(os.getpgid(pid), signal.SIGKILL)
-        exit(watcher_process_timed_out)
-
-    watcher = Process(target=kill_on_timeout, args=(process.pid, timeout))
-    watcher.start()
+    # attempt non-blocking process communication to capture output
+    def proc_comm(_process, _return):
+        try:
+            _stdout, _stderr = _process.communicate()
+            _return[0] = _stdout
+            _return[1] = _stderr
+        except Exception:
+            pass
 
     try:
-        # Now, block "forever" waiting on process. If the timeout-limited Event wait in the watcher pops,
-        # it will kill the process and Popen.communicate() will return
-        stdout, stderr = process.communicate()
-    except OSError as e:
-        _destroy_process(process, signal.SIGKILL)
-        raise ExtensionError("Error while running '{0}': {1}".format(cmd, e.strerror))
-    except Exception as e:
-        _destroy_process(process, signal.SIGKILL)
-        raise ExtensionError("Exception while running '{0}': {1}".format(cmd, e))
+        mgr = multiprocessing.Manager()
+        ret_dict = mgr.dict()
 
-    timeout_happened = False
-    watcher.join(1)
-    if watcher.is_alive():
-        watcher.terminate()
-    else:
-        timeout_happened = (watcher.exitcode == watcher_process_timed_out)
+        cproc = Process(target=proc_comm, args=(process, ret_dict))
+        cproc.start()
 
-    if timeout_happened:
-        msg = format_stdout_stderr(sanitize(stdout), sanitize(stderr))
-        raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, cmd, msg))
+        # allow 1s to capture output
+        cproc.join(1)
+
+        if cproc.is_alive():
+            cproc.terminate()
+
+        stdout = ret_dict[0]
+        stderr = ret_dict[1]
+
+    except Exception:
+        pass
 
     return stdout, stderr
 
@@ -204,10 +186,7 @@ def capture_from_process_raw(process, cmd, timeout):
             _destroy_process(process, signal.SIGKILL)
             raise ExtensionError("Subprocess was not root of its own process group")
 
-        if sys.version_info < (3, 3):
-            stdout, stderr = capture_from_process_pre_33(process, cmd, timeout)
-        else:
-            stdout, stderr = capture_from_process_modern(process, cmd, timeout)
+        stdout, stderr = capture_from_process_poll(process, cmd, timeout)
 
     return stdout, stderr
 
diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py
index 4cb63280..9325edba 100644
--- a/azurelinuxagent/common/version.py
+++ b/azurelinuxagent/common/version.py
@@ -106,6 +106,9 @@ def get_distro():
     if os.path.exists("/etc/cp-release"):
         osinfo = get_checkpoint_platform()
 
+    if os.path.exists("/home/guestshell/azure"):
+        osinfo = ['iosxe', 'csr1000v', '', 'Cisco IOSXE Linux']
+
     # Remove trailing whitespace and quote in distro name
     osinfo[0] = osinfo[0].strip('"').strip(' ').lower()
     return osinfo
@@ -113,7 +116,7 @@ def get_distro():
 
 AGENT_NAME = "WALinuxAgent"
 AGENT_LONG_NAME = "Azure Linux Agent"
-AGENT_VERSION = '2.2.27'
+AGENT_VERSION = '2.2.30'
 AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION)
 AGENT_DESCRIPTION = """
 The Azure Linux Agent supports the provisioning and running of Linux
diff --git a/azurelinuxagent/daemon/main.py b/azurelinuxagent/daemon/main.py
index f09a1bb9..2e0453e8 100644
--- a/azurelinuxagent/daemon/main.py
+++ b/azurelinuxagent/daemon/main.py
@@ -66,6 +66,8 @@ class DaemonHandler(object):
                     PY_VERSION_MICRO)
 
         self.check_pid()
+        self.initialize_environment()
+
         CGroups.setup()
 
         # If FIPS is enabled, set the OpenSSL environment variable
@@ -97,6 +99,12 @@ class DaemonHandler(object):
 
         fileutil.write_file(pid_file, ustr(os.getpid()))
 
+    def initialize_environment(self):
+        # Create lib dir
+        if not os.path.isdir(conf.get_lib_dir()):
+            fileutil.mkdir(conf.get_lib_dir(), mode=0o700)
+            os.chdir(conf.get_lib_dir())
+
     def daemon(self, child_args=None):
         logger.info("Run daemon")
 
@@ -107,11 +115,6 @@ class DaemonHandler(object):
         self.provision_handler = get_provision_handler()
         self.update_handler = get_update_handler()
 
-        # Create lib dir
-        if not os.path.isdir(conf.get_lib_dir()):
-            fileutil.mkdir(conf.get_lib_dir(), mode=0o700)
-            os.chdir(conf.get_lib_dir())
-
         if conf.get_detect_scvmm_env():
             self.scvmm_handler.run()
 
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index 986a7ca4..a66aacbd 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -39,7 +39,7 @@ import azurelinuxagent.common.version as version
 from azurelinuxagent.common.cgroups import CGroups, CGroupsTelemetry
 from azurelinuxagent.common.errorstate import ErrorState, ERROR_STATE_DELTA_DEFAULT, ERROR_STATE_DELTA_INSTALL
 from azurelinuxagent.common.event import add_event, WALAEventOperation, elapsed_milliseconds, report_event
-from azurelinuxagent.common.exception import ExtensionError, ProtocolError
+from azurelinuxagent.common.exception import ExtensionError, ProtocolError, ProtocolNotFoundError
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.protocol.restapi import ExtHandlerStatus, \
                                                     ExtensionStatus, \
@@ -185,7 +185,6 @@ class ExtHandlersHandler(object):
         self.protocol = None
         self.ext_handlers = None
         self.last_etag = None
-        self.last_upgrade_guids = {}
         self.log_report = False
         self.log_etag = True
         self.log_process = False
@@ -236,23 +235,6 @@ class ExtHandlersHandler(object):
                       message=msg)
             return
 
-    def get_upgrade_guid(self, name):
-        return self.last_upgrade_guids.get(name, (None, False))[0]
-
-    def get_log_upgrade_guid(self, ext_handler):
-        return self.last_upgrade_guids.get(ext_handler.name, (None, False))[1]
-
-    def set_log_upgrade_guid(self, ext_handler, log_val):
-        guid = self.get_upgrade_guid(ext_handler.name)
-        if guid is not None:
-            self.last_upgrade_guids[ext_handler.name] = (guid, log_val)
-
-    def is_new_guid(self, ext_handler):
-        last_guid = self.get_upgrade_guid(ext_handler.name)
-        if last_guid is None:
-            return True
-        return last_guid != ext_handler.properties.upgradeGuid
-
     def cleanup_outdated_handlers(self):
         handlers = []
         pkgs = []
@@ -333,28 +315,22 @@ class ExtHandlersHandler(object):
 
         try:
             state = ext_handler.properties.state
-            # The extension is to be enabled, there is an upgrade GUID
-            # and the GUID is NOT new
-            if state == u"enabled" and \
-                    ext_handler.properties.upgradeGuid is not None and \
-                    not self.is_new_guid(ext_handler):
-                ext_handler_i.ext_handler.properties.version = ext_handler_i.get_installed_version()
-                ext_handler_i.set_logger()
-                if self.last_etag != etag:
-                    self.set_log_upgrade_guid(ext_handler, True)
-
-                msg = "New GUID is the same as the old GUID. Exiting without upgrading."
-                if self.get_log_upgrade_guid(ext_handler):
-                    ext_handler_i.logger.info(msg)
-                    self.set_log_upgrade_guid(ext_handler, False)
-                ext_handler_i.set_handler_state(ExtHandlerState.Enabled)
-                ext_handler_i.set_handler_status(status="Ready", message="No change")
-                ext_handler_i.set_operation(WALAEventOperation.SkipUpdate)
-                ext_handler_i.report_event(message=ustr(msg), is_success=True)
-                return
-
-            self.set_log_upgrade_guid(ext_handler, True)
-            ext_handler_i.decide_version(target_state=state)
+            max_retries = 5
+            retry_count = 0
+            while ext_handler_i.decide_version(target_state=state) is None:
+                if retry_count >= max_retries:
+                    err_msg = "Unable to find manifest for extension {0}".format(ext_handler_i.ext_handler.name)
+                    ext_handler_i.set_operation(WALAEventOperation.Download)
+                    ext_handler_i.set_handler_status(message=ustr(err_msg), code=-1)
+                    ext_handler_i.report_event(message=ustr(err_msg), is_success=False)
+                    return
+                time.sleep(2**retry_count * 10)
+                retry_count += 1
+            if retry_count != 0:
+                ext_handler_i.set_operation(WALAEventOperation.Download)
+                err_msg = "Able to find manifest for extension {0} after {1} failed attempts.".format(
+                    ext_handler_i.ext_handler.name, retry_count)
+                ext_handler_i.report_event(message=ustr(err_msg))
             self.get_artifact_error_state.reset()
             if not ext_handler_i.is_upgrade and self.last_etag == etag:
                 if self.log_etag:
@@ -369,44 +345,35 @@ class ExtHandlersHandler(object):
             ext_handler_i.logger.info("Target handler state: {0}", state)
             if state == u"enabled":
                 self.handle_enable(ext_handler_i)
-                if ext_handler.properties.upgradeGuid is not None:
-                    ext_handler_i.logger.info("New Upgrade GUID: {0}", ext_handler.properties.upgradeGuid)
-                    self.last_upgrade_guids[ext_handler.name] = (ext_handler.properties.upgradeGuid, True)
             elif state == u"disabled":
                 self.handle_disable(ext_handler_i)
-                # Remove the GUID from the dictionary so that it is upgraded upon re-enable
-                self.last_upgrade_guids.pop(ext_handler.name, None)
             elif state == u"uninstall":
                 self.handle_uninstall(ext_handler_i)
-                # Remove the GUID from the dictionary so that it is upgraded upon re-install
-                self.last_upgrade_guids.pop(ext_handler.name, None)
             else:
                 message = u"Unknown ext handler state:{0}".format(state)
                 raise ExtensionError(message)
+        except ExtensionError as e:
+            self.handle_handle_ext_handler_error(ext_handler_i, e, e.code)
         except Exception as e:
-            msg = ustr(e)
-            ext_handler_i.set_handler_status(message=msg, code=-1)
-
-            self.get_artifact_error_state.incr()
-            if self.get_artifact_error_state.is_triggered():
-                report_event(op=WALAEventOperation.GetArtifactExtended,
-                             message="Failed to get artifact for over "
-                                     "{0}: {1}".format(self.get_artifact_error_state.min_timedelta, msg),
-                             is_success=False)
-                self.get_artifact_error_state.reset()
-            else:
-                ext_handler_i.logger.warn(msg)
+            self.handle_handle_ext_handler_error(ext_handler_i, e)
+
+    def handle_handle_ext_handler_error(self, ext_handler_i, e, code=-1):
+        msg = ustr(e)
+        ext_handler_i.set_handler_status(message=msg, code=code)
+
+        self.get_artifact_error_state.incr()
+        if self.get_artifact_error_state.is_triggered():
+            report_event(op=WALAEventOperation.GetArtifactExtended,
+                         message="Failed to get artifact for over "
+                                 "{0}: {1}".format(self.get_artifact_error_state.min_timedelta, msg),
+                         is_success=False)
+            self.get_artifact_error_state.reset()
+        else:
+            ext_handler_i.logger.warn(msg)
     
     def handle_enable(self, ext_handler_i):
         self.log_process = True
         old_ext_handler_i = ext_handler_i.get_installed_ext_handler()
-        if old_ext_handler_i is not None and \
-           old_ext_handler_i.version_gt(ext_handler_i):
-            msg = "Downgrade is not allowed. Skipping install and enable."
-            ext_handler_i.logger.error(msg)
-            ext_handler_i.set_operation(WALAEventOperation.Downgrade)
-            ext_handler_i.report_event(message=ustr(msg), is_success=True)
-            return
 
         handler_state = ext_handler_i.get_handler_state()
         ext_handler_i.logger.info("[Enable] current handler state is: {0}",
@@ -417,10 +384,13 @@ class ExtHandlersHandler(object):
             ext_handler_i.update_settings()
             if old_ext_handler_i is None:
                 ext_handler_i.install()
-            elif ext_handler_i.version_gt(old_ext_handler_i):
+            elif ext_handler_i.version_ne(old_ext_handler_i):
                 old_ext_handler_i.disable()
                 ext_handler_i.copy_status_files(old_ext_handler_i)
-                ext_handler_i.update()
+                if ext_handler_i.version_gt(old_ext_handler_i):
+                    ext_handler_i.update()
+                else:
+                    old_ext_handler_i.update(version=ext_handler_i.ext_handler.properties.version)
                 old_ext_handler_i.uninstall()
                 old_ext_handler_i.rm_ext_handler_dir()
                 ext_handler_i.update_with_install()
@@ -471,24 +441,28 @@ class ExtHandlersHandler(object):
             if self.log_report:
                 logger.verbose("Completed vm agent status report")
             self.report_status_error_state.reset()
+        except ProtocolNotFoundError as e:
+            self.report_status_error_state.incr()
+            message = "Failed to report vm agent status: {0}".format(e)
+            logger.verbose(message)
         except ProtocolError as e:
             self.report_status_error_state.incr()
             message = "Failed to report vm agent status: {0}".format(e)
             add_event(AGENT_NAME,
-                version=CURRENT_VERSION,
-                op=WALAEventOperation.ExtensionProcessing,
-                is_success=False,
-                message=message)
+                      version=CURRENT_VERSION,
+                      op=WALAEventOperation.ExtensionProcessing,
+                      is_success=False,
+                      message=message)
 
         if self.report_status_error_state.is_triggered():
             message = "Failed to report vm agent status for more than {0}"\
                 .format(self.report_status_error_state.min_timedelta)
 
             add_event(AGENT_NAME,
-                version=CURRENT_VERSION,
-                op=WALAEventOperation.ReportStatusExtended,
-                is_success=False,
-                message=message)
+                      version=CURRENT_VERSION,
+                      op=WALAEventOperation.ReportStatusExtended,
+                      is_success=False,
+                      message=message)
 
             self.report_status_error_state.reset()
 
@@ -498,9 +472,6 @@ class ExtHandlersHandler(object):
         handler_status = ext_handler_i.get_handler_status() 
         if handler_status is None:
             return
-        guid = self.get_upgrade_guid(ext_handler.name)
-        if guid is not None:
-            handler_status.upgradeGuid = guid
 
         handler_state = ext_handler_i.get_handler_state()
         if handler_state != ExtHandlerState.NotInstalled:
@@ -558,74 +529,21 @@ class ExtHandlerInstance(object):
         # - Find the installed package (its version must exactly match)
         # - Find the internal candidate (its version must exactly match)
         # - Separate the public packages
-        internal_pkg = None
+        selected_pkg = None
         installed_pkg = None
-        public_pkgs = []
+        pkg_list.versions.sort(key=lambda p: FlexibleVersion(p.version))
         for pkg in pkg_list.versions:
             pkg_version = FlexibleVersion(pkg.version)
             if pkg_version == installed_version:
                 installed_pkg = pkg
-            if pkg.isinternal and pkg_version == requested_version:
-                internal_pkg = pkg
-            if not pkg.isinternal:
-                public_pkgs.append(pkg)
-
-        internal_version = FlexibleVersion(internal_pkg.version) \
-                            if internal_pkg is not None \
-                            else FlexibleVersion()
-        public_pkgs.sort(key=lambda p: FlexibleVersion(p.version), reverse=True)
-
-        # Determine the preferred version and type of upgrade occurring
-        preferred_version = max(requested_version, installed_version)
-        is_major_upgrade = preferred_version.major > installed_version.major
-        allow_minor_upgrade = self.ext_handler.properties.upgradePolicy == 'auto'
-
-        # Find the first public candidate which
-        # - Matches the preferred major version
-        # - Does not upgrade to a new, disallowed major version
-        # - And only increments the minor version if allowed
-        # Notes:
-        # - The patch / hotfix version is not considered
-        public_pkg = None
-        for pkg in public_pkgs:
-            pkg_version = FlexibleVersion(pkg.version)
-            if pkg_version.major == preferred_version.major \
-                    and (not pkg.disallow_major_upgrade or not is_major_upgrade) \
-                    and (allow_minor_upgrade or pkg_version.minor == preferred_version.minor):
-                public_pkg = pkg
-                break
-
-        # If there are no candidates, locate the highest public version whose
-        # major matches that installed
-        if internal_pkg is None and public_pkg is None:
-            for pkg in public_pkgs:
-                pkg_version = FlexibleVersion(pkg.version)
-                if pkg_version.major == installed_version.major:
-                    public_pkg = pkg
-                    break
-
-        public_version = FlexibleVersion(public_pkg.version) \
-                            if public_pkg is not None \
-                            else FlexibleVersion()
-
-        # Select the candidate
-        # - Use the public candidate if there is no internal candidate or
-        #   the public is more recent (e.g., a hotfix patch)
-        # - Otherwise use the internal candidate
-        if internal_pkg is None or (public_pkg is not None and public_version > internal_version):
-            selected_pkg = public_pkg
-        else:
-            selected_pkg = internal_pkg
-
-        selected_version = FlexibleVersion(selected_pkg.version) \
-                            if selected_pkg is not None \
-                            else FlexibleVersion()
+            if requested_version.matches(pkg_version):
+                selected_pkg = pkg
 
         # Finally, update the version only if not downgrading
         # Note:
         #  - A downgrade, which will be bound to the same major version,
         #    is allowed if the installed version is no longer available
-        if target_state == u"uninstall":
+        if target_state == u"uninstall" or target_state == u"disabled":
             if installed_pkg is None:
                 msg = "Failed to find installed version of {0} " \
                     "to uninstall".format(self.ext_handler.name)
@@ -633,26 +551,20 @@ class ExtHandlerInstance(object):
             self.pkg = installed_pkg
             self.ext_handler.properties.version = str(installed_version) \
                                     if installed_version is not None else None
-        elif selected_pkg is None \
-                or (installed_pkg is not None and selected_version < installed_version):
-            self.pkg = installed_pkg
-            self.ext_handler.properties.version = str(installed_version) \
-                                    if installed_version is not None else None
         else:
             self.pkg = selected_pkg
-            self.ext_handler.properties.version = str(selected_pkg.version)
+            if self.pkg is not None:
+                self.ext_handler.properties.version = str(selected_pkg.version)
 
-        # Note if the selected package is greater than that installed
+        # Note if the selected package is different than that installed
         if installed_pkg is None \
-                or FlexibleVersion(self.pkg.version) > FlexibleVersion(installed_pkg.version):
+                or (self.pkg is not None and FlexibleVersion(self.pkg.version) != FlexibleVersion(installed_pkg.version)):
             self.is_upgrade = True
 
-        if self.pkg is None:
-            raise ExtensionError("Failed to find any valid extension package")
-
-        self.logger.verbose("Use version: {0}", self.pkg.version)
+        if self.pkg is not None:
+            self.logger.verbose("Use version: {0}", self.pkg.version)
         self.set_logger()
-        return
+        return self.pkg
 
     def set_logger(self):
         prefix = "[{0}]".format(self.get_full_name())
@@ -663,6 +575,11 @@ class ExtHandlerInstance(object):
         other_version = other.ext_handler.properties.version
         return FlexibleVersion(self_version) > FlexibleVersion(other_version)
 
+    def version_ne(self, other):
+        self_version = self.ext_handler.properties.version
+        other_version = other.ext_handler.properties.version
+        return FlexibleVersion(self_version) != FlexibleVersion(other_version)
+
     def get_installed_ext_handler(self):
         lastest_version = self.get_installed_version()
         if lastest_version is None:
@@ -815,7 +732,7 @@ class ExtHandlerInstance(object):
         man = self.load_manifest()
         enable_cmd = man.get_enable_command()
         self.logger.info("Enable extension [{0}]".format(enable_cmd))
-        self.launch_command(enable_cmd, timeout=300)
+        self.launch_command(enable_cmd, timeout=300, extension_error_code=1009)
         self.set_handler_state(ExtHandlerState.Enabled)
         self.set_handler_status(status="Ready", message="Plugin enabled")
 
@@ -824,7 +741,8 @@ class ExtHandlerInstance(object):
         man = self.load_manifest()
         disable_cmd = man.get_disable_command()
         self.logger.info("Disable extension [{0}]".format(disable_cmd))
-        self.launch_command(disable_cmd, timeout=900)
+        self.launch_command(disable_cmd, timeout=900,
+                            extension_error_code=1010)
         self.set_handler_state(ExtHandlerState.Installed)
         self.set_handler_status(status="NotReady", message="Plugin disabled")
 
@@ -833,7 +751,7 @@ class ExtHandlerInstance(object):
         install_cmd = man.get_install_command()
         self.logger.info("Install extension [{0}]".format(install_cmd))
         self.set_operation(WALAEventOperation.Install)
-        self.launch_command(install_cmd, timeout=900)
+        self.launch_command(install_cmd, timeout=900, extension_error_code=1007)
         self.set_handler_state(ExtHandlerState.Installed)
 
     def uninstall(self):
@@ -858,12 +776,18 @@ class ExtHandlerInstance(object):
             self.report_event(message=message, is_success=False)
             self.logger.warn(message)
 
-    def update(self):
+    def update(self, version=None):
+        if version is None:
+            version = self.ext_handler.properties.version
+
         self.set_operation(WALAEventOperation.Update)
         man = self.load_manifest()
         update_cmd = man.get_update_command()
         self.logger.info("Update extension [{0}]".format(update_cmd))
-        self.launch_command(update_cmd, timeout=900)
+        self.launch_command(update_cmd, 
+                            timeout=900,
+                            extension_error_code=1008, 
+                            env={'VERSION': version})
     
     def update_with_install(self):
         man = self.load_manifest()
@@ -976,11 +900,15 @@ class ExtHandlerInstance(object):
         last_update = int(time.time() - os.stat(heartbeat_file).st_mtime)
         return last_update <= 600
 
-    def launch_command(self, cmd, timeout=300):
+    def launch_command(self, cmd, timeout=300, extension_error_code=-1, env=None):
         begin_utc = datetime.datetime.utcnow()
         self.logger.verbose("Launch command: [{0}]", cmd)
         base_dir = self.get_base_dir()
 
+        if env is None:
+            env = {}
+        env.update(os.environ)
+
         try:
             # This should be .run(), but due to the wide variety
             # of Python versions we must support we must use .communicate().
@@ -998,22 +926,27 @@ class ExtHandlerInstance(object):
                 CGroups.add_to_extension_cgroup(self.ext_handler.name)
 
             process = subprocess.Popen(full_path,
-                                  shell=True,
-                                  cwd=base_dir,
-                                  stdout=subprocess.PIPE,
-                                  stderr=subprocess.PIPE,
-                                  env=os.environ,
-                                  preexec_fn=pre_exec_function)
+                                       shell=True,
+                                       cwd=base_dir,
+                                       stdout=subprocess.PIPE,
+                                       stderr=subprocess.PIPE,
+                                       env=env,
+                                       preexec_fn=pre_exec_function)
         except OSError as e:
-            raise ExtensionError("Failed to launch '{0}': {1}".format(full_path, e.strerror))
+            raise ExtensionError("Failed to launch '{0}': {1}".format(full_path, e.strerror),
+                                 code=extension_error_code)
 
         cg = CGroups.for_extension(self.ext_handler.name)
         CGroupsTelemetry.track_extension(self.ext_handler.name, cg)
         msg = capture_from_process(process, cmd, timeout)
 
         ret = process.poll()
-        if ret is None or ret != 0:
-            raise ExtensionError("Non-zero exit code: {0}, {1}\n{2}".format(ret, cmd, msg))
+        if ret is None:
+            raise ExtensionError("Process {0} was not terminated: {1}\n{2}".format(process.pid, cmd, msg),
+                                 code=extension_error_code)
+        if ret != 0:
+            raise ExtensionError("Non-zero exit code: {0}, {1}\n{2}".format(ret, cmd, msg),
+                                 code=extension_error_code)
 
         duration = elapsed_milliseconds(begin_utc)
         self.report_event(message="{0}\n{1}".format(cmd, msg), duration=duration, log_event=False)
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index 36062b4a..4292fc26 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -177,7 +177,11 @@ class UpdateHandler(object):
             start_time = time.time()
             while (time.time() - start_time) < CHILD_HEALTH_INTERVAL:
                 time.sleep(poll_interval)
-                ret = self.child_process.poll()
+                try:
+                    ret = self.child_process.poll()
+                except OSError:
+                    # if child_process has terminated, calling poll could raise an exception
+                    ret = -1
                 if ret is not None:
                     break
 
@@ -227,12 +231,13 @@ class UpdateHandler(object):
                     agent_cmd,
                     ustr(e))
                 logger.warn(msg)
+                detailed_message = '{0} {1}'.format(msg, traceback.format_exc())
                 add_event(
                     AGENT_NAME,
                     version=agent_version,
                     op=WALAEventOperation.Enable,
                     is_success=False,
-                    message=msg)
+                    message=detailed_message)
                 if latest_agent is not None:
                     latest_agent.mark_failure(is_fatal=True)
 
diff --git a/config/alpine/waagent.conf b/config/alpine/waagent.conf
index dbe9b882..5c620e70 100644
--- a/config/alpine/waagent.conf
+++ b/config/alpine/waagent.conf
@@ -88,6 +88,4 @@ OS.SshDir=/etc/ssh
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/arch/waagent.conf b/config/arch/waagent.conf
index b45d2e12..f5dd6ce1 100644
--- a/config/arch/waagent.conf
+++ b/config/arch/waagent.conf
@@ -114,6 +114,4 @@ OS.SshDir=/etc/ssh
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/bigip/waagent.conf b/config/bigip/waagent.conf
index 28828986..501292e5 100644
--- a/config/bigip/waagent.conf
+++ b/config/bigip/waagent.conf
@@ -92,6 +92,4 @@ AutoUpdate.Enabled=y
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/coreos/waagent.conf b/config/coreos/waagent.conf
index c6683b06..a58c9985 100644
--- a/config/coreos/waagent.conf
+++ b/config/coreos/waagent.conf
@@ -114,6 +114,4 @@ OS.OpensslPath=None
 OS.AllowHTTP=y
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/freebsd/waagent.conf b/config/freebsd/waagent.conf
index c8a59c5b..6e611d10 100644
--- a/config/freebsd/waagent.conf
+++ b/config/freebsd/waagent.conf
@@ -112,6 +112,4 @@ OS.SudoersDir=/usr/local/etc/sudoers.d
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/gaia/waagent.conf b/config/gaia/waagent.conf
index b2cda75d..168beda0 100644
--- a/config/gaia/waagent.conf
+++ b/config/gaia/waagent.conf
@@ -111,6 +111,4 @@ AutoUpdate.Enabled=n
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/iosxe/waagent.conf b/config/iosxe/waagent.conf
new file mode 100644
index 00000000..1c36def6
--- /dev/null
+++ b/config/iosxe/waagent.conf
@@ -0,0 +1,119 @@
+#
+# Microsoft Azure Linux Agent Configuration
+#
+
+# Enable instance creation
+Provisioning.Enabled=n
+
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
+# Password authentication for root account will be unavailable.
+Provisioning.DeleteRootPassword=y
+
+# Generate fresh host key pair.
+Provisioning.RegenerateSshHostKeyPair=n
+
+# Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto".
+# The "auto" option is supported on OpenSSH 5.9 (2011) and later.
+Provisioning.SshHostKeyPairType=rsa
+
+# Monitor host name changes and publish changes via DHCP requests.
+Provisioning.MonitorHostName=n
+
+# Decode CustomData from Base64.
+Provisioning.DecodeCustomData=n
+
+# Execute CustomData after provisioning.
+Provisioning.ExecuteCustomData=n
+
+# Algorithm used by crypt when generating password hash.
+#Provisioning.PasswordCryptId=6
+
+# Length of random salt used when generating password hash.
+#Provisioning.PasswordCryptSaltLength=10
+
+# Allow reset password of sys user
+Provisioning.AllowResetSysUser=n
+
+# Format if unformatted. If 'n', resource disk will not be mounted.
+ResourceDisk.Format=n
+
+# File system on the resource disk
+# Typically ext3 or ext4. FreeBSD images should use 'ufs2' here.
+ResourceDisk.Filesystem=ext4
+
+# Mount point for the resource disk
+ResourceDisk.MountPoint=/mnt/resource
+
+# Create and use swapfile on resource disk.
+ResourceDisk.EnableSwap=n
+
+# Size of the swapfile.
+ResourceDisk.SwapSizeMB=0
+
+# Comma-seperated list of mount options. See man(8) for valid options.
+ResourceDisk.MountOptions=None
+
+# Enable verbose logging (y|n)
+Logs.Verbose=n
+
+# Is FIPS enabled
+OS.EnableFIPS=n
+
+# Root device timeout in seconds.
+OS.RootDeviceScsiTimeout=300
+
+# If "None", the system default version is used.
+OS.OpensslPath=None
+
+# Set the SSH ClientAliveInterval
+# OS.SshClientAliveInterval=180
+
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
+# If set, agent will use proxy server to access internet
+#HttpProxy.Host=None
+#HttpProxy.Port=None
+
+# Detect Scvmm environment, default is n
+# DetectScvmmEnv=n
+
+#
+# Lib.Dir=/var/lib/waagent
+
+#
+# DVD.MountPoint=/mnt/cdrom/secure
+
+#
+# Pid.File=/var/run/waagent.pid
+
+#
+# Extension.LogDir=/var/log/azure
+
+#
+# Home.Dir=/home
+
+# Enable RDMA management and set up, should only be used in HPC images
+# OS.EnableRDMA=y
+
+# Enable or disable goal state processing auto-update, default is enabled
+AutoUpdate.Enabled=y
+
+# Determine the update family, this should not be changed
+# AutoUpdate.GAFamily=Prod
+
+# Determine if the overprovisioning feature is enabled. If yes, hold extension
+# handling until inVMArtifactsProfile.OnHold is false.
+# Default is enabled
+# EnableOverProvisioning=y
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of existing VMs
+OS.EnableFirewall=y
diff --git a/config/suse/waagent.conf b/config/suse/waagent.conf
index ed650f92..2a514584 100644
--- a/config/suse/waagent.conf
+++ b/config/suse/waagent.conf
@@ -114,6 +114,4 @@ OS.SshDir=/etc/ssh
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/ubuntu/waagent.conf b/config/ubuntu/waagent.conf
index c1568243..e67cdfe7 100644
--- a/config/ubuntu/waagent.conf
+++ b/config/ubuntu/waagent.conf
@@ -102,6 +102,4 @@ OS.SshDir=/etc/ssh
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/config/waagent.conf b/config/waagent.conf
index 7c23f119..90efa375 100644
--- a/config/waagent.conf
+++ b/config/waagent.conf
@@ -114,6 +114,4 @@ OS.SshDir=/etc/ssh
 # OS.AllowHTTP=n
 
 # Add firewall rules to protect access to Azure host node services
-# Note:
-# - The default is false to protect the state of existing VMs
 OS.EnableFirewall=y
diff --git a/setup.py b/setup.py
index 063779a8..e15a2d46 100755
--- a/setup.py
+++ b/setup.py
@@ -159,6 +159,15 @@ def get_data_files(name, version, fullname):
         set_conf_files(data_files, src=["config/debian/waagent.conf"])
         set_logrotate_files(data_files)
         set_udev_files(data_files, dest="/lib/udev/rules.d")
+    elif name == 'iosxe':
+        set_bin_files(data_files)
+        set_conf_files(data_files, src=["config/iosxe/waagent.conf"])
+        set_logrotate_files(data_files)
+        set_udev_files(data_files)
+        set_systemd_files(data_files, dest="/usr/lib/systemd/system")
+        if version.startswith("7.1"):
+            # TODO this is a mitigation to systemctl bug on 7.1
+            set_sysv_files(data_files)
     else:
         # Use default setting
         set_bin_files(data_files)
@@ -222,7 +231,7 @@ setuptools.setup(
     platforms='Linux',
     url='https://github.com/Azure/WALinuxAgent',
     license='Apache License Version 2.0',
-    packages=find_packages(exclude=["tests"]),
+    packages=find_packages(exclude=["tests*"]),
     py_modules=["__main__"],
     install_requires=requires,
     cmdclass={


Test patch:
diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py
index 7cb51668..9e6f3a44 100644
--- a/tests/ga/test_extension.py
+++ b/tests/ga/test_extension.py
@@ -326,7 +326,7 @@ class TestExtension(AgentTestCase):
         #Test hotfix
         test_data.goal_state = test_data.goal_state.replace("<Incarnation>2<",
                                                             "<Incarnation>3<")
-        test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.0")
+        test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.1")
         test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"1\"", 
                                                         "seqNo=\"2\"")
         exthandlers_handler.run()
@@ -336,7 +336,7 @@ class TestExtension(AgentTestCase):
         #Test upgrade
         test_data.goal_state = test_data.goal_state.replace("<Incarnation>3<",
                                                             "<Incarnation>4<")
-        test_data.ext_conf = test_data.ext_conf.replace("1.1.0", "1.2.0")
+        test_data.ext_conf = test_data.ext_conf.replace("1.1.1", "1.2.0")
         test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"2\"",
                                                         "seqNo=\"3\"")
         exthandlers_handler.run()
@@ -460,84 +460,72 @@ class TestExtension(AgentTestCase):
         self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
         self._assert_ext_status(protocol.report_ext_status, "success", 0)
 
-        #Test goal state not changed
-        exthandlers_handler.run()
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-
-        #Test goal state changed without new GUID
+        #Test goal state changed
         test_data.goal_state = test_data.goal_state.replace("<Incarnation>1<",
                                                             "<Incarnation>2<")
         exthandlers_handler.run()
         self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
         self._assert_ext_status(protocol.report_ext_status, "success", 0)
 
-        #Test GUID change without new version available
+        #Test minor version bump
         test_data.goal_state = test_data.goal_state.replace("<Incarnation>2<",
                                                             "<Incarnation>3<")
-        test_data.ext_conf = test_data.ext_conf.replace("FE0987654321", "FE0987654322")
+        test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.0")
         exthandlers_handler.run()
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
+        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0")
         self._assert_ext_status(protocol.report_ext_status, "success", 0)
 
-        #Test hotfix available without GUID change
+        #Test hotfix version bump
         test_data.goal_state = test_data.goal_state.replace("<Incarnation>3<",
                                                             "<Incarnation>4<")
-        test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.0")
-        exthandlers_handler.run()
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0")
-        self._assert_ext_status(protocol.report_ext_status, "success", 0)
-
-        #Test GUID change with hotfix
-        test_data.goal_state = test_data.goal_state.replace("<Incarnation>4<",
-                                                            "<Incarnation>5<")
-        test_data.ext_conf = test_data.ext_conf.replace("FE0987654322", "FE0987654323")
+        test_data.ext_conf = test_data.ext_conf.replace("1.1.0", "1.1.1")
         exthandlers_handler.run()
         self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1")
         self._assert_ext_status(protocol.report_ext_status, "success", 0)
 
         #Test disable
-        test_data.goal_state = test_data.goal_state.replace("<Incarnation>5<",
-                                                            "<Incarnation>6<")
+        test_data.goal_state = test_data.goal_state.replace("<Incarnation>4<",
+                                                            "<Incarnation>5<")
         test_data.ext_conf = test_data.ext_conf.replace("enabled", "disabled")
         exthandlers_handler.run()
         self._assert_handler_status(protocol.report_vm_status, "NotReady",
                                     1, "1.1.1")
 
         #Test uninstall
-        test_data.goal_state = test_data.goal_state.replace("<Incarnation>6<",
-                                                            "<Incarnation>7<")
+        test_data.goal_state = test_data.goal_state.replace("<Incarnation>5<",
+                                                            "<Incarnation>6<")
         test_data.ext_conf = test_data.ext_conf.replace("disabled", "uninstall")
         exthandlers_handler.run()
         self._assert_no_handler_status(protocol.report_vm_status)
 
         #Test uninstall again!
-        test_data.goal_state = test_data.goal_state.replace("<Incarnation>7<",
-                                                            "<Incarnation>8<")
+        test_data.goal_state = test_data.goal_state.replace("<Incarnation>6<",
+                                                            "<Incarnation>7<")
         exthandlers_handler.run()
         self._assert_no_handler_status(protocol.report_vm_status)
 
         #Test re-install
-        test_data.goal_state = test_data.goal_state.replace("<Incarnation>8<",
-                                                            "<Incarnation>9<")
+        test_data.goal_state = test_data.goal_state.replace("<Incarnation>7<",
+                                                            "<Incarnation>8<")
         test_data.ext_conf = test_data.ext_conf.replace("uninstall", "enabled")
         exthandlers_handler.run()
         self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1")
         self._assert_ext_status(protocol.report_ext_status, "success", 0)
 
-        #Test upgrade available without GUID change
-        test_data.goal_state = test_data.goal_state.replace("<Incarnation>9<",
-                                                            "<Incarnation>10<")
-        test_data.ext_conf = test_data.ext_conf.replace("1.1.0", "1.2.0")
+        #Test version bump post-re-install
+        test_data.goal_state = test_data.goal_state.replace("<Incarnation>8<",
+                                                            "<Incarnation>9<")
+        test_data.ext_conf = test_data.ext_conf.replace("1.1.1", "1.2.0")
         exthandlers_handler.run()
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1")
+        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0")
         self._assert_ext_status(protocol.report_ext_status, "success", 0)
 
-        #Test GUID change with upgrade available
-        test_data.goal_state = test_data.goal_state.replace("<Incarnation>10<",
-                                                            "<Incarnation>11<")
-        test_data.ext_conf = test_data.ext_conf.replace("FE0987654323", "FE0987654324")
+        #Test rollback
+        test_data.goal_state = test_data.goal_state.replace("<Incarnation>9<",
+                                                            "<Incarnation>10<")
+        test_data.ext_conf = test_data.ext_conf.replace("1.2.0", "1.1.0")
         exthandlers_handler.run()
-        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0")
+        self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0")
         self._assert_ext_status(protocol.report_ext_status, "success", 0)
 
     @patch('azurelinuxagent.ga.exthandlers.add_event')
@@ -669,12 +657,11 @@ class TestExtension(AgentTestCase):
                         datafile = DATA_FILE_EXT_INTERNALVERSION
                 else:
                     config_version = '1.0.0'
+                    decision_version = '1.0.0'
                     if autoupgrade:
                         datafile = DATA_FILE_EXT_AUTOUPGRADE
-                        decision_version = '1.2.0'
                     else:
                         datafile = DATA_FILE
-                        decision_version = '1.0.0'
 
                 _, protocol = self._create_mock(WireProtocolData(datafile), *args)
                 ext_handlers, _ = protocol.get_ext_handlers()
@@ -695,22 +682,25 @@ class TestExtension(AgentTestCase):
 
         # (installed_version, config_version, exptected_version, autoupgrade_expected_version)
         cases = [
-            (None,  '2.0',     '2.0.0',    '2.2.0'),
-            (None,  '2.0.0',   '2.0.0',    '2.2.0'),
-            ('1.0', '1.0.0',   '1.0.0',    '1.2.0'),
-            (None,  '2.1.0',   '2.1.1',    '2.2.0'),
-            (None,  '2.2.0',   '2.2.0',    '2.2.0'),
-            (None,  '2.3.0',   '2.3.0',    '2.3.0'),
-            (None,  '2.4.0',   '2.4.0',    '2.4.0'),
-            (None,  '3.0',     '3.0',      '3.1'),
-            (None,  '4.0',     '4.0.0.1',  '4.1.0.0'),
+            (None,  '2.0',     '2.0.0'),
+            (None,  '2.0.0',   '2.0.0'),
+            ('1.0', '1.0.0',   '1.0.0'),
+            (None,  '2.1.0',   '2.1.0'),
+            (None,  '2.1.1',   '2.1.1'),
+            (None,  '2.2.0',   '2.2.0'),
+            (None,  '2.3.0',   '2.3.0'),
+            (None,  '2.4.0',   '2.4.0'),
+            (None,  '3.0',     '3.0'),
+            (None,  '3.1',     '3.1'),
+            (None,  '4.0',     '4.0.0.1'),
+            (None,  '4.1',     '4.1.0.0'),
         ]
 
         _, protocol = self._create_mock(WireProtocolData(DATA_FILE), *args)
         version_uri = Mock()
         version_uri.uri = 'http://some/Microsoft.OSTCExtensions_ExampleHandlerLinux_asiaeast_manifest.xml'
 
-        for (installed_version, config_version, expected_version, autoupgrade_expected_version) in cases:
+        for (installed_version, config_version, expected_version) in cases:
             ext_handler = Mock()
             ext_handler.properties = Mock()
             ext_handler.name = 'OSTCExtensions.ExampleHandlerLinux'
@@ -723,15 +713,6 @@ class TestExtension(AgentTestCase):
             ext_handler_instance.decide_version()
             self.assertEqual(expected_version, ext_handler.properties.version)
 
-            ext_handler.properties.version = config_version
-            ext_handler.properties.upgradePolicy = 'auto'
-
-            ext_handler_instance = ExtHandlerInstance(ext_handler, protocol)
-            ext_handler_instance.get_installed_version = Mock(return_value=installed_version)
-
-            ext_handler_instance.decide_version()
-            self.assertEqual(autoupgrade_expected_version, ext_handler.properties.version)
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index a3aa7ae9..af5dcbba 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -1000,8 +1000,8 @@ class TestUpdate(UpdateTestCase):
         self.assertTrue(2 < len(self.update_handler.agents))
 
         # Purge every other agent
-        kept_agents = self.update_handler.agents[1::2]
-        purged_agents = self.update_handler.agents[::2]
+        kept_agents = self.update_handler.agents[::2]
+        purged_agents = self.update_handler.agents[1::2]
 
         # Reload and assert only the kept agents remain on disk
         self.update_handler.agents = kept_agents
diff --git a/tests/protocol/test_imds.py b/tests/protocol/test_imds.py
index 033dae78..327aa020 100644
--- a/tests/protocol/test_imds.py
+++ b/tests/protocol/test_imds.py
@@ -275,11 +275,7 @@ class TestImds(AgentTestCase):
                                 expected_response='')
 
     def test_field_validation(self):
-        self._assert_field('compute', 'name')
-        self._assert_field('compute', 'location')
-        self._assert_field('compute', 'vmSize')
-        self._assert_field('compute', 'subscriptionId')
-        self._assert_field('compute')
+        # TODO: compute fields (#1249)
 
         self._assert_field('network', 'interface', 'ipv4', 'ipAddress', 'privateIpAddress')
         self._assert_field('network', 'interface', 'ipv4', 'ipAddress')
diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py
index 64a6abed..e679d40d 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -254,11 +254,8 @@ class TestWireProtocol(AgentTestCase):
         goal_state = GoalState(WireProtocolData(DATA_FILE).goal_state)
 
         with patch.object(StatusBlob, "prepare", side_effect=Exception) as mock_prepare:
-            with patch.object(WireClient, "report_status_event") as mock_event:
-                self.assertRaises(ProtocolError, wire_protocol_client.upload_status_blob)
-
-                self.assertEqual(1, mock_prepare.call_count)
-                self.assertEqual(1, mock_event.call_count)
+            self.assertRaises(ProtocolError, wire_protocol_client.upload_status_blob)
+            self.assertEqual(1, mock_prepare.call_count)
 
     def test_get_in_vm_artifacts_profile_blob_not_available(self, *args):
         wire_protocol_client = WireProtocol(wireserver_url).client
diff --git a/tests/utils/test_process_util.py b/tests/utils/test_process_util.py
index 85abdb5a..a950e556 100644
--- a/tests/utils/test_process_util.py
+++ b/tests/utils/test_process_util.py
@@ -14,12 +14,12 @@
 #
 # Requires Python 2.6+ and Openssl 1.0+
 #
-
+import datetime
 import subprocess
 
 from azurelinuxagent.common.exception import ExtensionError
 from azurelinuxagent.common.utils.processutil \
-    import format_stdout_stderr, capture_from_process, capture_from_process_raw
+    import format_stdout_stderr, capture_from_process
 from tests.tools import *
 import sys
 
@@ -121,7 +121,14 @@ class TestProcessUtils(AgentTestCase):
         actual = capture_from_process(process, cmd)
         self.assertEqual(expected, actual)
 
-    def test_process_timeout(self):
+    def test_process_timeout_non_forked(self):
+        """
+        non-forked process runs for 20 seconds, timeout is 10 seconds
+        we expect:
+            - test to run in just over 10 seconds
+            - exception should be thrown
+            - output should be collected
+        """
         cmd = "{0} -t 20".format(process_target)
         process = subprocess.Popen(cmd,
                                    shell=True,
@@ -130,19 +137,93 @@ class TestProcessUtils(AgentTestCase):
                                    env=os.environ,
                                    preexec_fn=os.setsid)
 
-        if sys.version_info < (2, 7):
-            self.assertRaises(ExtensionError, capture_from_process_raw, process, cmd, 10)
-        else:
-            with self.assertRaises(ExtensionError) as ee:
-                capture_from_process_raw(process, cmd, 10)
+        try:
+            capture_from_process(process, 'sleep 20', 10)
+            self.fail('Timeout exception was expected')
+        except ExtensionError as e:
+            body = str(e)
+            self.assertTrue('Timeout(10)' in body)
+            self.assertTrue('Iteration 9' in body)
+            self.assertFalse('Iteration 11' in body)
+        except Exception as gen_ex:
+            self.fail('Unexpected exception: {0}'.format(gen_ex))
 
-            body = str(ee.exception)
-            if sys.version_info >= (3, 2):
-                self.assertNotRegex(body, "Iteration 12")
-                self.assertRegex(body, "Iteration 8")
-            else:
-                self.assertNotRegexpMatches(body, "Iteration 12")
-                self.assertRegexpMatches(body, "Iteration 8")
+    def test_process_timeout_forked(self):
+        """
+        forked process runs for 20 seconds, timeout is 10 seconds
+        we expect:
+            - test to run in less than 3 seconds
+            - no exception should be thrown
+            - no output is collected
+        """
+        cmd = "{0} -t 20 &".format(process_target)
+        process = subprocess.Popen(cmd,
+                                   shell=True,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE,
+                                   env=os.environ,
+                                   preexec_fn=os.setsid)
+
+        start = datetime.datetime.utcnow()
+        try:
+            cap = capture_from_process(process, 'sleep 20 &', 10)
+        except Exception as e:
+            self.fail('No exception should be thrown for a long running process which forks: {0}'.format(e))
+        duration = datetime.datetime.utcnow() - start
+
+        self.assertTrue(duration < datetime.timedelta(seconds=3))
+        self.assertEqual('[stdout]\ncannot collect stdout\n\n[stderr]\n', cap)
+
+    def test_process_behaved_non_forked(self):
+        """
+        non-forked process runs for 10 seconds, timeout is 20 seconds
+        we expect:
+            - test to run in just over 10 seconds
+            - no exception should be thrown
+            - output should be collected
+        """
+        cmd = "{0} -t 10".format(process_target)
+        process = subprocess.Popen(cmd,
+                                   shell=True,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE,
+                                   env=os.environ,
+                                   preexec_fn=os.setsid)
+
+        try:
+            body = capture_from_process(process, 'sleep 10', 20)
+        except Exception as gen_ex:
+            self.fail('Unexpected exception: {0}'.format(gen_ex))
+
+        self.assertFalse('Timeout' in body)
+        self.assertTrue('Iteration 9' in body)
+        self.assertTrue('Iteration 10' in body)
+
+    def test_process_behaved_forked(self):
+        """
+        forked process runs for 10 seconds, timeout is 20 seconds
+        we expect:
+            - test to run in under 3 seconds
+            - no exception should be thrown
+            - output is not collected
+        """
+        cmd = "{0} -t 10 &".format(process_target)
+        process = subprocess.Popen(cmd,
+                                   shell=True,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE,
+                                   env=os.environ,
+                                   preexec_fn=os.setsid)
+
+        start = datetime.datetime.utcnow()
+        try:
+            body = capture_from_process(process, 'sleep 10 &', 20)
+        except Exception as e:
+            self.fail('No exception should be thrown for a well behaved process which forks: {0}'.format(e))
+        duration = datetime.datetime.utcnow() - start
+
+        self.assertTrue(duration < datetime.timedelta(seconds=3))
+        self.assertEqual('[stdout]\ncannot collect stdout\n\n[stderr]\n', body)
 
     def test_process_bad_pgid(self):
         """