Repository: Azure/WALinuxAgent
Pull request #237 (2016-06-16 19:17:39)

Multi-nic improvements
Several distinct pieces here, so I have left them as separate commits for the PR. 

- update loopback detection to use SIOCGIFFLAGS and IFF_LOOPBACK
- examine the routing table for known wireserver ip, then skip the dhcp lookup and route manipulation
- ensure the first selected interface is the primary, which has the default route
- parse the wireserver from the dhcp leases file on disk; distro-specific, this is the ubuntu implementation with common parsing logic
- unit tests for each scenario
- addresses #188, #190, and #212 

msftclas: Hi __@hglkrijger__, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution!
    <span>
        In order for us to evaluate and accept your PR, we ask that you sign a contribution license agreement. It's all electronic and will take just minutes. I promise there's no faxing. https://cla.microsoft.com.
    </span>

TTYL, MSBOT;

ahmetalpbalkan: looks legit to me.
hglkrijger: /cc @jasonzio @szarkos @brendandixon for any additional review comments

Patch:
diff --git a/azurelinuxagent/common/dhcp.py b/azurelinuxagent/common/dhcp.py
index 0dc609bf..4e097644 100644
--- a/azurelinuxagent/common/dhcp.py
+++ b/azurelinuxagent/common/dhcp.py
@@ -32,6 +32,10 @@ from azurelinuxagent.common.utils.textutil import hex_dump, hex_dump2, \
 from azurelinuxagent.common.exception import DhcpError
 from azurelinuxagent.common.osutil import get_osutil
 
+# the kernel routing table representation of 168.63.129.16
+KNOWN_WIRESERVER_IP_ENTRY = '10813FA8'
+KNOWN_WIRESERVER_IP = '168.63.129.16'
+
 def get_dhcp_handler():
     return DhcpHandler()
 
@@ -52,8 +56,9 @@ class DhcpHandler(object):
         Configure default gateway and routes
         Save wire server endpoint if found
         """
-        self.send_dhcp_req()
-        self.conf_routes()
+        if not self.wireserver_route_exists and not self.dhcp_lease_exists:
+            self.send_dhcp_req()
+            self.conf_routes()
 
     def wait_for_network(self):
         """
@@ -67,6 +72,49 @@ class DhcpHandler(object):
             self.osutil.start_network()
             ipv4 = self.osutil.get_ip4_addr()
 
+    @property
+    def wireserver_route_exists(self):
+        """
+        Determine whether a route to the known wireserver
+        ip already exists, and if so use that as the endpoint.
+        This is true when running in a virtual network.
+        :return: True if a route to KNOWN_WIRESERVER_IP exists.
+        """
+        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
+                self.endpoint = KNOWN_WIRESERVER_IP
+                self.gateway = None
+                self.routes = None
+                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))
+        except Exception as e:
+            logger.error("could not determine whether route exists to {0}: {1}".format(KNOWN_WIRESERVER_IP, e))
+
+        return route_exists
+
+    @property
+    def dhcp_lease_exists(self):
+        """
+        Check whether the dhcp options cache exists and contains the
+        wireserver endpoint.
+        :return: True if the cached endpoint was found in the dhcp lease
+        """
+        exists = False
+        cached_endpoint = self.osutil.get_dhcp_lease_endpoint()
+        if cached_endpoint is not None:
+            self.endpoint = cached_endpoint
+            exists = True
+        return exists
+
+
     def conf_routes(self):
         logger.info("Configure routes")
         logger.info("Gateway:{0}", self.gateway)
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 5625406b..2cf34ce4 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -26,6 +26,8 @@ import time
 import pwd
 import fcntl
 import base64
+import glob
+import datetime
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.conf as conf
 from azurelinuxagent.common.exception import OSUtilError
@@ -400,7 +402,7 @@ class DefaultOSUtil(object):
     def get_mac_addr(self):
         """
         Convienience function, returns mac addr bound to
-        first non-loobback interface.
+        first non-loopback interface.
         """
         ifname=''
         while len(ifname) < 2 :
@@ -440,14 +442,143 @@ class DefaultOSUtil(object):
             logger.warn(('SIOCGIFCONF returned more than {0} up '
                          'network interfaces.'), expected)
         sock = buff.tostring()
+        primary = bytearray(self.get_primary_interface(), encoding='utf-8')
         for i in range(0, struct_size * expected, struct_size):
             iface=sock[i:i+16].split(b'\0', 1)[0]
