Repository: Azure/WALinuxAgent
Pull request #1266 (2018-07-19 07:01:28)

Updating the osinfo to list
The issue seen when running on a Mac is

```
======================================================================
ERROR: Failure: TypeError ('tuple' object does not support item assignment)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/venv2.7/lib/python2.7/site-packages/nose/loader.py", line 418, in loadTestsFromName
    addr.filename, addr.module)
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/venv2.7/lib/python2.7/site-packages/nose/importer.py", line 47, in importFromPath
    return self.importFromDir(dir_path, fqname)
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/venv2.7/lib/python2.7/site-packages/nose/importer.py", line 94, in importFromDir
    mod = load_module(part_fqname, fh, filename, desc)
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/tests/test_import.py", line 1, in <module>
    from tests.tools import *
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/tests/tools.py", line 31, in <module>
    import azurelinuxagent.common.event as event
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/azurelinuxagent/common/event.py", line 33, in <module>
    from azurelinuxagent.common.protocol.restapi import TelemetryEventParam, \
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/azurelinuxagent/common/protocol/__init__.py", line 18, in <module>
    from azurelinuxagent.common.protocol.util import get_protocol_util, \
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/azurelinuxagent/common/protocol/util.py", line 34, in <module>
    from azurelinuxagent.common.osutil import get_osutil
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/azurelinuxagent/common/osutil/__init__.py", line 18, in <module>
    from azurelinuxagent.common.osutil.factory import get_osutil
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/azurelinuxagent/common/osutil/factory.py", line 20, in <module>
    from azurelinuxagent.common.version import *
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/azurelinuxagent/common/version.py", line 135, in <module>
    __distro__ = get_distro()
  File "/Users/varadmeru/work/microsoft/github/WALinuxAgent/azurelinuxagent/common/version.py", line 110, in get_distro
    osinfo[0] = osinfo[0].strip('"').strip(' ').lower()
TypeError: 'tuple' object does not support item assignment

----------------------------------------------------------------------
Ran 58 tests in 0.104s

FAILED (errors=39)
```

The issue seems to be 
```
    if 'FreeBSD' in platform.system():
        release = re.sub('\-.*\Z', '', ustr(platform.release()))
        osinfo = ['freebsd', release, '', 'freebsd']          <------------------ A
    elif 'OpenBSD' in platform.system():
        release = re.sub('\-.*\Z', '', ustr(platform.release()))
        osinfo = ['openbsd', release, '', 'openbsd']          <------------------ B
    elif 'Linux' in platform.system():
        osinfo = get_linux_distribution(0, 'alpine')
    else:
        try:
            # dist() removed in Python 3.7
            osinfo = platform.dist()          <------------------ C
        except:
            osinfo = ('UNKNOWN', 'FFFF', '')          <------------------ D
```

`A`/`B` are lists (other calls return , whereas `C`/`D` returns a tuple. And for a platforms which are not listed in `A` and `B`, `C` is called, which returns a tuple.

vrdmr: @hglkrijger Added the tests. Thanks.
hglkrijger: There are two things you can do - add the method for 2.6 re: #1124, which we need to do anyway, or simply rewrite the function yourself; iterate through the lists and compare elements. I recommend the first approach.
vrdmr: @hglkrijger Added the backward compat fix and Travis builds correctly.

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/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 bf423ce8..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]
@@ -1568,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 650eaf0f..ecbd7c0f 100644
--- a/azurelinuxagent/common/version.py
+++ b/azurelinuxagent/common/version.py
@@ -85,9 +85,9 @@ def get_distro():
     else:
         try:
             # dist() removed in Python 3.7
-            osinfo = platform.dist()
+            osinfo = list(platform.dist()) + ['']
         except:
-            osinfo = ('UNKNOWN', 'FFFF', '')
+            osinfo = ['UNKNOWN', 'FFFF', '', '']
 
     # The platform.py lib has issue with detecting oracle linux distribution.
     # Merge the following patch provided by oracle as a temporary fix.
@@ -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.29'
+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/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index 21cc2ef2..a66aacbd 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -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()
@@ -502,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:
@@ -562,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)
@@ -637,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())
@@ -667,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:
@@ -819,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")
 
@@ -828,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")
 
