Repository: Azure/WALinuxAgent
Pull request #264 (2016-06-23 22:18:40)

Remove call to grep
Note one of the commits is just pep8 formatting fixes. 

Remove the call to grep and change to a open/read.


Patch:
diff --git a/.gitignore b/.gitignore
index c836fd9b..3e547318 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
 __pycache__/
 *.py[cod]
 
+# Virtualenv
+py3env/
+
 # C extensions
 *.so
 
diff --git a/azurelinuxagent/common/dhcp.py b/azurelinuxagent/common/dhcp.py
index c500563a..8c81f248 100644
--- a/azurelinuxagent/common/dhcp.py
+++ b/azurelinuxagent/common/dhcp.py
@@ -18,17 +18,14 @@ import os
 import socket
 import array
 import time
-import threading
 import azurelinuxagent.common.logger as logger
-import azurelinuxagent.common.conf as conf
-import azurelinuxagent.common.utils.fileutil as fileutil
 import azurelinuxagent.common.utils.shellutil as shellutil
+from azurelinuxagent.common.utils import fileutil
 from azurelinuxagent.common.utils.textutil import hex_dump, hex_dump2, \
-                                                  hex_dump3, \
-                                                  compare_bytes, str_to_ord, \
-                                                  unpack_big_endian, \
-                                                  unpack_little_endian, \
-                                                  int_to_ip4_addr
+    hex_dump3, \
+    compare_bytes, str_to_ord, \
+    unpack_big_endian, \
+    int_to_ip4_addr
 from azurelinuxagent.common.exception import DhcpError
 from azurelinuxagent.common.osutil import get_osutil
 
@@ -36,13 +33,16 @@ from azurelinuxagent.common.osutil import get_osutil
 KNOWN_WIRESERVER_IP_ENTRY = '10813FA8'
 KNOWN_WIRESERVER_IP = '168.63.129.16'
 
+
 def get_dhcp_handler():
     return DhcpHandler()
 
+
 class DhcpHandler(object):
     """
     Azure use DHCP option 245 to pass endpoint ip to VMs.
     """
+
     def __init__(self):
         self.osutil = get_osutil()
         self.endpoint = None