-            if iface == b'lo' or iface.startswith('lo:'):
+            if len(iface) == 0 or self.is_loopback(iface) or iface != primary:
+                # test the next one
+                logger.info('interface [{0}] skipped'.format(iface))
                 continue
             else:
+                # use this one
+                logger.info('interface [{0}] selected'.format(iface))
                 break
+
         return iface.decode('latin-1'), socket.inet_ntoa(sock[i+20:i+24])
 
+    def get_primary_interface(self):
+        """
+        Get the name of the primary interface, which is the one with the
+        default route attached to it; if there are multiple default routes,
+        the primary has the lowest Metric.
+        :return: the interface which has the default route
+        """
+        # from linux/route.h
+        RTF_GATEWAY = 0x02
+        DEFAULT_DEST = "00000000"
+
+        hdr_iface = "Iface"
+        hdr_dest = "Destination"
+        hdr_flags = "Flags"
+        hdr_metric = "Metric"
+
+        idx_iface = -1
+        idx_dest = -1
+        idx_flags = -1
+        idx_metric = -1
+        primary = None
+        primary_metric = None
+
+        logger.info("examine /proc/net/route for primary interface")
+        with open('/proc/net/route') as routing_table:
+            idx = 0
+            for header in filter(lambda h: len(h) > 0, routing_table.readline().strip(" \n").split("\t")):
+                if header == hdr_iface:
+                    idx_iface = idx
+                elif header == hdr_dest:
+                    idx_dest = idx
+                elif header == hdr_flags:
+                    idx_flags = idx
+                elif header == hdr_metric:
+                    idx_metric = idx
+                idx = idx + 1
+            for entry in routing_table.readlines():
+                route = entry.strip(" \n").split("\t")
+                if route[idx_dest] == DEFAULT_DEST and int(route[idx_flags]) & RTF_GATEWAY == RTF_GATEWAY:
+                    metric = int(route[idx_metric])
+                    iface = route[idx_iface]
+                    if primary is None or metric < primary_metric:
+                        primary = iface
+                        primary_metric = metric
+        logger.info('primary interface is [{0}]'.format(primary))
+        return primary
+
+
+    def is_primary_interface(self, ifname):
+        """
+        Indicate whether the specified interface is the primary.
+        :param ifname: the name of the interface - eth0, lo, etc.
+        :return: True if this interface binds the default route
+        """
+        return self.get_primary_interface() == ifname
+
+
+    def is_loopback(self, ifname):
+        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+        result = fcntl.ioctl(s.fileno(), 0x8913, struct.pack('256s', ifname[:15]))
+        flags, = struct.unpack('H', result[16:18])
+        isloopback = flags & 8 == 8
+        logger.info('interface [{0}] has flags [{1}], is loopback [{2}]'.format(ifname, flags, isloopback))
+        return isloopback
+
+    def get_dhcp_lease_endpoint(self):
+        """
+        OS specific, this should return the decoded endpoint of
+        the wireserver from option 245 in the dhcp leases file
+        if it exists on disk.
+        :return: The endpoint if available, or None
+        """
+        return None
+
+    @staticmethod
+    def get_endpoint_from_leases_path(pathglob):
+        """
+        Try to discover and decode the wireserver endpoint in the
+        specified dhcp leases path.
+        :param pathglob: The path containing dhcp lease files
+        :return: The endpoint if available, otherwise None
+        """
+        endpoint = None
+
+        HEADER_LEASE = "lease"
+        HEADER_OPTION = "option unknown-245"
+        HEADER_DNS = "option domain-name-servers"
+        HEADER_EXPIRE = "expire"
+        FOOTER_LEASE = "}"
+        FORMAT_DATETIME = "%Y/%m/%d %H:%M:%S"
+
+        for lease_file in glob.glob(pathglob):
+            leases = open(lease_file).read()
+            if HEADER_OPTION in leases:
+                cached_endpoint = None
+                has_option_245 = False
+                expired = True  # assume expired
+                for line in leases.splitlines():
+                    if line.startswith(HEADER_LEASE):
+                        cached_endpoint = None
+                        has_option_245 = False
+                    elif HEADER_DNS in line:
+                        cached_endpoint = line.replace(HEADER_DNS, '').strip(" ;")
+                    elif HEADER_OPTION in line:
+                        has_option_245 = True
+                    elif HEADER_EXPIRE in line:
+                        if "never" in line:
+                            expired = False
+                        else:
+                            try:
+                                expire_string = line.split(" ", 4)[-1].strip(";")
+                                expire_date = datetime.datetime.strptime(expire_string, FORMAT_DATETIME)
+                                if expire_date > datetime.datetime.utcnow():
+                                    expired = False
+                            except:
+                                logger.error("could not parse expiry token '{0}'".format(line))
+                    elif FOOTER_LEASE in line:
+                        if not expired and cached_endpoint is not None and has_option_245:
+                            endpoint = cached_endpoint
+                            # return the first valid entry
+                            break
+        return endpoint
+
     def is_missing_default_route(self):
         routes = shellutil.run_get_output("route -n")[1]
         for route in routes.split("\n"):
