Repository: Azure/WALinuxAgent
Pull request #250 (2016-06-20 23:25:05)

Host plugin channel
Separate commits here since one is purely reformatting.

- add a protocol handler for the host plugin channel
- add apis for /versions, /status and /vmAgentLog
- make use of the /status api to report vm status as a fallback
- adds unit tests

Note that the /vmAgentLog is not called or unit tested yet.

msftclas: Hi __@hglkrijger__, I'm your friendly neighborhood Microsoft Pull Request Bot (You can call me MSBOT). Thanks for your contribution!
    <p>
        It looks like you're a Microsoft contributor (Hans Krijger). If you're full-time, we DON'T require a Contribution License Agreement. If you are a vendor, please DO sign the electronic Contribution License Agreement. It will take 2 minutes and there's no faxing! https://cla.microsoft.com.
    </p>

TTYL, MSBOT;

hglkrijger: /cc @brendandixon @ahmetalpbalkan 
brendandixon: @hglkrijger Other than my comments above, it looks fine.

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/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py
new file mode 100644
index 00000000..65696046
--- /dev/null
+++ b/azurelinuxagent/common/protocol/hostplugin.py
@@ -0,0 +1,124 @@
+# Microsoft Azure Linux Agent
+#
+# 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+
+#
+
+from azurelinuxagent.common.protocol.wire import *
+from azurelinuxagent.common.utils import textutil
+
+HOST_PLUGIN_PORT = 32526
+URI_FORMAT_GET_API_VERSIONS = "http://{0}:{1}/versions"
+URI_FORMAT_PUT_VM_STATUS = "http://{0}:{1}/status"
+URI_FORMAT_PUT_LOG = "http://{0}:{1}/vmAgentLog"
+API_VERSION = "2015-09-01"
+
+
+class HostPluginProtocol(object):
+    def __init__(self, endpoint):
+        if endpoint is None:
+            raise ProtocolError("Host plugin endpoint not provided")
+        self.is_initialized = False
+        self.is_available = False
+        self.api_versions = None
+        self.endpoint = endpoint
+
+    def ensure_initialized(self):
+        if not self.is_initialized:
+            self.api_versions = self.get_api_versions()
+            self.is_available = API_VERSION in self.api_versions
+            self.is_initialized = True
+        return self.is_available
+
+    def get_api_versions(self):
+        url = URI_FORMAT_GET_API_VERSIONS.format(self.endpoint,
+                                                 HOST_PLUGIN_PORT)
+        logger.info("getting API versions at [{0}]".format(url))
+        try:
+            response = restutil.http_get(url)
+            if response.status != httpclient.OK:
+                logger.error(
+                    "get API versions returned status code [{0}]".format(
+                        response.status))
+                return []
+            return response.read()
+        except HttpError as e:
+            logger.error("get API versions failed with [{0}]".format(e))
+            return []
+
+    def put_vm_status(self, status_blob, sas_url):
+        """
+        Try to upload the VM status via the host plugin /status channel
+        :param sas_url: the blob SAS url to pass to the host plugin
+        :type status_blob: StatusBlob
+        """
+        if not self.ensure_initialized():
+            logger.error("host plugin channel is not available")
+            return
+        if status_blob is None or status_blob.vm_status is None:
+            logger.error("no status data was provided")
+            return
+        url = URI_FORMAT_PUT_VM_STATUS.format(self.endpoint, HOST_PLUGIN_PORT)
+        status = textutil.b64encode(status_blob.vm_status)
+        headers = {"x-ms-version": API_VERSION}
+        blob_headers = [{'headerName': 'x-ms-version',
+                         'headerValue': status_blob.__storage_version__},
+                        {'headerName': 'x-ms-blob-type',
+                         'headerValue': status_blob.type}]
+        data = json.dumps({'requestUri': sas_url, 'headers': blob_headers,
+                           'content': status}, sort_keys=True)
+        logger.info("put VM status at [{0}]".format(url))
+        try:
+            response = restutil.http_put(url, data, headers)
+            if response.status != httpclient.OK:
+                logger.error("put VM status returned status code [{0}]".format(
+                    response.status))
+        except HttpError as e:
+            logger.error("put VM status failed with [{0}]".format(e))
+
+    def put_vm_log(self, content, container_id, deployment_id):
+        """
+        Try to upload the given content to the host plugin
+        :param deployment_id: the deployment id, which is obtained from the
+        goal state (tenant name)
+        :param container_id: the container id, which is obtained from the
+        goal state
+        :param content: the binary content of the zip file to upload
+        :return:
+        """
+        if not self.ensure_initialized():
+            logger.error("host plugin channel is not available")
+            return
+        if content is None or container_id is None or deployment_id is None:
+            logger.error(
+                "invalid arguments passed: "
+                "[{0}], [{1}], [{2}]".format(
+                    content,
+                    container_id,
+                    deployment_id))
+            return
+        url = URI_FORMAT_PUT_LOG.format(self.endpoint, HOST_PLUGIN_PORT)
+
+        headers = {"x-ms-vmagentlog-deploymentid": deployment_id,
+                   "x-ms-vmagentlog-containerid": container_id}
+        logger.info("put VM log at [{0}]".format(url))
+        try:
+            response = restutil.http_put(url, content, headers)
+            if response.status != httpclient.OK:
+                logger.error("put log returned status code [{0}]".format(
+                    response.status))
+        except HttpError as e:
+            logger.error("put log failed with [{0}]".format(e))
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index 815fb6bf..6b51d9dd 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -16,25 +16,17 @@
 #
 # Requires Python 2.4+ and Openssl 1.0+
 
-import os
-import json
-import re
 import time
-import traceback
 import xml.sax.saxutils as saxutils
 import azurelinuxagent.common.conf as conf
-import azurelinuxagent.common.logger as logger
-from azurelinuxagent.common.exception import ProtocolError, HttpError, \
-                                      ProtocolNotFoundError
-from azurelinuxagent.common.future import ustr, httpclient, bytebuffer
-import azurelinuxagent.common.utils.restutil as restutil
+from azurelinuxagent.common.exception import ProtocolNotFoundError
+from azurelinuxagent.common.future import httpclient, bytebuffer
 from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, \
-                                           getattrib, gettext, remove_bom, \
-                                           get_bytes_from_pem
+    getattrib, gettext, remove_bom, get_bytes_from_pem
 import azurelinuxagent.common.utils.fileutil as fileutil
-import azurelinuxagent.common.utils.shellutil as shellutil
 from azurelinuxagent.common.utils.cryptutil import CryptUtil
 from azurelinuxagent.common.protocol.restapi import *