@@ -837,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):
@@ -862,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()
@@ -980,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().
@@ -1002,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/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/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/common/test_version.py b/tests/common/test_version.py
index d8c4ea93..bb7f4d37 100644
--- a/tests/common/test_version.py
+++ b/tests/common/test_version.py
@@ -23,10 +23,75 @@ import mock
 
 from azurelinuxagent.common.version import set_current_agent, \
     AGENT_LONG_VERSION, AGENT_VERSION, AGENT_NAME, AGENT_NAME_PATTERN, \
-    get_f5_platform
+    get_f5_platform, get_distro
 from tests.tools import *
 
 
+def freebsd_system():
+    return ["FreeBSD"]
+
+
+def freebsd_system_release(x, y, z):
+    return "10.0"
+
+
+def openbsd_system():
+    return ["OpenBSD"]
+
+
+def openbsd_system_release(x, y, z):
+    return "20.0"
+
+
+def default_system():
+    return [""]
+
+
+def default_system_no_linux_distro():
+    return '', '', ''
+
+def default_system_exception():
+    raise Exception
+
+
+class TestAgentVersion(AgentTestCase):
+    def setUp(self):
+        AgentTestCase.setUp(self)
+        return
+
+    @mock.patch('platform.system', side_effect=freebsd_system)
+    @mock.patch('re.sub', side_effect=freebsd_system_release)
+    def test_distro_is_correct_format_when_freebsd(self, platform_system_name, mock_variable):
+        osinfo = get_distro()
+        freebsd_list = ['freebsd', "10.0", '', 'freebsd']
+        self.assertListEqual(freebsd_list, osinfo)
+        return
+
+    @mock.patch('platform.system', side_effect=openbsd_system)
+    @mock.patch('re.sub', side_effect=openbsd_system_release)
+    def test_distro_is_correct_format_when_openbsd(self, platform_system_name, mock_variable):
+        osinfo = get_distro()
+        openbsd_list = ['openbsd', "20.0", '', 'openbsd']
+        self.assertListEqual(openbsd_list, osinfo)
+        return
+
+    @mock.patch('platform.system', side_effect=default_system)
+    @mock.patch('platform.dist', side_effect=default_system_no_linux_distro)
+    def test_distro_is_correct_format_when_default_case(self, platform_system_name, default_system_no_linux):
+        osinfo = get_distro()
+        default_list = ['', '', '', '']
+        self.assertListEqual(default_list, osinfo)
+        return
+
+    @mock.patch('platform.system', side_effect=default_system)
+    @mock.patch('platform.dist', side_effect=default_system_exception)
+    def test_distro_is_correct_for_exception_case(self, platform_system_name, default_system_no_linux):
+        osinfo = get_distro()
+        default_list = ['unknown', 'FFFF', '', '']
+        self.assertListEqual(default_list, osinfo)
+        return
+
+
 class TestCurrentAgentName(AgentTestCase):
     def setUp(self):
         AgentTestCase.setUp(self)
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/tools.py b/tests/tools.py
index 9de73778..e6bc4501 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -18,8 +18,9 @@
 """
 Define util functions for unit test
 """
-
+import difflib
 import os
+import pprint
 import re
 import shutil
 import tempfile
@@ -60,6 +61,17 @@ def _do_nothing():
 
 _MAX_LENGTH = 120
 
+_MAX_LENGTH_SAFE_REPR = 80
+
+def safe_repr(obj, short=False):
+    try:
+        result = repr(obj)
+    except Exception:
+        result = object.__repr__(obj)
+    if not short or len(result) < _MAX_LENGTH:
+        return result
+    return result[:_MAX_LENGTH_SAFE_REPR] + ' [truncated]...'
+
 
 def skip_if_predicate_false(predicate, message):
     if not predicate():
@@ -111,6 +123,8 @@ class AgentTestCase(unittest.TestCase):
             cls.assertIsNone = cls.emulate_assertIsNone
         if not hasattr(cls, "assertIsNotNone"):
             cls.assertIsNone = cls.emulate_assertIsNotNone
+        if not hasattr(cls, "assertListEqual"):
+            cls.assertListEqual = cls.emulate_assertListEqual
 
     def setUp(self):
         prefix = "{0}_".format(self.__class__.__name__)
@@ -163,6 +177,110 @@ class AgentTestCase(unittest.TestCase):
             msg = msg if msg is not None else '{0} is None'.format(_safe_repr(x))
             self.fail(msg)
 