@@ -80,23 +80,26 @@ class DhcpHandler(object):
         This is true when running in a virtual network.
         :return: True if a route to KNOWN_WIRESERVER_IP exists.
         """
-        route_exists=False
+        route_exists = False
         logger.info("test for route to {0}".format(KNOWN_WIRESERVER_IP))
         try:
-            route = shellutil.run_get_output("grep -c {0} /proc/net/route".format(KNOWN_WIRESERVER_IP_ENTRY))
-            # route[0]: (int) return code
-            # route[1]: (str) output
-            if route is not None and route[0] == 0 and int(route[1]) > 0:
-                # reset self.gateway and self.routes; we do not need to alter the routing table
+            route_file = '/proc/net/route'
+            if os.path.exists(route_file) and \
+                    KNOWN_WIRESERVER_IP_ENTRY in open(route_file).read():
+                # reset self.gateway and self.routes
+                # we do not need to alter the routing table
                 self.endpoint = KNOWN_WIRESERVER_IP
                 self.gateway = None
                 self.routes = None
-                route_exists=True
+                route_exists = True
                 logger.info("route to {0} exists".format(KNOWN_WIRESERVER_IP))
             else:
-                logger.warn("no route exists to {0}".format(KNOWN_WIRESERVER_IP))
+                logger.warn(
+                    "no route exists to {0}".format(KNOWN_WIRESERVER_IP))
         except Exception as e:
-            logger.error("could not determine whether route exists to {0}: {1}".format(KNOWN_WIRESERVER_IP, e))
+            logger.error(
+                "could not determine whether route exists to {0}: {1}".format(
+                    KNOWN_WIRESERVER_IP, e))
 
         return route_exists
 
@@ -114,19 +117,18 @@ class DhcpHandler(object):
             exists = True
         return exists
 
-
     def conf_routes(self):
         logger.info("Configure routes")
         logger.info("Gateway:{0}", self.gateway)
         logger.info("Routes:{0}", self.routes)
-        #Add default gateway
+        # Add default gateway
         if self.gateway is not None:
-            self.osutil.route_add(0 , 0, self.gateway)
+            self.osutil.route_add(0, 0, self.gateway)
         if self.routes is not None:
             for route in self.routes:
                 self.osutil.route_add(route[0], route[1], route[2])
 
-    def _send_dhcp_req(self, request):    
+    def _send_dhcp_req(self, request):
         __waiting_duration__ = [0, 10, 30, 60, 60]
         for duration in __waiting_duration__:
             try:
@@ -165,7 +167,7 @@ class DhcpHandler(object):
             self.osutil.stop_dhcp_service()
 
         resp = self._send_dhcp_req(req)
-        
+
         if self.osutil.is_dhcp_enabled():
             self.osutil.start_dhcp_service()
 
@@ -176,6 +178,7 @@ class DhcpHandler(object):
             raise DhcpError("Failed to receive dhcp response.")
         self.endpoint, self.gateway, self.routes = parse_dhcp_resp(resp)
 
+
 def validate_dhcp_resp(request, response):
     bytes_recv = len(response)
     if bytes_recv < 0xF6:
@@ -200,18 +203,20 @@ def validate_dhcp_resp(request, response):
                        hex_dump3(request, 4, 4),
                        hex_dump3(response, 4, 4))
         raise DhcpError("TransactionID in dhcp respones "
-                            "doesn't match the request")
+                        "doesn't match the request")
 
     if not compare_bytes(request, response, 0x1C, 6):
         logger.verbose("Mac Address not match:\nsend={0},\nreceive={1}",
                        hex_dump3(request, 0x1C, 6),
                        hex_dump3(response, 0x1C, 6))
         raise DhcpError("Mac Addr in dhcp respones "
-                            "doesn't match the request")
+                        "doesn't match the request")
+
 
 def parse_route(response, option, i, length, bytes_recv):
     # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx
-    logger.verbose("Routes at offset: {0} with length:{1}", hex(i), hex(length))
+    logger.verbose("Routes at offset: {0} with length:{1}", hex(i),
+                   hex(length))
     routes = []
     if length < 5:
         logger.error("Data too small for option:{0}", option)
@@ -232,6 +237,7 @@ def parse_route(response, option, i, length, bytes_recv):
         logger.error("Unable to parse routes")
     return routes
 
+
 def parse_ip_addr(response, option, i, length, bytes_recv):
     if i + 5 < bytes_recv:
         if length != 4:
@@ -244,6 +250,7 @@ def parse_ip_addr(response, option, i, length, bytes_recv):
         logger.error("Data too small for option:{0}", option)
     return None
 
+
 def parse_dhcp_resp(response):
     """
     Parse DHCP response:
@@ -256,18 +263,18 @@ def parse_dhcp_resp(response):
     routes = None
 
     # Walk all the returned options, parsing out what we need, ignoring the
-    # others. We need the custom option 245 to find the the endpoint we talk to,
-    # as well as, to handle some Linux DHCP client incompatibilities,
-    # options 3 for default gateway and 249 for routes. And 255 is end.
+    # others. We need the custom option 245 to find the the endpoint we talk to
+    # as well as to handle some Linux DHCP client incompatibilities;
+    # options 3 for default gateway and 249 for routes; 255 is end.
 
-    i = 0xF0 # offset to first option
+    i = 0xF0  # offset to first option
     while i < bytes_recv:
         option = str_to_ord(response[i])
         length = 0
         if (i + 1) < bytes_recv:
             length = str_to_ord(response[i + 1])
         logger.verbose("DHCP option {0} at offset:{1} with length:{2}",
-                    hex(option), hex(i), hex(length))
+                       hex(option), hex(i), hex(length))
         if option == 255:
             logger.verbose("DHCP packet ended at offset:{0}", hex(i))
             break
@@ -278,14 +285,16 @@ def parse_dhcp_resp(response):
             logger.verbose("Default gateway:{0}, at {1}", gateway, hex(i))
         elif option == 245:
             endpoint = parse_ip_addr(response, option, i, length, bytes_recv)
-            logger.verbose("Azure wire protocol endpoint:{0}, at {1}", endpoint,
-                        hex(i))
+            logger.verbose("Azure wire protocol endpoint:{0}, at {1}",
+                           endpoint,
+                           hex(i))
         else:
             logger.verbose("Skipping DHCP option:{0} at {1} with length {2}",
-                        hex(option), hex(i), hex(length))
+                           hex(option), hex(i), hex(length))
         i += length + 2
     return endpoint, gateway, routes
 
+
 def socket_send(request):
     sock = None
     try:
@@ -306,27 +315,28 @@ def socket_send(request):
         if sock is not None:
             sock.close()
 
+
 def build_dhcp_request(mac_addr, request_broadcast):
     """
     Build DHCP request string.
     """
     #
     # typedef struct _DHCP {
-    #     UINT8   Opcode;                    /* op:    BOOTREQUEST or BOOTREPLY */
-    #     UINT8   HardwareAddressType;       /* htype: ethernet */
-    #     UINT8   HardwareAddressLength;     /* hlen:  6 (48 bit mac address) */
-    #     UINT8   Hops;                      /* hops:  0 */
-    #     UINT8   TransactionID[4];          /* xid:   random */
-    #     UINT8   Seconds[2];                /* secs:  0 */
-    #     UINT8   Flags[2];                  /* flags: 0 or 0x8000 for broadcast */
-    #     UINT8   ClientIpAddress[4];        /* ciaddr: 0 */
-    #     UINT8   YourIpAddress[4];          /* yiaddr: 0 */
-    #     UINT8   ServerIpAddress[4];        /* siaddr: 0 */
-    #     UINT8   RelayAgentIpAddress[4];    /* giaddr: 0 */
-    #     UINT8   ClientHardwareAddress[16]; /* chaddr: 6 byte eth MAC address */
-    #     UINT8   ServerName[64];            /* sname:  0 */
-    #     UINT8   BootFileName[128];         /* file:   0  */
-    #     UINT8   MagicCookie[4];            /*   99  130   83   99 */
+    #  UINT8   Opcode;                    /* op:    BOOTREQUEST or BOOTREPLY */
+    #  UINT8   HardwareAddressType;       /* htype: ethernet */
+    #  UINT8   HardwareAddressLength;     /* hlen:  6 (48 bit mac address) */
+    #  UINT8   Hops;                      /* hops:  0 */
+    #  UINT8   TransactionID[4];          /* xid:   random */
+    #  UINT8   Seconds[2];                /* secs:  0 */
+    #  UINT8   Flags[2];                  /* flags: 0 or 0x8000 for broadcast*/
+    #  UINT8   ClientIpAddress[4];        /* ciaddr: 0 */
+    #  UINT8   YourIpAddress[4];          /* yiaddr: 0 */
+    #  UINT8   ServerIpAddress[4];        /* siaddr: 0 */
+    #  UINT8   RelayAgentIpAddress[4];    /* giaddr: 0 */
+    #  UINT8   ClientHardwareAddress[16]; /* chaddr: 6 byte eth MAC address */
+    #  UINT8   ServerName[64];            /* sname:  0 */
+    #  UINT8   BootFileName[128];         /* file:   0  */
+    #  UINT8   MagicCookie[4];            /*   99  130   83   99 */
     #                                        /* 0x63 0x82 0x53 0x63 */
     #     /* options -- hard code ours */
     #
@@ -354,11 +364,12 @@ def build_dhcp_request(mac_addr, request_broadcast):
         request[4 + a] = str_to_ord(trans_id[a])
 
     logger.verbose("BuildDhcpRequest: transactionId:%s,%04X" % (
-                   hex_dump2(trans_id),
-                   unpack_big_endian(request, 4, 4)))
+        hex_dump2(trans_id),
+        unpack_big_endian(request, 4, 4)))
 
     if request_broadcast:
-        # set boradcast flag to true to request the dhcp sever to respond to a boradcast address,
+        # set broadcast flag to true to request the dhcp sever
+        # to respond to a boradcast address,
         # this is useful when user dhclient fails.
         request[0x0A] = 0x80;
 
@@ -375,5 +386,6 @@ def build_dhcp_request(mac_addr, request_broadcast):
         request[0xEC + a] = [99, 130, 83, 99, 53, 1, 1, 255][a]
     return array.array("B", request)
 
+
 def gen_trans_id():
     return os.urandom(4)
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index 0df28169..a482e319 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -1199,11 +1199,18 @@ class ExtensionManifest(object):
     def _handle_packages(self, packages, isinternal):
         for package in packages:
             version = findtext(package, "Version")
+
+            disallow_major_upgrade = findtext(package, "DisallowMajorVersionUpgrade")
+            if disallow_major_upgrade is None:
+                disallow_major_upgrade = ''
+            disallow_major_upgrade = disallow_major_upgrade.lower() == "true"
+
             uris = find(package, "Uris")
             uri_list = findall(uris, "Uri")
             uri_list = [gettext(x) for x in uri_list]
             pkg = ExtHandlerPackage()
             pkg.version = version