+from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol
 
 VERSION_INFO_URI = "http://{0}/?comp=versions"
 GOAL_STATE_URI = "http://{0}/machine/?comp=goalstate"
@@ -58,25 +50,31 @@ TRANSPORT_PRV_FILE_NAME = "TransportPrivate.pem"
 PROTOCOL_VERSION = "2012-11-30"
 ENDPOINT_FINE_NAME = "WireServer"
 
-SHORT_WAITING_INTERVAL = 1 # 1 second
-LONG_WAITING_INTERVAL = 15 # 15 seconds
+SHORT_WAITING_INTERVAL = 1  # 1 second
+LONG_WAITING_INTERVAL = 15  # 15 seconds
+
+
+class UploadError(HttpError):
+    pass
+
 
 class WireProtocolResourceGone(ProtocolError):
     pass
 
+
 class WireProtocol(Protocol):
-    """Slim layer to adapte wire protocol data to metadata protocol interface"""
+    """Slim layer to adapt wire protocol data to metadata protocol interface"""
 
     def __init__(self, endpoint):
         if endpoint is None:
-            raise ProtocolError("WireProtocl endpoint is None")
+            raise ProtocolError("WireProtocol endpoint is None")
         self.endpoint = endpoint
         self.client = WireClient(self.endpoint)
 
     def detect(self):
         self.client.check_wire_protocol_version()
 