+    def emulate_assertListEqual(self, seq1, seq2, msg=None, seq_type=None):
+        """An equality assertion for ordered sequences (like lists and tuples).
+
+        For the purposes of this function, a valid ordered sequence type is one
+        which can be indexed, has a length, and has an equality operator.
+
+        Args:
+            seq1: The first sequence to compare.
+            seq2: The second sequence to compare.
+            seq_type: The expected datatype of the sequences, or None if no
+                    datatype should be enforced.
+            msg: Optional message to use on failure instead of a list of
+                    differences.
+        """
+        if seq_type is not None:
+            seq_type_name = seq_type.__name__
+            if not isinstance(seq1, seq_type):
+                raise self.failureException('First sequence is not a %s: %s'
+                                        % (seq_type_name, safe_repr(seq1)))
+            if not isinstance(seq2, seq_type):
+                raise self.failureException('Second sequence is not a %s: %s'
+                                        % (seq_type_name, safe_repr(seq2)))
+        else:
+            seq_type_name = "sequence"
+
+        differing = None
+        try:
+            len1 = len(seq1)
+        except (TypeError, NotImplementedError):
+            differing = 'First %s has no length.    Non-sequence?' % (
+                    seq_type_name)
+
+        if differing is None:
+            try:
+                len2 = len(seq2)
+            except (TypeError, NotImplementedError):
+                differing = 'Second %s has no length.    Non-sequence?' % (
+                        seq_type_name)
+
+        if differing is None:
+            if seq1 == seq2:
+                return
+
+            seq1_repr = safe_repr(seq1)
+            seq2_repr = safe_repr(seq2)
+            if len(seq1_repr) > 30:
+                seq1_repr = seq1_repr[:30] + '...'
+            if len(seq2_repr) > 30:
+                seq2_repr = seq2_repr[:30] + '...'
+            elements = (seq_type_name.capitalize(), seq1_repr, seq2_repr)
+            differing = '%ss differ: %s != %s\n' % elements
+
+            for i in xrange(min(len1, len2)):
+                try:
+                    item1 = seq1[i]
+                except (TypeError, IndexError, NotImplementedError):
+                    differing += ('\nUnable to index element %d of first %s\n' %
+                                 (i, seq_type_name))
+                    break
+
+                try:
+                    item2 = seq2[i]
+                except (TypeError, IndexError, NotImplementedError):
+                    differing += ('\nUnable to index element %d of second %s\n' %
+                                 (i, seq_type_name))
+                    break
+
+                if item1 != item2:
+                    differing += ('\nFirst differing element %d:\n%s\n%s\n' %
+                                 (i, safe_repr(item1), safe_repr(item2)))
+                    break
+            else:
+                if (len1 == len2 and seq_type is None and
+                    type(seq1) != type(seq2)):
+                    # The sequences are the same, but have differing types.
+                    return
+
+            if len1 > len2:
+                differing += ('\nFirst %s contains %d additional '
+                             'elements.\n' % (seq_type_name, len1 - len2))
+                try:
+                    differing += ('First extra element %d:\n%s\n' %
+                                  (len2, safe_repr(seq1[len2])))
+                except (TypeError, IndexError, NotImplementedError):
+                    differing += ('Unable to index element %d '
+                                  'of first %s\n' % (len2, seq_type_name))
+            elif len1 < len2:
+                differing += ('\nSecond %s contains %d additional '
+                             'elements.\n' % (seq_type_name, len2 - len1))
+                try:
+                    differing += ('First extra element %d:\n%s\n' %
+                                  (len1, safe_repr(seq2[len1])))
+                except (TypeError, IndexError, NotImplementedError):
+                    differing += ('Unable to index element %d '
+                                  'of second %s\n' % (len1, seq_type_name))
+        standardMsg = differing
+        diffMsg = '\n' + '\n'.join(
+            difflib.ndiff(pprint.pformat(seq1).splitlines(),
+                          pprint.pformat(seq2).splitlines()))
+        standardMsg = self._truncateMessage(standardMsg, diffMsg)
+        msg = self._formatMessage(msg, standardMsg)
+        self.fail(msg)
+
+
     @staticmethod
     def _create_files(tmp_dir, prefix, suffix, count, with_sleep=0):
         for i in range(count):
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):
         """