+            pkg.disallow_major_upgrade = disallow_major_upgrade
             for uri in uri_list:
                 pkg_uri = ExtHandlerVersionUri()
                 pkg_uri.uri = uri
diff --git a/azurelinuxagent/common/utils/flexible_version.py b/azurelinuxagent/common/utils/flexible_version.py
index bcac06b0..047acc45 100644
--- a/azurelinuxagent/common/utils/flexible_version.py
+++ b/azurelinuxagent/common/utils/flexible_version.py
@@ -34,9 +34,11 @@ class FlexibleVersion(version.Version):
 
         self._compile_pattern()
 
+        self.prerelease = None
+        self.version = ()
         if vstring:
-            self.parse(vstring)
-
+            self._parse(vstring)
+        return
 
     _nn_version = 'version'
     _nn_prerel_sep = 'prerel_sep'
@@ -47,21 +49,35 @@ class FlexibleVersion(version.Version):
         pn=_nn_prerel_sep,
         sep='|'.join(map(re.escape, ('.', '-'))))
 
-    def parse(self, vstring):
+    @property
+    def major(self):
+        return self.version[0] if len(self.version) > 0 else 0
+
+    @property
+    def minor(self):
+        return self.version[1] if len(self.version) > 1 else 0
+
+    @property
+    def patch(self):
+        return self.version[2] if len(self.version) > 2 else 0
+
+    def _parse(self, vstring):
         m = self.version_re.match(vstring)
         if not m:
             raise ValueError("Invalid version number '{0}'".format(vstring))
 
+        self.prerelease = None
+        self.version = ()
+
         self.prerel_sep = m.group(self._nn_prerel_sep)
         tag = m.group(self._nn_prerel_tag)
         tag_num = m.group(self._nn_prerel_num)
 
         if tag is not None and tag_num is not None:
             self.prerelease = (tag, int(tag_num) if len(tag_num) else None)
-        else:
-            self.prerelease = None
 
         self.version = tuple(map(int, self.sep_re.split(m.group(self._nn_version))))
+        return
 
     def __add__(self, increment):
         version = list(self.version)
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index 5222cee7..98fb948a 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -16,14 +16,21 @@
 #
 # Requires Python 2.4+ and Openssl 1.0+
 #
-import os
-import zipfile
-import time
+
+import glob
 import json
-import subprocess
+import os
 import shutil
+import subprocess
+import time
+import zipfile
+
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
+import azurelinuxagent.common.utils.fileutil as fileutil
+import azurelinuxagent.common.utils.restutil as restutil
+import azurelinuxagent.common.utils.shellutil as shellutil
+
 from azurelinuxagent.common.event import add_event, WALAEventOperation
 from azurelinuxagent.common.exception import ExtensionError, ProtocolError, HttpError
 from azurelinuxagent.common.future import ustr
@@ -35,9 +42,7 @@ from azurelinuxagent.common.protocol.restapi import ExtHandlerStatus, \
                                                     VMStatus, ExtHandler, \
                                                     get_properties, \
                                                     set_properties
-import azurelinuxagent.common.utils.fileutil as fileutil
-import azurelinuxagent.common.utils.restutil as restutil
-import azurelinuxagent.common.utils.shellutil as shellutil
+from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
 from azurelinuxagent.common.utils.textutil import Version
 from azurelinuxagent.common.protocol import get_protocol_util
 
@@ -287,44 +292,102 @@ class ExtHandlerInstance(object):
                                  logger.LogLevel.INFO, log_file)
 
     def decide_version(self):
-        """
-        If auto-upgrade, get the largest public extension version under 
-        the requested major version family of currently installed plugin version
-
-        Else, get the highest hot-fix for requested version, 
-        """
         self.logger.info("Decide which version to use")
         try:
             pkg_list = self.protocol.get_ext_handler_pkgs(self.ext_handler)
         except ProtocolError as e:
             raise ExtensionError("Failed to get ext handler pkgs", e)
 