diff --git a/azurelinuxagent/common/osutil/ubuntu.py b/azurelinuxagent/common/osutil/ubuntu.py
index 806f87a6..4032cf43 100644
--- a/azurelinuxagent/common/osutil/ubuntu.py
+++ b/azurelinuxagent/common/osutil/ubuntu.py
@@ -16,19 +16,7 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
-import os
-import re
-import pwd
-import shutil
-import socket
-import array
-import struct
-import fcntl
-import time
-import azurelinuxagent.common.logger as logger
-import azurelinuxagent.common.utils.fileutil as fileutil
 import azurelinuxagent.common.utils.shellutil as shellutil
-import azurelinuxagent.common.utils.textutil as textutil
 from azurelinuxagent.common.osutil.default import DefaultOSUtil
 
 class Ubuntu14OSUtil(DefaultOSUtil):
@@ -50,6 +38,9 @@ class Ubuntu14OSUtil(DefaultOSUtil):
     def restore_rules_files(self, rules_files=""):
         pass
 
+    def get_dhcp_lease_endpoint(self):
+        return self.get_endpoint_from_leases_path('/var/lib/dhcp/dhclient.*.leases')
+
 class Ubuntu12OSUtil(Ubuntu14OSUtil):
     def __init__(self):
         super(Ubuntu12OSUtil, self).__init__()
diff --git a/azurelinuxagent/distro/__init__.py b/azurelinuxagent/distro/__init__.py
new file mode 100644
index 00000000..1ea2f383
--- /dev/null
+++ b/azurelinuxagent/distro/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2014 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.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/default/__init__.py b/azurelinuxagent/distro/default/__init__.py
new file mode 100644
index 00000000..1ea2f383
--- /dev/null
+++ b/azurelinuxagent/distro/default/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2014 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.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/default/rdma.py b/azurelinuxagent/distro/default/rdma.py
index 3d6c94df..0ab9320a 100644
--- a/azurelinuxagent/distro/default/rdma.py
+++ b/azurelinuxagent/distro/default/rdma.py
@@ -21,10 +21,14 @@ Handle packages and modules to enable RDMA for IB networking
 
 import os
 import re
-import azurelinuxagent.logger as logger
+import azurelinuxagent.common.logger as logger
+import azurelinuxagent.common.utils.shellutil as shellutil
 
 
 class RDMAHandler(object):
+
+    driver_module_name = 'hv_network_direct'
+
     def __get_rdma_version(self):
         """Retrieve the firmware version information from the system.
            This depends on information provided by the Linux kernel."""
@@ -58,12 +62,13 @@ class RDMAHandler(object):
     def load_driver_module(self):
         """Load the kernel driver, this depends on the proper driver
            to be installed with the install_driver() method"""
-        driver_module_name = 'hv_network_direct'
-        result = os.system('modprobe %s' % driver_module_name)
+        result = shellutil.run('modprobe %s' % self.driver_module_name)
         if result != 0:
             error_msg = 'Could not load "%s" kernel module. '
             error_msg += 'Run "modprobe %s" as root for more details'
-            logger.error(error_msg % (driver_module_name, driver_module_name))
+            logger.error(
+                error_msg % (self.driver_module_name, self.driver_module_name)
+            )
             return
 
         return True