-        trans_prv_file = os.path.join(conf.get_lib_dir(), 
+        trans_prv_file = os.path.join(conf.get_lib_dir(),
                                       TRANSPORT_PRV_FILE_NAME)
         trans_cert_file = os.path.join(conf.get_lib_dir(),
                                        TRANSPORT_CERT_FILE_NAME)
@@ -103,7 +101,7 @@ class WireProtocol(Protocol):
         return certificates.cert_list
 
     def get_vmagent_manifests(self):
-        #Update goal state to get latest extensions config
+        # Update goal state to get latest extensions config
         self.client.update_goal_state()
         goal_state = self.client.get_goal_state()
         ext_conf = self.client.get_ext_conf()
@@ -116,11 +114,11 @@ class WireProtocol(Protocol):
 
     def get_ext_handlers(self):
         logger.verb("Get extension handler config")
-        #Update goal state to get latest extensions config
+        # Update goal state to get latest extensions config
         self.client.update_goal_state()
         goal_state = self.client.get_goal_state()
         ext_conf = self.client.get_ext_conf()
-        #In wire protocol, incarnation is equivalent to ETag 
+        # In wire protocol, incarnation is equivalent to ETag
         return ext_conf.ext_handlers, goal_state.incarnation
 
     def get_ext_handler_pkgs(self, ext_handler):
@@ -128,7 +126,7 @@ class WireProtocol(Protocol):
         goal_state = self.client.get_goal_state()
         man = self.client.get_ext_manifest(ext_handler, goal_state)
         return man.pkg_list
-   
+
     def report_provision_status(self, provision_status):
         validata_param("provision_status", provision_status, ProvisionStatus)
 
@@ -153,6 +151,7 @@ class WireProtocol(Protocol):
         validata_param("events", events, TelemetryEventList)
         self.client.report_event(events)
 
+
 def _build_role_properties(container_id, role_instance_id, thumbprint):
     xml = (u"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
            u"<RoleProperties>"
@@ -171,9 +170,10 @@ def _build_role_properties(container_id, role_instance_id, thumbprint):
            u"").format(container_id, role_instance_id, thumbprint)
     return xml
 
+
 def _build_health_report(incarnation, container_id, role_instance_id,
                          status, substatus, description):
-    #Escape '&', '<' and '>'
+    # Escape '&', '<' and '>'
     description = saxutils.escape(ustr(description))
     detail = u''
     if substatus is not None:
@@ -207,21 +207,25 @@ def _build_health_report(incarnation, container_id, role_instance_id,
                        detail)
     return xml
 
+
 """
 Convert VMStatus object to status blob format
 """
+
+
 def ga_status_to_v1(ga_status):
     formatted_msg = {
-        'lang' : 'en-US',
-        'message' : ga_status.message
+        'lang': 'en-US',
+        'message': ga_status.message
     }
     v1_ga_status = {
-        'version' : ga_status.version,
-        'status' : ga_status.status,
-        'formattedMessage' : formatted_msg
+        'version': ga_status.version,
+        'status': ga_status.status,
+        'formattedMessage': formatted_msg
     }
     return v1_ga_status
 
+
 def ext_substatus_to_v1(sub_status_list):
     status_list = []
     for substatus in sub_status_list:
@@ -229,7 +233,7 @@ def ext_substatus_to_v1(sub_status_list):
             "name": substatus.name,
             "status": substatus.status,
             "code": substatus.code,
-            "formattedMessage":{
+            "formattedMessage": {
                 "lang": "en-US",
                 "message": substatus.message
             }
@@ -237,20 +241,21 @@ def ext_substatus_to_v1(sub_status_list):
         status_list.append(status)
     return status_list
 
+
 def ext_status_to_v1(ext_name, ext_status):
     if ext_status is None:
         return None
     timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
     v1_sub_status = ext_substatus_to_v1(ext_status.substatusList)
     v1_ext_status = {
-        "status":{
+        "status": {
             "name": ext_name,
             "configurationAppliedTime": ext_status.configurationAppliedTime,
             "operation": ext_status.operation,
             "status": ext_status.status,
             "code": ext_status.code,
             "formattedMessage": {
-                "lang":"en-US",
+                "lang": "en-US",
                 "message": ext_status.message
             }
         },
@@ -260,51 +265,53 @@ def ext_status_to_v1(ext_name, ext_status):
     if len(v1_sub_status) != 0:
         v1_ext_status['substatus'] = v1_sub_status
     return v1_ext_status
-    
+
+
 def ext_handler_status_to_v1(handler_status, ext_statuses, timestamp):
     v1_handler_status = {
-        'handlerVersion' : handler_status.version,
-        'handlerName' : handler_status.name,
-        'status' : handler_status.status,
+        'handlerVersion': handler_status.version,
+        'handlerName': handler_status.name,
+        'status': handler_status.status,
         'code': handler_status.code
     }
     if handler_status.message is not None:
         v1_handler_status["formattedMessage"] = {
-            "lang":"en-US",
+            "lang": "en-US",
             "message": handler_status.message
         }
 
     if len(handler_status.extensions) > 0:
-        #Currently, no more than one extension per handler
+        # Currently, no more than one extension per handler
         ext_name = handler_status.extensions[0]
         ext_status = ext_statuses.get(ext_name)
         v1_ext_status = ext_status_to_v1(ext_name, ext_status)
         if ext_status is not None and v1_ext_status is not None:
             v1_handler_status["runtimeSettingsStatus"] = {
-                'settingsStatus' : v1_ext_status,
-                'sequenceNumber' : ext_status.sequenceNumber
+                'settingsStatus': v1_ext_status,
+                'sequenceNumber': ext_status.sequenceNumber
             }
     return v1_handler_status
 
+
 def vm_status_to_v1(vm_status, ext_statuses):
     timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
 
     v1_ga_status = ga_status_to_v1(vm_status.vmAgent)
     v1_handler_status_list = []
     for handler_status in vm_status.vmAgent.extensionHandlers:
-        v1_handler_status = ext_handler_status_to_v1(handler_status, 
+        v1_handler_status = ext_handler_status_to_v1(handler_status,
                                                      ext_statuses, timestamp)
         if v1_handler_status is not None:
             v1_handler_status_list.append(v1_handler_status)
 
     v1_agg_status = {
         'guestAgentStatus': v1_ga_status,
-        'handlerAggregateStatus' : v1_handler_status_list
+        'handlerAggregateStatus': v1_handler_status_list
     }
     v1_vm_status = {
-        'version' : '1.0',
-        'timestampUTC' : timestamp,
-        'aggregateStatus' : v1_agg_status
+        'version': '1.0',
+        'timestampUTC': timestamp,
+        'aggregateStatus': v1_agg_status
     }
     return v1_vm_status
 
@@ -314,15 +321,17 @@ class StatusBlob(object):
         self.vm_status = None
         self.ext_statuses = {}
         self.client = client
+        self.type = None
+        self.data = None
 
     def set_vm_status(self, vm_status):
         validata_param("vmAgent", vm_status, VMStatus)
         self.vm_status = vm_status
-    
+
     def set_ext_status(self, ext_handler_name, ext_status):
         validata_param("extensionStatus", ext_status, ExtensionStatus)
-        self.ext_statuses[ext_handler_name]= ext_status
-        
+        self.ext_statuses[ext_handler_name] = ext_status
+
     def to_json(self):
         report = vm_status_to_v1(self.vm_status, self.ext_statuses)
         return json.dumps(report)
@@ -330,29 +339,33 @@ class StatusBlob(object):
     __storage_version__ = "2014-02-14"
 
     def upload(self, url):
-        #TODO upload extension only if content has changed
+        # TODO upload extension only if content has changed
         logger.verb("Upload status blob")
-        blob_type = self.get_blob_type(url)
-
-        data = self.to_json()
+        upload_successful = False
+        self.type = self.get_blob_type(url)
+        self.data = self.to_json()
         try:
-            if blob_type == "BlockBlob":
-                self.put_block_blob(url, data)
-            elif blob_type == "PageBlob":
-                self.put_page_blob(url, data)
+            if self.type == "BlockBlob":
+                self.put_block_blob(url, self.data)
+            elif self.type == "PageBlob":
+                self.put_page_blob(url, self.data)
             else:
-                raise ProtocolError("Unknown blob type: {0}".format(blob_type))
+                raise ProtocolError("Unknown blob type: {0}".format(self.type))
         except HttpError as e:
-            raise ProtocolError("Failed to upload status blob: {0}".format(e))
+            logger.warn("Initial upload failed [{0}]".format(e))
+        else:
+            logger.info("Uploading status blob succeeded")
+            upload_successful = True
+        return upload_successful
 
     def get_blob_type(self, url):
-        #Check blob type
+        # Check blob type
         logger.verb("Check blob type.")
         timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
         try:
             resp = self.client.call_storage_service(restutil.http_head, url, {
-                "x-ms-date" :  timestamp,
-                'x-ms-version' : self.__class__.__storage_version__
+                "x-ms-date": timestamp,
+                'x-ms-version': self.__class__.__storage_version__
             })
         except HttpError as e:
             raise ProtocolError((u"Failed to get status blob type: {0}"
@@ -368,45 +381,37 @@ class StatusBlob(object):
     def put_block_blob(self, url, data):
         logger.verb("Upload block blob")
         timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-        try:
-            resp = self.client.call_storage_service(restutil.http_put, url, 
-                                                    data, {
-                "x-ms-date" :  timestamp,
-                "x-ms-blob-type" : "BlockBlob",
-                "Content-Length": ustr(len(data)),
-                "x-ms-version" : self.__class__.__storage_version__
-            })
-        except HttpError as e:
-            raise ProtocolError((u"Failed to upload block blob: {0}"
-                                 u"").format(e))
+        resp = self.client.call_storage_service(restutil.http_put, url, data,
+                        {
+                            "x-ms-date": timestamp,
+                            "x-ms-blob-type": "BlockBlob",
+                            "Content-Length": ustr(len(data)),
+                            "x-ms-version": self.__class__.__storage_version__
+                        })
         if resp.status != httpclient.CREATED:
-            raise ProtocolError(("Failed to upload block blob: {0}"
-                                 "").format(resp.status))
+            raise UploadError(
+                "Failed to upload block blob: {0}".format(resp.status))
 
     def put_page_blob(self, url, data):
         logger.verb("Replace old page blob")
 
-        #Convert string into bytes
-        data=bytearray(data, encoding='utf-8')
+        # Convert string into bytes
+        data = bytearray(data, encoding='utf-8')
         timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
 
-        #Align to 512 bytes
+        # Align to 512 bytes
         page_blob_size = int((len(data) + 511) / 512) * 512
-        try:
-            resp = self.client.call_storage_service(restutil.http_put, url, 
-                                                    "", {
-                "x-ms-date" :  timestamp,
-                "x-ms-blob-type" : "PageBlob",
-                "Content-Length": "0",
-                "x-ms-blob-content-length" : ustr(page_blob_size),
-                "x-ms-version" : self.__class__.__storage_version__
-            })
-        except HttpError as e:
-            raise ProtocolError((u"Failed to clean up page blob: {0}"
-                                 u"").format(e))
+        resp = self.client.call_storage_service(restutil.http_put, url, "",
+                        {
+                            "x-ms-date": timestamp,
+                            "x-ms-blob-type": "PageBlob",
+                            "Content-Length": "0",
+                            "x-ms-blob-content-length": ustr(page_blob_size),
+                            "x-ms-version": self.__class__.__storage_version__
+                        })
         if resp.status != httpclient.CREATED:
-            raise ProtocolError(("Failed to clean up page blob: {0}"
-                                 "").format(resp.status))
+            raise UploadError(
+                "Failed to clean up page blob: {0}".format(resp.status))
 
         if url.count("?") < 0:
             url = "{0}?comp=page".format(url)
@@ -414,34 +419,32 @@ class StatusBlob(object):
             url = "{0}&comp=page".format(url)
 
         logger.verb("Upload page blob")
-        page_max = 4 * 1024 * 1024 #Max page size: 4MB
+        page_max = 4 * 1024 * 1024  # Max page size: 4MB
         start = 0
         end = 0
         while end < len(data):
             end = min(len(data), start + page_max)
             content_size = end - start
-            #Align to 512 bytes
+            # Align to 512 bytes
             page_end = int((end + 511) / 512) * 512
             buf_size = page_end - start
             buf = bytearray(buf_size)
             buf[0: content_size] = data[start: end]
-            try:
-                resp = self.client.call_storage_service(restutil.http_put, url, 
-                                                        bytebuffer(buf), {
-                    "x-ms-date" :  timestamp,
-                    "x-ms-range" : "bytes={0}-{1}".format(start, page_end - 1),
-                    "x-ms-page-write" : "update",
-                    "x-ms-version" : self.__class__.__storage_version__,
+            resp = self.client.call_storage_service(
+                restutil.http_put, url, bytebuffer(buf),
+                {
+                    "x-ms-date": timestamp,
+                    "x-ms-range": "bytes={0}-{1}".format(start, page_end - 1),
+                    "x-ms-page-write": "update",
+                    "x-ms-version": self.__class__.__storage_version__,
                     "Content-Length": ustr(page_end - start)
                 })
-            except HttpError as e:
-                raise ProtocolError((u"Failed to upload page blob: {0}"
-                                     u"").format(e))
             if resp is None or resp.status != httpclient.CREATED:
-                raise ProtocolError(("Failed to upload page blob: {0}"
-                                     "").format(resp.status))
+                raise UploadError(
+                    "Failed to upload page blob: {0}".format(resp.status))
             start = end
 
+
 def event_param_to_v1(param):
     param_format = '<Param Name="{0}" Value={1} T="{2}" />'
     param_type = type(param.value)
@@ -459,6 +462,7 @@ def event_param_to_v1(param):
     return param_format.format(param.name, saxutils.quoteattr(ustr(param.value)),
                                attr_type)
 
+
 def event_to_v1(event):
     params = ""
     for param in event.parameters:
@@ -468,6 +472,7 @@ def event_to_v1(event):
                  '</Event>').format(event.eventId, params)
     return event_str
 
+
 class WireClient(object):
     def __init__(self, endpoint):
         logger.info("Wire server endpoint:{0}", endpoint)
@@ -481,6 +486,7 @@ class WireClient(object):
         self.last_request = 0
         self.req_count = 0
         self.status_blob = StatusBlob(self)
+        self.host_plugin = HostPluginProtocol(self.endpoint)
 
     def prevent_throttling(self):
         """
@@ -489,14 +495,14 @@ class WireClient(object):
         now = time.time()
         if now - self.last_request < 1:
             logger.verb("Last request issued less than 1 second ago")
-            logger.verb("Sleep {0} second to avoid throttling.", 
+            logger.verb("Sleep {0} second to avoid throttling.",
                         SHORT_WAITING_INTERVAL)
             time.sleep(SHORT_WAITING_INTERVAL)
         self.last_request = now
 
         self.req_count += 1
         if self.req_count % 3 == 0:
-            logger.verb("Sleep {0} second to avoid throttling.", 
+            logger.verb("Sleep {0} second to avoid throttling.",
                         SHORT_WAITING_INTERVAL)
             time.sleep(SHORT_WAITING_INTERVAL)
             self.req_count = 0
@@ -510,7 +516,7 @@ class WireClient(object):
             resp = http_req(*args, **kwargs)
             if resp.status == httpclient.FORBIDDEN:
                 logger.warn("Sending too much request to wire server")
-                logger.info("Sleep {0} second to avoid throttling.", 
+                logger.info("Sleep {0} second to avoid throttling.",
                             LONG_WAITING_INTERVAL)
                 time.sleep(LONG_WAITING_INTERVAL)
             elif resp.status == httpclient.GONE:
@@ -530,12 +536,12 @@ class WireClient(object):
 
     def fetch_config(self, uri, headers):
         try:
-            resp = self.call_wireserver(restutil.http_get, uri, 
+            resp = self.call_wireserver(restutil.http_get, uri,
                                         headers=headers)
         except HttpError as e:
             raise ProtocolError(ustr(e))
 
-        if(resp.status != httpclient.OK):
+        if (resp.status != httpclient.OK):
             raise ProtocolError("{0} - {1}".format(resp.status, uri))
 
         return self.decode_config(resp.read())
@@ -562,7 +568,7 @@ class WireClient(object):
             resp = http_req(*args, **kwargs)
             if resp.status == httpclient.SERVICE_UNAVAILABLE:
                 logger.warn("Storage service is not avaible temporaryly")
-                logger.info("Will retry later, in {0} seconds", 
+                logger.info("Will retry later, in {0} seconds",
                             LONG_WAITING_INTERVAL)
                 time.sleep(LONG_WAITING_INTERVAL)
             else:
@@ -574,28 +580,27 @@ class WireClient(object):
         for version_uri in version_uris:
             logger.verb("Fetch ext handler manifest: {0}", version_uri.uri)
             try:
-                resp = self.call_storage_service(restutil.http_get, 
-                                                 version_uri.uri, None, 
+                resp = self.call_storage_service(restutil.http_get,
+                                                 version_uri.uri, None,
                                                  chk_proxy=True)
             except HttpError as e:
                 raise ProtocolError(ustr(e))
 
             if resp.status == httpclient.OK:
                 return self.decode_config(resp.read())
-            logger.warn("Failed to fetch ExtensionManifest: {0}, {1}", 
+            logger.warn("Failed to fetch ExtensionManifest: {0}, {1}",
                         resp.status, version_uri.uri)
-            logger.info("Will retry later, in {0} seconds", 
+            logger.info("Will retry later, in {0} seconds",
                         LONG_WAITING_INTERVAL)
             time.sleep(LONG_WAITING_INTERVAL)
         raise ProtocolError(("Failed to fetch ExtensionManifest from "
                              "all sources"))
 
-
     def update_hosting_env(self, goal_state):
         if goal_state.hosting_env_uri is None:
             raise ProtocolError("HostingEnvironmentConfig uri is empty")
         local_file = os.path.join(conf.get_lib_dir(), HOSTING_ENV_FILE_NAME)
-        xml_text = self.fetch_config(goal_state.hosting_env_uri, 
+        xml_text = self.fetch_config(goal_state.hosting_env_uri,
                                      self.get_header())
         self.save_cache(local_file, xml_text)
         self.hosting_env = HostingEnv(xml_text)
@@ -604,7 +609,7 @@ class WireClient(object):
         if goal_state.shared_conf_uri is None:
             raise ProtocolError("SharedConfig uri is empty")
         local_file = os.path.join(conf.get_lib_dir(), SHARED_CONF_FILE_NAME)
-        xml_text = self.fetch_config(goal_state.shared_conf_uri, 
+        xml_text = self.fetch_config(goal_state.shared_conf_uri,
                                      self.get_header())
         self.save_cache(local_file, xml_text)
         self.shared_conf = SharedConfig(xml_text)
@@ -613,7 +618,7 @@ class WireClient(object):
         if goal_state.certs_uri is None:
             return
         local_file = os.path.join(conf.get_lib_dir(), CERTS_FILE_NAME)
-        xml_text = self.fetch_config(goal_state.certs_uri, 
+        xml_text = self.fetch_config(goal_state.certs_uri,
                                      self.get_header_for_cert())
         self.save_cache(local_file, xml_text)
         self.certs = Certificates(self, xml_text)
@@ -624,31 +629,31 @@ class WireClient(object):
             self.ext_conf = ExtensionsConfig(None)
             return
         incarnation = goal_state.incarnation
-        local_file = os.path.join(conf.get_lib_dir(), 
-                                 EXT_CONF_FILE_NAME.format(incarnation))
+        local_file = os.path.join(conf.get_lib_dir(),
+                                  EXT_CONF_FILE_NAME.format(incarnation))
         xml_text = self.fetch_config(goal_state.ext_uri, self.get_header())
         self.save_cache(local_file, xml_text)
         self.ext_conf = ExtensionsConfig(xml_text)
-    
+
     def update_goal_state(self, forced=False, max_retry=3):
         uri = GOAL_STATE_URI.format(self.endpoint)
         xml_text = self.fetch_config(uri, self.get_header())
         goal_state = GoalState(xml_text)
 
-        incarnation_file = os.path.join(conf.get_lib_dir(), 
+        incarnation_file = os.path.join(conf.get_lib_dir(),
                                         INCARNATION_FILE_NAME)
 
         if not forced:
             last_incarnation = None
-            if(os.path.isfile(incarnation_file)):
+            if (os.path.isfile(incarnation_file)):
                 last_incarnation = fileutil.read_file(incarnation_file)
             new_incarnation = goal_state.incarnation
             if last_incarnation is not None and \
-                    last_incarnation == new_incarnation:
-                #Goalstate is not updated.
+                            last_incarnation == new_incarnation:
+                # Goalstate is not updated.
                 return
 
-        #Start updating goalstate, retry on 410
+        # Start updating goalstate, retry on 410
         for retry in range(0, max_retry):
             try:
                 self.goal_state = goal_state
@@ -669,8 +674,8 @@ class WireClient(object):
         raise ProtocolError("Exceeded max retry updating goal state")
 
     def get_goal_state(self):
-        if(self.goal_state is None):
-            incarnation_file = os.path.join(conf.get_lib_dir(), 
+        if (self.goal_state is None):
+            incarnation_file = os.path.join(conf.get_lib_dir(),
                                             INCARNATION_FILE_NAME)
             incarnation = self.fetch_cache(incarnation_file)
 
@@ -681,21 +686,21 @@ class WireClient(object):
         return self.goal_state
 
     def get_hosting_env(self):
-        if(self.hosting_env is None):
+        if (self.hosting_env is None):
             local_file = os.path.join(conf.get_lib_dir(), HOSTING_ENV_FILE_NAME)
             xml_text = self.fetch_cache(local_file)
             self.hosting_env = HostingEnv(xml_text)
         return self.hosting_env
 
     def get_shared_conf(self):
-        if(self.shared_conf is None):
+        if (self.shared_conf is None):
             local_file = os.path.join(conf.get_lib_dir(), SHARED_CONF_FILE_NAME)
             xml_text = self.fetch_cache(local_file)
             self.shared_conf = SharedConfig(xml_text)
         return self.shared_conf
 
     def get_certs(self):
-        if(self.certs is None):
+        if (self.certs is None):
             local_file = os.path.join(conf.get_lib_dir(), CERTS_FILE_NAME)
             xml_text = self.fetch_cache(local_file)
             self.certs = Certificates(self, xml_text)
@@ -704,7 +709,7 @@ class WireClient(object):
         return self.certs
 
     def get_ext_conf(self):
-        if(self.ext_conf is None):
+        if (self.ext_conf is None):
             goal_state = self.get_goal_state()
             if goal_state.ext_uri is None:
                 self.ext_conf = ExtensionsConfig(None)
@@ -730,7 +735,7 @@ class WireClient(object):
         xml_text = self.fetch_manifest(vmagent_manifest.versionsManifestUris)
         fileutil.write_file(local_file, xml_text)
         return ExtensionManifest(xml_text)
-    
+
     def check_wire_protocol_version(self):
         uri = VERSION_INFO_URI.format(self.endpoint)
         version_info_xml = self.fetch_config(uri, None)
@@ -746,11 +751,13 @@ class WireClient(object):
             error = ("Agent supported wire protocol version: {0} was not "
                      "advised by Fabric.").format(PROTOCOL_VERSION)
             raise ProtocolNotFoundError(error)
-   
+
     def upload_status_blob(self):
         ext_conf = self.get_ext_conf()
         if ext_conf.status_upload_blob is not None:
-            self.status_blob.upload(ext_conf.status_upload_blob)
+            if not self.status_blob.upload(ext_conf.status_upload_blob):
+                self.host_plugin.put_vm_status(self.status_blob,
+                                               ext_conf.status_upload_blob)
 
     def report_role_prop(self, thumbprint):
         goal_state = self.get_goal_state()
@@ -762,7 +769,7 @@ class WireClient(object):
         headers = self.get_header_for_xml_content()
         try:
             resp = self.call_wireserver(restutil.http_post, role_prop_uri,
-                                        role_prop, headers = headers)
+                                        role_prop, headers=headers)
         except HttpError as e:
             raise ProtocolError((u"Failed to send role properties: {0}"
                                  u"").format(e))
@@ -783,7 +790,7 @@ class WireClient(object):
         headers = self.get_header_for_xml_content()
         try:
             resp = self.call_wireserver(restutil.http_post, health_report_uri,
-                                        health_report, headers = headers, max_retry=8)
+                                        health_report, headers=headers, max_retry=8)
         except HttpError as e:
             raise ProtocolError((u"Failed to send provision status: {0}"
                                  u"").format(e))
@@ -795,8 +802,8 @@ class WireClient(object):
         uri = TELEMETRY_URI.format(self.endpoint)
         data_format = ('<?xml version="1.0"?>'
                        '<TelemetryData version="1.0">'
-                          '<Provider id="{0}">{1}'
-                          '</Provider>'
+                       '<Provider id="{0}">{1}'
+                       '</Provider>'
                        '</TelemetryData>')
         data = data_format.format(provider_id, event_str)
         try:
@@ -811,7 +818,7 @@ class WireClient(object):
 
     def report_event(self, event_list):
         buf = {}
-        #Group events by providerId
+        # Group events by providerId
         for event in event_list.events:
             if event.providerId not in buf:
                 buf[event.providerId] = ""
@@ -824,22 +831,22 @@ class WireClient(object):
                 buf[event.providerId] = ""
             buf[event.providerId] = buf[event.providerId] + event_str
 
-        #Send out all events left in buffer.
+        # Send out all events left in buffer.
         for provider_id in list(buf.keys()):
             if len(buf[provider_id]) > 0:
                 self.send_event(provider_id, buf[provider_id])
 
     def get_header(self):
         return {
-            "x-ms-agent-name":"WALinuxAgent",
-            "x-ms-version":PROTOCOL_VERSION
+            "x-ms-agent-name": "WALinuxAgent",
+            "x-ms-version": PROTOCOL_VERSION
         }
 
     def get_header_for_xml_content(self):
         return {
-            "x-ms-agent-name":"WALinuxAgent",
-            "x-ms-version":PROTOCOL_VERSION,
-            "Content-Type":"text/xml;charset=utf-8"
+            "x-ms-agent-name": "WALinuxAgent",
+            "x-ms-version": PROTOCOL_VERSION,
+            "Content-Type": "text/xml;charset=utf-8"
         }
 
     def get_header_for_cert(self):
@@ -848,12 +855,13 @@ class WireClient(object):
         content = self.fetch_cache(trans_cert_file)
         cert = get_bytes_from_pem(content)
         return {
-            "x-ms-agent-name":"WALinuxAgent",
-            "x-ms-version":PROTOCOL_VERSION,
+            "x-ms-agent-name": "WALinuxAgent",
+            "x-ms-version": PROTOCOL_VERSION,
             "x-ms-cipher-name": "DES_EDE3_CBC",
-            "x-ms-guest-agent-public-x509-cert":cert
+            "x-ms-guest-agent-public-x509-cert": cert
         }
 
+
 class VersionInfo(object):
     def __init__(self, xml_text):
         """
@@ -885,7 +893,6 @@ class VersionInfo(object):
 
 
 class GoalState(object):
-
     def __init__(self, xml_text):
         if xml_text is None:
             raise ValueError("GoalState.xml is None")
@@ -911,7 +918,7 @@ class GoalState(object):
         self.expected_state = findtext(xml_doc, "ExpectedState")
         self.hosting_env_uri = findtext(xml_doc, "HostingEnvironmentConfig")
         self.shared_conf_uri = findtext(xml_doc, "SharedConfig")
-        self.certs_uri = findtext(xml_doc, "Certificates") 
+        self.certs_uri = findtext(xml_doc, "Certificates")
         self.ext_uri = findtext(xml_doc, "ExtensionsConfig")
         role_instance = find(xml_doc, "RoleInstance")
         self.role_instance_id = findtext(role_instance, "InstanceId")
@@ -927,6 +934,7 @@ class HostingEnv(object):
     parse Hosting enviromnet config and store in
     HostingEnvironmentConfig.xml
     """
+
     def __init__(self, xml_text):
         if xml_text is None:
             raise ValueError("HostingEnvironmentConfig.xml is None")
@@ -950,10 +958,12 @@ class HostingEnv(object):
         self.deployment_name = getattrib(deployment, "name")
         return self
 
+
 class SharedConfig(object):
     """
     parse role endpoint server and goal state config.
     """
+
     def __init__(self, xml_text):
         logger.verb("Load SharedConfig.xml")
         self.parse(xml_text)
@@ -962,14 +972,15 @@ class SharedConfig(object):
         """
         parse and write configuration to file SharedConfig.xml.
         """
-        #Not used currently
+        # Not used currently
         return self
 
-class Certificates(object):
 
+class Certificates(object):
     """
     Object containing certificates of host and provisioned user.
     """
+
     def __init__(self, client, xml_text):
         logger.verb("Load Certificates.xml")
         self.client = client
@@ -984,7 +995,7 @@ class Certificates(object):
         data = findtext(xml_doc, "Data")
         if data is None:
             return
-        
+
         cryptutil = CryptUtil(conf.get_openssl_cmd())
         p7m_file = os.path.join(conf.get_lib_dir(), P7M_FILE_NAME)
         p7m = ("MIME-Version:1.0\n"
@@ -995,17 +1006,17 @@ class Certificates(object):
                "{2}").format(p7m_file, p7m_file, data)
 
         self.client.save_cache(p7m_file, p7m)
-        
-        trans_prv_file = os.path.join(conf.get_lib_dir(), 
+
+        trans_prv_file = os.path.join(conf.get_lib_dir(),
                                       TRANSPORT_PRV_FILE_NAME)
         trans_cert_file = os.path.join(conf.get_lib_dir(),
                                        TRANSPORT_CERT_FILE_NAME)
         pem_file = os.path.join(conf.get_lib_dir(), PEM_FILE_NAME)
-        #decrypt certificates
-        cryptutil.decrypt_p7m(p7m_file, trans_prv_file, trans_cert_file, 
-                              pem_file) 
+        # decrypt certificates
+        cryptutil.decrypt_p7m(p7m_file, trans_prv_file, trans_cert_file,
+                              pem_file)
 
-        #The parsing process use public key to match prv and crt.
+        # The parsing process use public key to match prv and crt.
         buf = []
         begin_crt = False
         begin_prv = False
@@ -1032,18 +1043,18 @@ class Certificates(object):
                     pub = cryptutil.get_pubkey_from_crt(tmp_file)
                     thumbprint = cryptutil.get_thumbprint_from_crt(tmp_file)
                     thumbprints[pub] = thumbprint
-                    #Rename crt with thumbprint as the file name
+                    # Rename crt with thumbprint as the file name
                     crt = "{0}.crt".format(thumbprint)
                     v1_cert_list.append({
-                        "name":None,
-                        "thumbprint":thumbprint
+                        "name": None,
+                        "thumbprint": thumbprint
                     })
                     os.rename(tmp_file, os.path.join(conf.get_lib_dir(), crt))
                     buf = []
                     index += 1
                     begin_crt = False
 
-        #Rename prv key with thumbprint as the file name
+        # Rename prv key with thumbprint as the file name
         for pubkey in prvs:
             thumbprint = thumbprints[pubkey]
             if thumbprint:
@@ -1057,7 +1068,7 @@ class Certificates(object):
             self.cert_list.certificates.append(cert)
 
     def write_to_tmp_file(self, index, suffix, buf):
-        file_name = os.path.join(conf.get_lib_dir(), 
+        file_name = os.path.join(conf.get_lib_dir(),
                                  "{0}.{1}".format(index, suffix))
         self.client.save_cache(file_name, "".join(buf))
         return file_name
@@ -1085,7 +1096,7 @@ class ExtensionsConfig(object):
 
         ga_families_list = find(xml_doc, "GAFamilies")
         ga_families = findall(ga_families_list, "GAFamily")
-        
+
         for ga_family in ga_families:
             family = findtext(ga_family, "Name")
             uris_list = find(ga_family, "Uris")
@@ -1131,13 +1142,13 @@ class ExtensionsConfig(object):
 
     def parse_plugin_settings(self, ext_handler, plugin_settings):
         if plugin_settings is None:
-            return 
+            return
 
         name = ext_handler.name
         version = ext_handler.properties.version
         settings = [x for x in plugin_settings \
-                          if getattrib(x, "name") == name and \
-                             getattrib(x ,"version") == version]
+                    if getattrib(x, "name") == name and \
+                    getattrib(x, "version") == version]
 
         if settings is None or len(settings) == 0:
             return
@@ -1155,8 +1166,8 @@ class ExtensionsConfig(object):
         for plugin_settings_list in runtime_settings["runtimeSettings"]:
             handler_settings = plugin_settings_list["handlerSettings"]
             ext = Extension()
-            #There is no "extension name" in wire protocol.
-            #Put
+            # There is no "extension name" in wire protocol.
+            # Put
             ext.name = ext_handler.name
             ext.sequenceNumber = seqNo
             ext.publicSettings = handler_settings.get("publicSettings")
@@ -1165,6 +1176,7 @@ class ExtensionsConfig(object):
             ext.certificateThumbprint = thumbprint
             ext_handler.properties.extensions.append(ext)
 
+
 class ExtensionManifest(object):
     def __init__(self, xml_text):
         if xml_text is None:
diff --git a/azurelinuxagent/common/utils/textutil.py b/azurelinuxagent/common/utils/textutil.py
index 4bdd177b..6e03d6c3 100644
--- a/azurelinuxagent/common/utils/textutil.py
+++ b/azurelinuxagent/common/utils/textutil.py
@@ -16,23 +16,25 @@
 #
 # Requires Python 2.4+ and Openssl 1.0+
 
+import base64
 import crypt
 import random
 import string
 import struct
 import xml.dom.minidom as minidom
-import sys
 from distutils.version import LooseVersion
 
+
 def parse_doc(xml_text):
     """
     Parse xml document from string
     """
-    #The minidom lib has some issue with unicode in python2.
-    #Encode the string into utf-8 first
+    # The minidom lib has some issue with unicode in python2.
+    # Encode the string into utf-8 first
     xml_text = xml_text.encode('utf-8')
     return minidom.parseString(xml_text)
 
+
 def findall(root, tag, namespace=None):
     """
     Get all nodes by tag and namespace under Node root.
@@ -45,6 +47,7 @@ def findall(root, tag, namespace=None):
     else:
         return root.getElementsByTagNameNS(namespace, tag)
 
+
 def find(root, tag, namespace=None):
     """
     Get first node by tag and namespace under Node root.
@@ -55,18 +58,20 @@ def find(root, tag, namespace=None):
     else:
         return None
 
+
 def gettext(node):
     """
     Get node text
     """
     if node is None:
         return None
-    
+
     for child in node.childNodes:
         if child.nodeType == child.TEXT_NODE:
             return child.data
     return None
 
+
 def findtext(root, tag, namespace=None):
     """
     Get text of node by tag and namespace under Node root.
@@ -74,6 +79,7 @@ def findtext(root, tag, namespace=None):
     node = find(root, tag, namespace=namespace)
     return gettext(node)
 
+
 def getattrib(node, attr_name):
     """
     Get attribute of xml node
@@ -83,6 +89,7 @@ def getattrib(node, attr_name):
     else:
         return None
 
+
 def unpack(buf, offset, range):
     """
     Unpack bytes into python values.
@@ -92,43 +99,50 @@ def unpack(buf, offset, range):
         result = (result << 8) | str_to_ord(buf[offset + i])
     return result
 
+
 def unpack_little_endian(buf, offset, length):
     """
     Unpack little endian bytes into python values.
     """
     return unpack(buf, offset, list(range(length - 1, -1, -1)))
 
+
 def unpack_big_endian(buf, offset, length):
     """
     Unpack big endian bytes into python values.
     """
     return unpack(buf, offset, list(range(0, length)))
 
+
 def hex_dump3(buf, offset, length):
     """
     Dump range of buf in formatted hex.
     """
     return ''.join(['%02X' % str_to_ord(char) for char in buf[offset:offset + length]])
 
+
 def hex_dump2(buf):
     """
     Dump buf in formatted hex.
     """
     return hex_dump3(buf, 0, len(buf))
 
+
 def is_in_range(a, low, high):
     """
     Return True if 'a' in 'low' <= a >= 'high'
     """
     return (a >= low and a <= high)
 
+
 def is_printable(ch):
     """
     Return True if character is displayable.
     """
     return (is_in_range(ch, str_to_ord('A'), str_to_ord('Z'))
-           or is_in_range(ch, str_to_ord('a'), str_to_ord('z'))
-           or is_in_range(ch, str_to_ord('0'), str_to_ord('9')))
+            or is_in_range(ch, str_to_ord('a'), str_to_ord('z'))
+            or is_in_range(ch, str_to_ord('0'), str_to_ord('9')))
+
 
 def hex_dump(buffer, size):
     """
@@ -155,7 +169,7 @@ def hex_dump(buffer, size):
                 j += 1
             result += " "
             for j in range(i - (i % 16), i + 1):
-                byte=buffer[j]
+                byte = buffer[j]
                 if type(byte) == str:
                     byte = str_to_ord(byte.decode('latin1'))
                 k = '.'
@@ -166,6 +180,7 @@ def hex_dump(buffer, size):
                 result += "\n"
     return result
 
+
 def str_to_ord(a):
     """
     Allows indexing into a string or an array of integers transparently.
@@ -175,12 +190,14 @@ def str_to_ord(a):
         a = ord(a)
     return a
 
+
 def compare_bytes(a, b, start, length):
     for offset in range(start, start + length):
         if str_to_ord(a[offset]) != str_to_ord(b[offset]):
             return False
     return True
 
+
 def int_to_ip4_addr(a):
     """
     Build DHCP request string.
@@ -190,6 +207,7 @@ def int_to_ip4_addr(a):
                             (a >> 8) & 0xFF,
                             (a) & 0xFF)
 
+
 def hexstr_to_bytearray(a):
     """
     Return hex string packed into a binary struct.
@@ -199,6 +217,7 @@ def hexstr_to_bytearray(a):
         b += struct.pack("B", int(a[c * 2:c * 2 + 2], 16))
     return b
 
+
 def set_ssh_config(config, name, val):
     notfound = True
     for i in range(0, len(config)):
@@ -206,7 +225,7 @@ def set_ssh_config(config, name, val):
             config[i] = "{0} {1}".format(name, val)
             notfound = False
         elif config[i].startswith("Match"):
-            #Match block must be put in the end of sshd config
+            # Match block must be put in the end of sshd config
             break
     if notfound:
         config.insert(i, "{0} {1}".format(name, val))
@@ -231,16 +250,18 @@ def set_ini_config(config, name, val):
 
 def remove_bom(c):
     if str_to_ord(c[0]) > 128 and str_to_ord(c[1]) > 128 and \
-            str_to_ord(c[2]) > 128:
+                    str_to_ord(c[2]) > 128:
         c = c[3:]
     return c
 
+
 def gen_password_hash(password, crypt_id, salt_len):
     collection = string.ascii_letters + string.digits
     salt = ''.join(random.choice(collection) for _ in range(salt_len))
     salt = "${0}${1}".format(crypt_id, salt)
     return crypt.crypt(password, salt)
 
+
 def get_bytes_from_pem(pem_str):
     base64_bytes = ""
     for line in pem_str.split('\n'):
@@ -249,5 +270,11 @@ def get_bytes_from_pem(pem_str):
     return base64_bytes
 
 
-Version = LooseVersion
+def b64encode(s):
+    from azurelinuxagent.common.version import PY_VERSION_MAJOR
+    if PY_VERSION_MAJOR > 2:
+        return base64.b64encode(bytes(s, 'utf-8')).decode('utf-8')
+    return base64.b64encode(s)
+
 
+Version = LooseVersion
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/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+
+#
+


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/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py
new file mode 100644
index 00000000..65c84659
--- /dev/null
+++ b/tests/protocol/test_hostplugin.py
@@ -0,0 +1,85 @@
+# 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+
+#
+
+from tests.tools import *
+import unittest
+import azurelinuxagent.common.protocol.wire as wire
+import azurelinuxagent.common.protocol.restapi as restapi
+
+wireserver_url = "168.63.129.16"
+sas_url = "http://sas_url"
+api_versions = '["2015-09-01"]'
+
+
+class TestHostPlugin(AgentTestCase):
+    def test_fallback(self):
+        with patch.object(wire.HostPluginProtocol,
+                          "put_vm_status") as patch_put:
+            with patch.object(wire.StatusBlob, "upload") as patch_upload:
+                patch_upload.return_value = False
+                wire_protocol_client = wire.WireProtocol(wireserver_url).client
+                wire_protocol_client.ext_conf = wire.ExtensionsConfig(None)
+                wire_protocol_client.ext_conf.status_upload_blob = sas_url
+                wire_protocol_client.upload_status_blob()
+                self.assertTrue(patch_put.call_count == 1,
+                                "Fallback was not engaged")
+                self.assertTrue(patch_put.call_args[0][1] == sas_url)
+
+    def test_no_fallback(self):
+        with patch.object(wire.HostPluginProtocol,
+                          "put_vm_status") as patch_put:
+            with patch.object(wire.StatusBlob, "upload") as patch_upload:
+                patch_upload.return_value = True
+                wire_protocol_client = wire.WireProtocol(wireserver_url).client
+                wire_protocol_client.ext_conf = wire.ExtensionsConfig(None)
+                wire_protocol_client.ext_conf.status_upload_blob = sas_url
+                wire_protocol_client.upload_status_blob()
+                self.assertTrue(patch_put.call_count == 0,
+                                "Fallback was engaged")
+
+    def test_init_put(self):
+        expected_url = "http://168.63.129.16:32526/status"
+        expected_headers = {'x-ms-version': '2015-09-01'}
+        expected_content = '{"content": "b2s=", ' \
+                           '"headers": [{"headerName": "x-ms-version", ' \
+                           '"headerValue": "2014-02-14"}, ' \
+                           '{"headerName": "x-ms-blob-type", "headerValue": ' \
+                           '"BlockBlob"}], ' \
+                           '"requestUri": "http://sas_url"}'
+
+        host_client = wire.HostPluginProtocol(wireserver_url)
+        self.assertFalse(host_client.is_initialized)
+        self.assertTrue(host_client.api_versions is None)
+        status_blob = wire.StatusBlob(None)
+        status_blob.vm_status = "ok"
+        status_blob.type = "BlockBlob"
+        with patch.object(wire.HostPluginProtocol,
+                          "get_api_versions") as patch_get:
+            patch_get.return_value = api_versions
+            with patch.object(restapi.restutil, "http_put") as patch_put:
+                patch_put.return_value = MagicMock()
+                host_client.put_vm_status(status_blob, sas_url)
+                self.assertTrue(host_client.is_initialized)
+                self.assertFalse(host_client.api_versions is None)
+                self.assertTrue(patch_put.call_count == 1)
+                self.assertTrue(patch_put.call_args[0][0] == expected_url)
+                self.assertTrue(patch_put.call_args[0][1] == expected_content)
+                self.assertTrue(patch_put.call_args[0][2] == expected_headers)
+
+
+if __name__ == '__main__':
+    unittest.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):