-        version = self.ext_handler.properties.version 
-        update_policy = self.ext_handler.properties.upgradePolicy
-        
-        version_frag = version.split('.')
-        if len(version_frag) < 2:
-            raise ExtensionError("Wrong version format: {0}".format(version))
-
-        version_prefix = None
-        if update_policy is not None and update_policy == 'auto':
-            version_prefix = "{0}.".format(version_frag[0])
+        # Determine the desired and installed versions
+        requested_version = FlexibleVersion(self.ext_handler.properties.version)
+        installed_version = FlexibleVersion(self.get_installed_version())
+        if installed_version is None:
+            installed_version = requested_version
+
+        # Divide packages
+        # - 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
+        installed_pkg = None
+        public_pkgs = []
+        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 pkg: FlexibleVersion(pkg.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:
-            version_prefix = "{0}.{1}.".format(version_frag[0], version_frag[1])
-        
-        packages = [x for x in pkg_list.versions
-                    if x.version.startswith(version_prefix) and
-                        Version(x.version) >= Version(version) or
-                        x.version == version]
-        
-        packages = sorted(packages, key=lambda x: (not x.isinternal, Version(x.version)), 
-                          reverse=True)
+            selected_pkg = internal_pkg
+
+        selected_version = FlexibleVersion(selected_pkg.version) \
+                            if selected_pkg is not None \
+                            else FlexibleVersion()
+
+        # 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 selected_pkg is None \
+            or (installed_pkg is not None and selected_version < installed_version):
+            self.pkg = installed_pkg
+            self.ext_handler.properties.version = installed_version
+        else:
+            self.pkg = selected_pkg
+            self.ext_handler.properties.version = selected_pkg.version
+
+        if self.pkg is None:
+            raise ExtensionError("Failed to find any valid extension package")
 
-        if len(packages) <= 0:
-            raise ExtensionError("Failed to find and valid extension package")
-        self.pkg = packages[0]
-        self.ext_handler.properties.version = packages[0].version
         self.logger.info("Use version: {0}", self.pkg.version)
+        return
 
     def version_gt(self, other):
         self_version = self.ext_handler.properties.version
@@ -332,31 +395,29 @@ class ExtHandlerInstance(object):
         return Version(self_version) > Version(other_version)
 
     def get_installed_ext_handler(self):
-        lastest_version = None
-        ext_handler_name = self.ext_handler.name
-
-        for dir_name in os.listdir(conf.get_lib_dir()):
-            path = os.path.join(conf.get_lib_dir(), dir_name)
-            if os.path.isdir(path) and dir_name.startswith(ext_handler_name):
-                seperator = dir_name.rfind('-')
-                if seperator < 0:
-                    continue
-                installed_name = dir_name[0: seperator]
-                installed_version = dir_name[seperator + 1:] 
-                if installed_name != ext_handler_name:
-                    continue
-                if lastest_version is None or \
-                        Version(lastest_version) < Version(installed_version):
-                   lastest_version = installed_version
-
+        lastest_version = self.get_installed_version()
         if lastest_version is None:
             return None
         
-        data = get_properties(self.ext_handler)
-        old_ext_handler = ExtHandler()
-        set_properties("ExtHandler", old_ext_handler, data)
-        old_ext_handler.properties.version = lastest_version
-        return ExtHandlerInstance(old_ext_handler, self.protocol)
+        installed_handler = ExtHandler()
+        set_properties("ExtHandler", installed_handler, get_properties(self.ext_handler))
+        installed_handler.properties.version = lastest_version
+        return ExtHandlerInstance(installed_handler, self.protocol)
+
+    def get_installed_version(self):
+        lastest_version = None
+
+        for path in glob.iglob(os.path.join(conf.get_lib_dir(), self.ext_handler.name + "-*")):
+            if not os.path.isdir(path):
+                continue
+
+            separator = path.rfind('-')
+            version = FlexibleVersion(path[separator+1:])
+
+            if lastest_version is None or lastest_version < version:
+                lastest_version = version
+
+        return str(lastest_version) if lastest_version is not None else None
     
     def copy_status_files(self, old_ext_handler_i):
         self.logger.info("Copy status files from old plugin to new")
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index b3332bda..7fc39213 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -80,10 +80,9 @@ class UpdateHandler(object):
             agent_bin = latest_agent.get_agent_bin()
             agent_name = latest_agent.name()
 
-        devnull = open(os.devnull, 'w')
         try:
             child = subprocess.Popen([agent_bin, 'run-exthandlers'], 
-                                      stdout=devnull, stderr=devnull)
+                                      stdout=sys.stdout, stderr=sys.stderr)
             ret = child.wait()
             if ret == None:
                 ret = 1