@@ -73,3 +78,16 @@ class RDMAHandler(object):
            be overwritten in the child implementation."""
 
         raise Exception('RDMAHandler.install_driver not implemented')
+
+    def is_driver_loaded(self):
+        """Check if the network module is loaded in kernel space"""
+        cmd = 'lsmod | grep %s' % self.driver_module_name
+        status, loaded_modules = shellutil.run_get_output(cmd)
+        if loaded_modules:
+            return True
+
+    def reboot_system(self):
+        """Reboot the system. This is required as the kernel module for
+           the rdma driver cannot be unloaded with rmmod"""
+        logger.info('System reboot')
+        shellutil.run('shutdown -r now')
diff --git a/azurelinuxagent/distro/suse/__init__.py b/azurelinuxagent/distro/suse/__init__.py
new file mode 100644
index 00000000..1ea2f383
--- /dev/null
+++ b/azurelinuxagent/distro/suse/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2014 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.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/suse/rdma.py b/azurelinuxagent/distro/suse/rdma.py
index a48f8e83..dd66b909 100644
--- a/azurelinuxagent/distro/suse/rdma.py
+++ b/azurelinuxagent/distro/suse/rdma.py
@@ -34,11 +34,14 @@ class SUSERDMAHandler(RDMAHandler):
             error_msg += 'no driver will be installed.'
             logger.error(error_msg)
             return
-        zypper_install = 'zypper -n in'
-        zypper_search = 'zypper se -s'
+        zypper_install = 'zypper -n in %s'
+        zypper_remove = 'zypper -n rm %s'
+        zypper_search = 'zypper se -s %s'
         package_name = 'msft-rdma-kmp-default'
-        cmd = '%s %s' % (zypper_search, package_name)
+        cmd = zypper_search % package_name
         status, repo_package_info = shellutil.run_get_output(cmd)
+        driver_package_versions = []
+        driver_package_installed = False
         for entry in repo_package_info.split('\n'):
             if package_name in entry:
                 sections = entry.split('|')
@@ -48,22 +51,45 @@ class SUSERDMAHandler(RDMAHandler):
                     continue
                 installed = sections[0].strip()
                 version = sections[3].strip()
-                if fw_version in version:
-                    if installed == 'i':
-                        info_msg = 'Driver package "%s-%s" '
-                        info_msg += 'is already installed, nothing to do'
-                        logger.info(info_msg % (package_name, version))
-                        break
-                    cmd = '%s %s-%s' % (zypper_install, package_name, version)
-                    result = os.system(cmd)
-                    if result:
-                        error_msg = 'Failed install of package "%s-%s" '
-                        error_msg += 'from available repositories.'
-                        logger.error(error_msg % (package_name, version))
-                    msg = 'Successfully installed "%s-%s" from '
-                    msg += 'configured repositories'
-                    logger.info(msg % (package_name, version))
-                    break
+                driver_package_versions.append(version)
+                if fw_version in version and installed == 'i':
+                    info_msg = 'Matching driver package "%s-%s" '
+                    info_msg += 'is already installed, nothing to do'
+                    logger.info(info_msg % (package_name, version))
+                    return True
+                if installed == 'i':
+                    driver_package_installed = True
+
+        # If we get here the driver package is installed but the
+        # version doesn't match or no package is installed
+        requires_reboot = False
+        if driver_package_installed:
+            # Unloading the particular driver with rmmod does not work
+            # We have to reboot after the new driver is installed
+            if self.is_driver_loaded():
+                info_msg = 'Currently loaded driver does not match the '
+                info_msg += 'Firmware implementation, rebbot is required.'
+                logger.info(info_msg)
+                requires_reboot = True
+            cmd = zypper_remove % package_name
+            shellutil.run(cmd)
+
+        for entry in driver_package_versions:
+            if fw_version in version:
+                complete_name = '%s-%s' % (package_name, version)
+                cmd = zypper_install % complete_name
+                result = shellutil.run(cmd)
+                if result:
+                    error_msg = 'Failed install of package "%s" '
+                    error_msg += 'from available repositories.'
+                    logger.error(error_msg % complete_name)
+                msg = 'Successfully installed "%s" from '
+                msg += 'configured repositories'
+                logger.info(msg % complete_name)
+                self.load_driver_module()
+                if requires_reboot:
+                    self.reboot_system()
+                return True
         else:
             local_packages = glob.glob('/opt/microsoft/rdma/*.rpm')
             for local_package in local_packages:
@@ -73,7 +99,7 @@ class SUSERDMAHandler(RDMAHandler):
                         package_name in local_package and
                         fw_version in local_package
                 ):
-                    cmd = '%s %s' % (zypper_install, local_package)
+                    cmd = zypper_install % local_package
                     result = shellutil.run(cmd)
                     if result:
                         error_msg = 'Failed install of package "%s" '
@@ -83,8 +109,12 @@ class SUSERDMAHandler(RDMAHandler):
                     msg = 'Successfully installed "%s" from '
                     msg += 'local package cache'
                     logger.info(msg % (local_package))
-                    break
+                    self.load_driver_module()
+                    if requires_reboot:
+                        self.reboot_system()
+                    return True
             else:
                 error_msg = 'Unable to find driver package that matches '
                 error_msg += 'RDMA firmware version "%s"' % fw_version
                 logger.error(error_msg)
+                return
diff --git a/init/walinuxagent.service b/init/waagent.service
similarity index 84%
rename from init/walinuxagent.service
rename to init/waagent.service
index 39d01d1c..e91f1433 100644
--- a/init/walinuxagent.service
+++ b/init/waagent.service
@@ -8,7 +8,7 @@ ConditionPathExists=/etc/waagent.conf
 
 [Service]
 Type=simple
-ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon
+ExecStart=/usr/bin/python -u /usr/sbin/waagent -daemon
 Restart=always
 RestartSec=5
 
diff --git a/setup.py b/setup.py
index 0f42ba61..3e3d579d 100755
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,9 @@ def set_systemd_files(data_files, dest="/lib/systemd/system",
 def set_rc_files(data_files, dest="/etc/rc.d/", src=["init/freebsd/waagent"]):
     data_files.append((dest, src))
 
+def set_udev_files(data_files, dest="/etc/udev/rules.d/", src=["config/66-azure-storage.rules", "config/99-azure-product-uuid.rules"]):
+    data_files.append((dest, src))
+
 
 def get_data_files(name, version, fullname):
     """