Test patch:
diff --git a/tests/common/dhcp/test_dhcp.py b/tests/common/dhcp/test_dhcp.py
index 77cfd581..5e4d6085 100644
--- a/tests/common/dhcp/test_dhcp.py
+++ b/tests/common/dhcp/test_dhcp.py
@@ -15,6 +15,7 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+import mock
 import azurelinuxagent.common.dhcp as dhcp
 import azurelinuxagent.common.osutil.default as osutil
 from tests.tools import *
@@ -29,8 +30,20 @@ class TestDHCP(AgentTestCase):
         self.assertTrue(dhcp_handler.gateway is None)
 
         # execute
-        with patch("subprocess.check_output", return_value=b'1'):
-            self.assertTrue(dhcp_handler.wireserver_route_exists)
+        routing_table = "\
+            Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	" \
+                        "Mask		MTU	Window	IRTT \n\
+            eth0	00000000	10813FA8	0003	0	    0	5	" \
+                        "00000000	0	0	0   \n\
+            eth0	00345B0A	00000000	0001	0	    0	5	" \
+                        "00000000	0	0	0   \n\
+            lo	    00000000	01345B0A	0003	0	    0	1	" \
+                        "00FCFFFF	0	0	0   \n"
+
+        with patch("os.path.exists", return_value=True):
+            mo = mock.mock_open(read_data=routing_table)
+            with patch(open_patch(), mo):
+                self.assertTrue(dhcp_handler.wireserver_route_exists)
 
         # test
         self.assertTrue(dhcp_handler.endpoint is not None)
diff --git a/tests/data/wire/manifest.xml b/tests/data/wire/manifest.xml
index c538aaca..ff42b9d4 100644
--- a/tests/data/wire/manifest.xml
+++ b/tests/data/wire/manifest.xml
@@ -18,6 +18,7 @@
         </Plugin>
         <Plugin>
             <Version>2.1.0</Version><Uris><Uri>http://host/OSTCExtensions.ExampleHandlerLinux__2.1.0</Uri></Uris>
+            <DisallowMajorVersionUpgrade>True</DisallowMajorVersionUpgrade>
         </Plugin>
         <Plugin>
             <Version>2.1.1</Version><Uris><Uri>http://host/OSTCExtensions.ExampleHandlerLinux__2.1.1</Uri></Uris>
diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py
index 061c0ee3..1f748257 100644
--- a/tests/ga/test_extension.py
+++ b/tests/ga/test_extension.py
@@ -38,12 +38,14 @@ class TestExtension(AgentTestCase):
                           handler_status.name)
         self.assertEquals(version, handler_status.version)
         self.assertEquals(expected_ext_count, len(handler_status.extensions))
+        return
     
     def _assert_no_handler_status(self, report_vm_status):
         self.assertTrue(report_vm_status.called)
         args, kw = report_vm_status.call_args
         vm_status = args[0]
         self.assertEquals(0, len(vm_status.vmAgent.extensionHandlers))
+        return
 
     def _create_mock(self, test_data, mock_http_get, MockCryptUtil, _):
         """Test enable/disable/unistall of an extension"""
@@ -59,7 +61,6 @@ class TestExtension(AgentTestCase):
         protocol.report_vm_status = MagicMock()
 
         handler.protocol_util.get_protocol = Mock(return_value=protocol)
-        
         return handler, protocol
         
     def test_ext_handler(self, *args):
@@ -185,84 +186,77 @@ class TestExtension(AgentTestCase):
     def test_ext_handler_version_decide_autoupgrade_internalversion(self, *args):
         for internal in [False, True]:
             for autoupgrade in [False, True]:
-                # only python 3.4+ has subTest
-                # with self.subTest(autoupgrade=autoupgrade, internal=internal):
-                    if internal:
-                        config_version = '1.2.0'
-                        decision_version = '1.2.0'
-                        if autoupgrade:
-                            datafile = DATA_FILE_EXT_AUTOUPGRADE_INTERNALVERSION
-                        else:
-                            datafile = DATA_FILE_EXT_INTERNALVERSION
+                if internal:
+                    config_version = '1.2.0'
+                    decision_version = '1.2.0'
+                    if autoupgrade:
+                        datafile = DATA_FILE_EXT_AUTOUPGRADE_INTERNALVERSION
+                    else:
+                        datafile = DATA_FILE_EXT_INTERNALVERSION
+                else:
+                    config_version = '1.0.0'
+                    if autoupgrade:
+                        datafile = DATA_FILE_EXT_AUTOUPGRADE
+                        decision_version = '1.1.0'
                     else:
-                        config_version = '1.0.0'
-                        if autoupgrade:
-                            datafile = DATA_FILE_EXT_AUTOUPGRADE
-                            decision_version = '1.1.0'
-                        else:
-                            datafile = DATA_FILE
-                            decision_version = '1.0.0'
-
-                    _, protocol = self._create_mock(WireProtocolData(datafile), *args)
-                    ext_handlers, _ = protocol.get_ext_handlers()
-                    self.assertEqual(1, len(ext_handlers.extHandlers))
-                    ext_handler = ext_handlers.extHandlers[0]
-                    self.assertEqual('OSTCExtensions.ExampleHandlerLinux', ext_handler.name)
-                    self.assertEqual(config_version, ext_handler.properties.version, "config version.")
-                    ExtHandlerInstance(ext_handler, protocol).decide_version()
-                    self.assertEqual(decision_version, ext_handler.properties.version, "decision version.")
+                        datafile = DATA_FILE
+                        decision_version = '1.0.0'
+
+                _, protocol = self._create_mock(WireProtocolData(datafile), *args)
+                ext_handlers, _ = protocol.get_ext_handlers()
+                self.assertEqual(1, len(ext_handlers.extHandlers))
+                ext_handler = ext_handlers.extHandlers[0]
+                self.assertEqual('OSTCExtensions.ExampleHandlerLinux', ext_handler.name)
+                self.assertEqual(config_version, ext_handler.properties.version, "config version.")
+                ExtHandlerInstance(ext_handler, protocol).decide_version()
+                self.assertEqual(decision_version, ext_handler.properties.version, "decision version.")
 
     def test_ext_handler_version_decide_between_minor_versions(self, *args):
         """
         Using v2.x~v4.x for unit testing
         Available versions via manifest XML (I stands for internal):
         2.0.0, 2.1.0, 2.1.1, 2.2.0, 2.3.0(I), 2.4.0(I), 3.0, 3.1, 4.0.0.0, 4.0.0.1, 4.1.0.0
+        See tests/data/wire/manifest.xml for possible versions
         """
 
-        # (config_version, exptected_version, autoupgrade_expected_version)
+        # (installed_version, config_version, exptected_version, autoupgrade_expected_version)
         cases = [
-            ('2.0',     '2.0.0',    '2.2.0'),
-            ('2.0.0',   '2.0.0',    '2.2.0'),
-            ('2.1.0',   '2.1.1',    '2.2.0'),
-            ('2.2.0',   '2.2.0',    '2.2.0'),
-            ('2.3.0',   '2.3.0',    '2.4.0'),
-            ('2.4.0',   '2.4.0',    '2.4.0'),
-            ('3.0',     '3.0',      '3.1'),
-            ('4.0',     '4.0.0.1',  '4.1.0.0'),
+            (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.1.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'),
         ]
 
         _, protocol = self._create_mock(WireProtocolData(DATA_FILE), *args)
         version_uri = Mock()
         version_uri.uri = 'http://some/Microsoft.OSTCExtensions_ExampleHandlerLinux_asiaeast_manifest.xml'
 
-        for (config_version, expected_version, autoupgrade_expected_version) in cases:
+        for (installed_version, config_version, expected_version, autoupgrade_expected_version) in cases:
             ext_handler = Mock()
             ext_handler.properties = Mock()
             ext_handler.name = 'OSTCExtensions.ExampleHandlerLinux'
             ext_handler.versionUris = [version_uri]
             ext_handler.properties.version = config_version
-            ExtHandlerInstance(ext_handler, protocol).decide_version()
+            
+            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(expected_version, ext_handler.properties.version)
 
             ext_handler.properties.version = config_version
             ext_handler.properties.upgradePolicy = 'auto'
-            ExtHandlerInstance(ext_handler, protocol).decide_version()
-            self.assertEqual(autoupgrade_expected_version, ext_handler.properties.version)
 
-    def test_ext_handler_version_invalid_versions(self, *args):
-        cases = ['2', '2.5', '2.0.1']
-
-        _, protocol = self._create_mock(WireProtocolData(DATA_FILE), *args)
-        version_uri = Mock()
-        version_uri.uri = 'http://some/Microsoft.OSTCExtensions_ExampleHandlerLinux_asiaeast_manifest.xml'
+            ext_handler_instance = ExtHandlerInstance(ext_handler, protocol)
+            ext_handler_instance.get_installed_version = Mock(return_value=installed_version)
 
-        for config_version in cases:
-            ext_handler = Mock()
-            ext_handler.properties = Mock()
-            ext_handler.name = 'OSTCExtensions.ExampleHandlerLinux'
-            ext_handler.versionUris = [version_uri]
-            ext_handler.properties.version = config_version
-            self.assertRaises(ExtensionError, ExtHandlerInstance(ext_handler, protocol).decide_version)
+            ext_handler_instance.decide_version()
+            self.assertEqual(autoupgrade_expected_version, ext_handler.properties.version)
 
 
 if __name__ == '__main__':
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index 05b7fb19..67c47ace 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -15,10 +15,11 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+from __future__ import print_function
+
 import copy
 import glob
 import json
-import mock
 import random
 import subprocess
 import sys
@@ -81,6 +82,12 @@ def get_agent_version():
     return FlexibleVersion(get_agent_name().split("-")[1])
 
 
+def faux_logger():
+    print("STDOUT message")
+    print("STDERR message", file=sys.stderr)
+    return DEFAULT
+
+
 class UpdateTestCase(AgentTestCase):
     def setUp(self):
         AgentTestCase.setUp(self)
@@ -578,6 +585,30 @@ class TestUpdate(UpdateTestCase):
             sys.argv[0])
         return
 