@@ -65,6 +68,7 @@ def get_data_files(name, version, fullname):
         set_bin_files(data_files)
         set_conf_files(data_files)
         set_logrotate_files(data_files)
+        set_udev_files(data_files)
         if version.startswith("6"):
             set_sysv_files(data_files)
         else:
@@ -79,12 +83,14 @@ def get_data_files(name, version, fullname):
         set_conf_files(data_files, dest="/usr/share/oem", 
                        src=["config/coreos/waagent.conf"])
         set_logrotate_files(data_files)
+        set_udev_files(data_files)
         set_files(data_files, dest="/usr/share/oem", 
                   src=["init/coreos/cloud-config.yml"])
     elif name == 'ubuntu':
         set_bin_files(data_files)
         set_conf_files(data_files, src=["config/ubuntu/waagent.conf"])
         set_logrotate_files(data_files)
+        set_udev_files(data_files, src=["config/99-azure-product-uuid.rules"])
         if version.startswith("12") or version.startswith("14"):
             #Ubuntu12.04/14.04 - uses upstart
             set_files(data_files, dest="/etc/init",
@@ -102,6 +108,7 @@ def get_data_files(name, version, fullname):
         set_bin_files(data_files)
         set_conf_files(data_files, src=["config/suse/waagent.conf"])
         set_logrotate_files(data_files)
+        set_udev_files(data_files)
         if fullname == 'SUSE Linux Enterprise Server' and \
                 version.startswith('11') or \
                 fullname == 'openSUSE' and version.startswith('13.1'):
@@ -119,6 +126,7 @@ def get_data_files(name, version, fullname):
         set_bin_files(data_files)
         set_conf_files(data_files)
         set_logrotate_files(data_files)
+        set_udev_files(data_files)
         set_sysv_files(data_files)
     return data_files
 


Test patch:
diff --git a/tests/common/dhcp/__init__.py b/tests/common/dhcp/__init__.py
new file mode 100644
index 00000000..2ef4c167
--- /dev/null
+++ b/tests/common/dhcp/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2014 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.4+ and Openssl 1.0+
+#
diff --git a/tests/common/dhcp/test_dhcp.py b/tests/common/dhcp/test_dhcp.py
new file mode 100644
index 00000000..77cfd581
--- /dev/null
+++ b/tests/common/dhcp/test_dhcp.py
@@ -0,0 +1,63 @@
+# Copyright 2014 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.4+ and Openssl 1.0+
+#
+
+import azurelinuxagent.common.dhcp as dhcp
+import azurelinuxagent.common.osutil.default as osutil
+from tests.tools import *
+
+
+class TestDHCP(AgentTestCase):
+    def test_wireserver_route_exists(self):
+        # setup
+        dhcp_handler = dhcp.get_dhcp_handler()
+        self.assertTrue(dhcp_handler.endpoint is None)
+        self.assertTrue(dhcp_handler.routes is None)
+        self.assertTrue(dhcp_handler.gateway is None)
+
+        # execute
+        with patch("subprocess.check_output", return_value=b'1'):
+            self.assertTrue(dhcp_handler.wireserver_route_exists)
+
+        # test
+        self.assertTrue(dhcp_handler.endpoint is not None)
+        self.assertTrue(dhcp_handler.routes is None)
+        self.assertTrue(dhcp_handler.gateway is None)
+
+    def test_wireserver_route_not_exists(self):
+        # setup
+        dhcp_handler = dhcp.get_dhcp_handler()
+        self.assertTrue(dhcp_handler.endpoint is None)
+        self.assertTrue(dhcp_handler.routes is None)
+        self.assertTrue(dhcp_handler.gateway is None)
+
+        # execute
+        self.assertFalse(dhcp_handler.wireserver_route_exists)
+
+        # test
+        self.assertTrue(dhcp_handler.endpoint is None)
+        self.assertTrue(dhcp_handler.routes is None)
+        self.assertTrue(dhcp_handler.gateway is None)
+
+    def test_dhcp_lease_exists(self):
+        dhcp_handler = dhcp.get_dhcp_handler()
+        dhcp_handler.osutil = osutil.DefaultOSUtil()
+        with patch.object(osutil.DefaultOSUtil, 'get_dhcp_lease_endpoint', return_value=None):
+            self.assertFalse(dhcp_handler.dhcp_lease_exists)
+            self.assertEqual(dhcp_handler.endpoint, None)
+        with patch.object(osutil.DefaultOSUtil, 'get_dhcp_lease_endpoint', return_value="foo"):
+            self.assertTrue(dhcp_handler.dhcp_lease_exists)
+            self.assertEqual(dhcp_handler.endpoint, "foo")
diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py
index b0dea03f..23c5f57e 100644
--- a/tests/common/osutil/test_default.py
+++ b/tests/common/osutil/test_default.py
@@ -15,9 +15,12 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+import socket
+import glob
+import mock
 import azurelinuxagent.common.osutil.default as osutil
 import azurelinuxagent.common.utils.shellutil as shellutil
-import mock
+from azurelinuxagent.common.osutil import get_osutil
 from tests.tools import *
 
 
@@ -36,7 +39,90 @@ class TestOSUtil(AgentTestCase):
             self.assertEqual(run_patch.call_count, retries)
             self.assertEqual(run_patch.call_args_list[0][0][0], 'ifdown {0} && ifup {0}'.format(ifname))
 
+    def test_get_first_if(self):
+        ifname, ipaddr = osutil.DefaultOSUtil().get_first_if()
+        self.assertTrue(ifname.startswith('eth'))
+        self.assertTrue(ipaddr is not None)
+        try:
+            socket.inet_aton(ipaddr)
+        except socket.error:
+            self.fail("not a valid ip address")
+
+    def test_isloopback(self):
+        self.assertTrue(osutil.DefaultOSUtil().is_loopback(b'lo'))
+        self.assertFalse(osutil.DefaultOSUtil().is_loopback(b'eth0'))
+
+    def test_isprimary(self):
+        routing_table = "\
+        Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask		MTU	Window	IRTT \n\
+        eth0	00000000	01345B0A	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"
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('lo'))
+            self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('eth0'))
+
+    def test_multiple_default_routes(self):
+        routing_table = "\
+        Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask		MTU	Window	IRTT \n\
+        high	00000000	01345B0A	0003	0	    0	5	00000000	0	0	0   \n\
+        low1	00000000	01345B0A	0003	0	    0	1	00FCFFFF	0	0	0   \n"
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('low1'))
+
+    def test_multiple_interfaces(self):
+        routing_table = "\
+        Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask		MTU	Window	IRTT \n\
+        first	00000000	01345B0A	0003	0	    0	1	00000000	0	0	0   \n\
+        secnd	00000000	01345B0A	0003	0	    0	1	00FCFFFF	0	0	0   \n"
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('first'))
+
+    def test_interface_flags(self):
+        routing_table = "\
+        Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask		MTU	Window	IRTT \n\
+        nflg	00000000	01345B0A	0001	0	    0	1	00000000	0	0	0   \n\
+        flgs	00000000	01345B0A	0003	0	    0	1	00FCFFFF	0	0	0   \n"
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('flgs'))
+
+    def test_no_interface(self):
+        routing_table = "\
+        Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask		MTU	Window	IRTT \n\
+        ndst	00000001	01345B0A	0003	0	    0	1	00000000	0	0	0   \n\
+        nflg	00000000	01345B0A	0001	0	    0	1	00FCFFFF	0	0	0   \n"
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('ndst'))
+            self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('nflg'))
+            self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('invalid'))
+
+    def test_dhcp_lease_default(self):
+        self.assertTrue(osutil.DefaultOSUtil().get_dhcp_lease_endpoint() is None)
+
+    def test_dhcp_lease_ubuntu(self):
+        with patch.object(glob, "glob", return_value=['/var/lib/dhcp/dhclient.eth0.leases']):
+            with patch(open_patch(), mock.mock_open(read_data=load_data("dhcp.leases"))):
+                endpoint = get_osutil(distro_name='ubuntu', distro_version='12.04').get_dhcp_lease_endpoint()
+                self.assertTrue(endpoint is not None)
+                self.assertEqual(endpoint, "168.63.129.16")
+
+                endpoint = get_osutil(distro_name='ubuntu', distro_version='12.04').get_dhcp_lease_endpoint()
+                self.assertTrue(endpoint is not None)
+                self.assertEqual(endpoint, "168.63.129.16")
+
+                endpoint = get_osutil(distro_name='ubuntu', distro_version='14.04').get_dhcp_lease_endpoint()
+                self.assertTrue(endpoint is not None)
+                self.assertEqual(endpoint, "168.63.129.16")
 
 if __name__ == '__main__':
     unittest.main()
-
diff --git a/tests/data/dhcp.leases b/tests/data/dhcp.leases
new file mode 100644
index 00000000..875b13be
--- /dev/null
+++ b/tests/data/dhcp.leases
@@ -0,0 +1,56 @@
+lease {
+  interface "eth0";
+  fixed-address 10.0.1.4;
+  server-name "RDE41D2D9BB18C";
+  option subnet-mask 255.255.255.0;
+  option dhcp-lease-time 4294967295;
+  option routers 10.0.1.1;
+  option dhcp-message-type 5;
+  option dhcp-server-identifier 168.63.129.16;
+  option domain-name-servers expired;
+  option dhcp-renewal-time 4294967295;
+  option unknown-245 a8:3f:81:10;
+  option dhcp-rebinding-time 4294967295;
+  option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net";
+  renew 4 2015/06/16 16:58:54;
+  rebind 4 2015/06/16 16:58:54;
+  expire 4 2015/06/16 16:58:54;
+}
+lease {
+  interface "eth0";
+  fixed-address 10.0.1.4;
+  server-name "RDE41D2D9BB18C";
+  option subnet-mask 255.255.255.0;
+  option dhcp-lease-time 4294967295;
+  option routers 10.0.1.1;
+  option dhcp-message-type 5;
+  option dhcp-server-identifier 168.63.129.16;
+  option domain-name-servers 168.63.129.16;
+  option dhcp-renewal-time 4294967295;
+  option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1;
+  option unknown-245 a8:3f:81:10;
+  option dhcp-rebinding-time 4294967295;
+  option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net";
+  renew 0 2152/07/23 23:27:10;
+  rebind 0 2152/07/23 23:27:10;
+  expire 0 2152/07/23 23:27:10;
+}
+lease {
+  interface "eth0";
+  fixed-address 10.0.1.4;
+  server-name "RDE41D2D9BB18C";
+  option subnet-mask 255.255.255.0;
+  option dhcp-lease-time 4294967295;
+  option routers 10.0.1.1;
+  option dhcp-message-type 5;
+  option dhcp-server-identifier 168.63.129.16;
+  option domain-name-servers invalid;
+  option dhcp-renewal-time 4294967295;
+  option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1;
+  option unknown-245 a8:3f:81:10;
+  option dhcp-rebinding-time 4294967295;
+  option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net";
+  renew 0 2152/07/23 23:27:10;
+  rebind 0 2152/07/23 23:27:10;
+  expire 0 never;
+}
diff --git a/tests/distro/__init__.py b/tests/distro/__init__.py
new file mode 100644
index 00000000..2ef4c167
--- /dev/null
+++ b/tests/distro/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2014 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.4+ and Openssl 1.0+
+#
diff --git a/tests/distro/test_scvmm.py b/tests/distro/test_scvmm.py
index 2aeff303..a617805a 100644
--- a/tests/distro/test_scvmm.py
+++ b/tests/distro/test_scvmm.py
@@ -33,17 +33,19 @@ class TestSCVMM(AgentTestCase):
         scvmm_file = os.path.join(self.tmp_dir, scvmm.VMM_CONF_FILE_NAME)
         fileutil.write_file(scvmm_file, "")
 