+    def test_run_latest_forwards_output(self):
+        try:
+            tempdir = tempfile.mkdtemp()
+            stdout_path = os.path.join(tempdir, "stdout")
+            stderr_path = os.path.join(tempdir, "stderr")
+
+            with open(stdout_path, "w") as stdout:
+                with open(stderr_path, "w") as stderr:
+                    saved_stdout, sys.stdout = sys.stdout, stdout
+                    saved_stderr, sys.stderr = sys.stderr, stderr
+                    try:
+                        self._test_run_latest(side_effect=faux_logger)
+                    finally:
+                        sys.stdout = saved_stdout
+                        sys.stderr = saved_stderr
+
+            with open(stdout_path, "r") as stdout:
+                self.assertEqual(1, len(stdout.readlines()))
+            with open(stderr_path, "r") as stderr:
+                self.assertEqual(1, len(stderr.readlines()))
+        finally:
+            shutil.rmtree(tempdir, True)
+        return
+
     def test_run_latest_marks_failures(self):
         # logger.add_logger_appender(logger.AppenderType.STDOUT)
         self.prepare_agents()
diff --git a/tests/utils/test_flexible_version.py b/tests/utils/test_flexible_version.py
index 6db4d76e..23d1d418 100644
--- a/tests/utils/test_flexible_version.py
+++ b/tests/utils/test_flexible_version.py
@@ -270,6 +270,48 @@ class TestFlexibleVersion(unittest.TestCase):
                 .format(t))
         return
 
+    def test_major(self):
+        tests = {
+            '1' : 1,
+            '1.2' : 1,
+            '1.2.3' : 1,
+            '1.2.3.4' : 1
+        }
+        for test in iter(tests):
+            expectation = tests[test]
+            self.assertEqual(
+                expectation,
+                FlexibleVersion(test).major)
+        return
+
+    def test_minor(self):
+        tests = {
+            '1' : 0,
+            '1.2' : 2,
+            '1.2.3' : 2,
+            '1.2.3.4' : 2
+        }
+        for test in iter(tests):
+            expectation = tests[test]
+            self.assertEqual(
+                expectation,
+                FlexibleVersion(test).minor)
+        return
+
+    def test_patch(self):
+        tests = {
+            '1' : 0,
+            '1.2' : 0,
+            '1.2.3' : 3,
+            '1.2.3.4' : 3
+        }
+        for test in iter(tests):
+            expectation = tests[test]
+            self.assertEqual(
+                expectation,
+                FlexibleVersion(test).patch)
+        return
+
     def test_parse(self):
         tests = {
             "1.2.3.4": ((1, 2, 3, 4), None),
@@ -279,7 +321,7 @@ class TestFlexibleVersion(unittest.TestCase):
         }
         for test in iter(tests):
             expectation = tests[test]
-            self.v.parse(test)
+            self.v._parse(test)
             self.assertEqual(expectation, (self.v.version, self.v.prerelease))
         return