-        patch = mock.patch.object(scvmm.ScvmmHandler, 'start_scvmm_agent').start()
-
-        # execute
-        with self.assertRaises(SystemExit):
-            get_daemon_handler().daemon()
-
-        # assert
-        patch.assert_called()
-
-        # cleanup
-        os.remove(scvmm_file)
+        with patch.object(scvmm.ScvmmHandler, 'start_scvmm_agent') as po:
+            with patch('os.listdir', return_value=["sr0", "sr1", "sr2"]):
+                # execute
+                failed = False
+                try:
+                    scvmm.get_scvmm_handler().run()
+                except:
+                    failed = True
+                # assert
+                self.assertTrue(failed)
+                self.assertTrue(po.call_count == 1)
+                # cleanup
+                os.remove(scvmm_file)
 
 
     def test_scvmm_detection_with_multiple_cdroms(self):
@@ -51,17 +53,15 @@ class TestSCVMM(AgentTestCase):
         conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir)
         conf.get_detect_scvmm_env = Mock(return_value=True)
 
-        patch_mount = mock.patch.object(DefaultOSUtil, 'mount_dvd').start()
-
         # execute
-        with patch('os.listdir', return_value=["sr0", "sr1", "sr2"]):
-            scvmm.ScvmmHandler().detect_scvmm_env()
-
-        # assert
-        assert patch_mount.call_count == 3
-        assert patch_mount.call_args_list[0][1]['dvd_device'] == '/dev/sr0'
-        assert patch_mount.call_args_list[1][1]['dvd_device'] == '/dev/sr1'
-        assert patch_mount.call_args_list[2][1]['dvd_device'] == '/dev/sr2'
+        with mock.patch.object(DefaultOSUtil, 'mount_dvd') as patch_mount:
+            with patch('os.listdir', return_value=["sr0", "sr1", "sr2"]):
+                scvmm.ScvmmHandler().detect_scvmm_env()
+                # assert
+                assert patch_mount.call_count == 3
+                assert patch_mount.call_args_list[0][1]['dvd_device'] == '/dev/sr0'
+                assert patch_mount.call_args_list[1][1]['dvd_device'] == '/dev/sr1'
+                assert patch_mount.call_args_list[2][1]['dvd_device'] == '/dev/sr2'
 
 
     def test_scvmm_detection_without_file(self):
@@ -72,13 +72,11 @@ class TestSCVMM(AgentTestCase):
         if os.path.exists(scvmm_file):
             os.remove(scvmm_file)
 
-        patch_start = mock.patch.object(scvmm.ScvmmHandler, 'start_scvmm_agent').start()
-
-        # execute
-        scvmm.ScvmmHandler().detect_scvmm_env()
-
-        # assert
-        patch_start.assert_not_called()
+        with mock.patch.object(scvmm.ScvmmHandler, 'start_scvmm_agent') as patch_start:
+            # execute
+            scvmm.ScvmmHandler().detect_scvmm_env()
+            # assert
+            patch_start.assert_not_called()
 
 
 if __name__ == '__main__':
diff --git a/tests/tools.py b/tests/tools.py
index aef641e3..8f469ce6 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -30,6 +30,7 @@ from functools import wraps
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.event as event
+from azurelinuxagent.common.version import PY_VERSION_MAJOR
 
 #Import mock module for Python2 and Python3
 try:
@@ -95,6 +96,12 @@ supported_distro = [
 
 ]
 
+def open_patch():
+    open_name = '__builtin__.open'
+    if PY_VERSION_MAJOR == 3:
+        open_name = 'builtins.open'
+    return open_name
+
 def distros(distro_name=".*", distro_version=".*", distro_full_name=".*"):
     """Run test on multiple distros"""
     def decorator(test_method):