Repository: Azure/WALinuxAgent
Pull request #625 (2017-03-26 01:00:21)

Enabled PageBlob support through HostGAPlugin
Signed-off-by: Brendan Dixon <brendandixon@me.com>


Patch:
diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py
index bdae56e2..f4ece298 100644
--- a/azurelinuxagent/common/protocol/hostplugin.py
+++ b/azurelinuxagent/common/protocol/hostplugin.py
@@ -16,8 +16,12 @@
 #
 # Requires Python 2.4+ and Openssl 1.0+
 #
+
+import base64
+
 from azurelinuxagent.common.protocol.wire import *
 from azurelinuxagent.common.utils import textutil
+from azurelinuxagent.common.version import PY_VERSION_MAJOR
 
 HOST_PLUGIN_PORT = 32526
 URI_FORMAT_GET_API_VERSIONS = "http://{0}:{1}/versions"
@@ -30,12 +34,13 @@ HEADER_VERSION = "x-ms-version"
 HEADER_HOST_CONFIG_NAME = "x-ms-host-config-name"
 HEADER_ARTIFACT_LOCATION = "x-ms-artifact-location"
 HEADER_ARTIFACT_MANIFEST_LOCATION = "x-ms-artifact-manifest-location"
+MAXIMUM_PAGEBLOB_PAGE_SIZE = 4 * 1024 * 1024  # Max page size: 4MB
 
 
 class HostPluginProtocol(object):
     def __init__(self, endpoint, container_id, role_config_name):
         if endpoint is None:
-            raise ProtocolError("Host plugin endpoint not provided")
+            raise ProtocolError("HostGAPlugin: Endpoint not provided")
         self.is_initialized = False
         self.is_available = False
         self.api_versions = None
@@ -60,29 +65,31 @@ class HostPluginProtocol(object):
     def get_api_versions(self):
         url = URI_FORMAT_GET_API_VERSIONS.format(self.endpoint,
                                                  HOST_PLUGIN_PORT)
-        logger.verbose("getting API versions at [{0}]".format(url))
+        logger.verbose("HostGAPlugin: Getting API versions at [{0}]".format(
+            url))
         return_val = []
         try:
             headers = {HEADER_CONTAINER_ID: self.container_id}
             response = restutil.http_get(url, headers)
             if response.status != httpclient.OK:
                 logger.error(
-                    "get API versions returned status code [{0}]".format(
-                        response.status))
+                    "HostGAPlugin: Failed Get API versions: {0}".format(
+                        self._read_response_error(response)))
             else:
                 return_val = ustr(remove_bom(response.read()), encoding='utf-8')
 
         except HttpError as e:
-            logger.error("get API versions failed with [{0}]".format(e))
+            logger.error("HostGAPlugin: Exception Get API versions: " \
+                "{0}".format(e))
 
         return return_val
 
     def get_artifact_request(self, artifact_url, artifact_manifest_url=None):
         if not self.ensure_initialized():
-            logger.error("host plugin channel is not available")
+            logger.error("HostGAPlugin: Host plugin channel is not available")
             return None, None
         if textutil.is_str_none_or_whitespace(artifact_url):
-            logger.error("no extension artifact url was provided")
+            logger.error("HostGAPlugin: No extension artifact url was provided")
             return None, None
 
         url = URI_FORMAT_GET_EXTENSION_ARTIFACT.format(self.endpoint,
@@ -97,45 +104,6 @@ class HostPluginProtocol(object):
 
         return url, headers
 
-    def put_vm_status(self, status_blob, sas_url, config_blob_type=None):
-        """
-        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
-        :param config_blob_type: the blob type from the extension config
-        :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
-        try:
-            url = URI_FORMAT_PUT_VM_STATUS.format(self.endpoint, HOST_PLUGIN_PORT)
-            logger.verbose("Posting VM status to host plugin")
-            status = textutil.b64encode(status_blob.data)
-            blob_type = status_blob.type if status_blob.type else config_blob_type
-            headers = {HEADER_VERSION: API_VERSION,
-                       "Content-type": "application/json",
-                       HEADER_CONTAINER_ID: self.container_id,
-                       HEADER_HOST_CONFIG_NAME: self.role_config_name}
-            blob_headers = [{'headerName': 'x-ms-version',
-                             'headerValue': status_blob.__storage_version__},
-                            {'headerName': 'x-ms-blob-type',
-                             'headerValue': blob_type}]
-            data = json.dumps({'requestUri': sas_url, 'headers': blob_headers,
-                               'content': status}, sort_keys=True)
-            response = restutil.http_put(url, data=data, headers=headers)
-            if response.status != httpclient.OK:
-                logger.warn("PUT {0} [{1}: {2}]",
-                            url,
-                            response.status,
-                            response.reason)
-            else:
-                logger.verbose("Successfully uploaded status to host plugin")
-        except Exception as e:
-            logger.error("Put VM status failed [{0}]", e)
-
     def put_vm_log(self, content):
         """
         Try to upload the given content to the host plugin
@@ -147,13 +115,13 @@ class HostPluginProtocol(object):
         :return:
         """
         if not self.ensure_initialized():
-            logger.error("host plugin channel is not available")
+            logger.error("HostGAPlugin: Host plugin channel is not available")
             return
         if content is None \
                 or self.container_id is None \
                 or self.deployment_id is None:
             logger.error(
-                "invalid arguments passed: "
+                "HostGAPlugin: Invalid arguments passed: "
                 "[{0}], [{1}], [{2}]".format(
                     content,
                     self.container_id,
@@ -163,11 +131,146 @@ class HostPluginProtocol(object):
 
         headers = {"x-ms-vmagentlog-deploymentid": self.deployment_id,
                    "x-ms-vmagentlog-containerid": self.container_id}
-        logger.info("put VM log at [{0}]".format(url))
+        logger.info("HostGAPlugin: Put VM log to [{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(
+                logger.error("HostGAPlugin: Put log failed: Code {0}".format(
                     response.status))
         except HttpError as e:
-            logger.error("put log failed with [{0}]".format(e))
+            logger.error("HostGAPlugin: Put log exception: {0}".format(e))
+
+    def put_vm_status(self, status_blob, sas_url, config_blob_type=None):
+        """
+        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
+        :param config_blob_type: the blob type from the extension config
+        :type status_blob: StatusBlob
+        """
+        if not self.ensure_initialized():
+            logger.error("HostGAPlugin: HostGAPlugin is not available")
+            return
+        if status_blob is None or status_blob.vm_status is None:
+            logger.error("HostGAPlugin: Status blob was not provided")
+            return
+
+        logger.verbose("HostGAPlugin: Posting VM status")
+        try:
+            blob_type = status_blob.type if status_blob.type else config_blob_type
+
+            if blob_type == "BlockBlob":
+                self._put_block_blob_status(sas_url, status_blob)
+            else:
+                self._put_page_blob_status(sas_url, status_blob)
+
+        except Exception as e:
+            logger.error("HostGAPlugin: Exception Put VM status: {0}", e)
+    
+    def _put_block_blob_status(self, sas_url, status_blob):
+        url = URI_FORMAT_PUT_VM_STATUS.format(self.endpoint, HOST_PLUGIN_PORT)
+
+        response = restutil.http_put(url,
+                        data=self._build_status_data(
+                                    sas_url,
+                                    status_blob.get_block_blob_headers(len(status_blob.data)),
+                                    bytearray(status_blob.data, encoding='utf-8')),
+                        headers=self._build_status_headers())
+
+        if response.status != httpclient.OK:
+            raise HttpError("HostGAPlugin: Put BlockBlob failed: {0}".format(
+                self._read_response_error(response)))
+        else:
+            logger.verbose("HostGAPlugin: Put BlockBlob status succeeded")
+
+    def _put_page_blob_status(self, sas_url, status_blob):
+        url = URI_FORMAT_PUT_VM_STATUS.format(self.endpoint, HOST_PLUGIN_PORT)
+
+        # Convert the status into a blank-padded string whose length is modulo 512
+        status = bytearray(status_blob.data, encoding='utf-8')
+        status_size = int((len(status) + 511) / 512) * 512
+        status = bytearray(status_blob.data.ljust(status_size), encoding='utf-8')
+
+        # First, initialize an empty blob
+        response = restutil.http_put(url,
+                        data=self._build_status_data(
+                                    sas_url,
+                                    status_blob.get_page_blob_create_headers(status_size)),
+                        headers=self._build_status_headers())
+
+        if response.status != httpclient.OK:
+            raise HttpError(
+                "HostGAPlugin: Failed PageBlob clean-up: {0}".format(
+                    self._read_response_error(response)))
+        else:
+            logger.verbose("HostGAPlugin: PageBlob clean-up succeeded")
+        
+        # Then, upload the blob in pages
+        if sas_url.count("?") <= 0:
+            sas_url = "{0}?comp=page".format(sas_url)
+        else:
+            sas_url = "{0}&comp=page".format(sas_url)
+
+        start = 0
+        end = 0
+        while start < len(status):
+            # Create the next page
+            end = start + min(len(status) - start, MAXIMUM_PAGEBLOB_PAGE_SIZE)
+            page_size = int((end - start + 511) / 512) * 512
+            buf = bytearray(page_size)
+            buf[0: end - start] = status[start: end]
+
+            # Send the page
+            response = restutil.http_put(url,
+                            data=self._build_status_data(
+                                        sas_url,
+                                        status_blob.get_page_blob_page_headers(start, end),
+                                        buf),
+                            headers=self._build_status_headers())
+
+            if response.status != httpclient.OK:
+                raise HttpError(
+                    "HostGAPlugin Error: Put PageBlob bytes [{0},{1}]: " \
+                    "{2}".format(
+                        start, end, self._read_response_error(response)))
+
+            # Advance to the next page (if any)
+            start = end
+        
+    def _build_status_data(self, sas_url, blob_headers, content=None):
+        headers = []
+        for name in iter(blob_headers.keys()):
+            headers.append({
+                'headerName': name,
+                'headerValue': blob_headers[name]
+            })
+
+        data = {
+            'requestUri': sas_url,
+            'headers': headers
+        }
+        if not content is None:
+            data['content'] = self._base64_encode(content)
+        return json.dumps(data, sort_keys=True)
+    
+    def _build_status_headers(self):
+        return {
+            HEADER_VERSION: API_VERSION,
+            "Content-type": "application/json",
+            HEADER_CONTAINER_ID: self.container_id,
+            HEADER_HOST_CONFIG_NAME: self.role_config_name
+        }
+    
+    def _base64_encode(self, data):
+        s = base64.b64encode(bytes(data))
+        if PY_VERSION_MAJOR > 2:
+            return s.decode('utf-8')
+        return s
+    
+    def _read_response_error(self, response):
+        body = remove_bom(response.read())
+        if PY_VERSION_MAJOR < 3:
+            body = ustr(body, encoding='utf-8')
+        return "{0}, {1}, {2}".format(
+            response.status,
+            response.reason,
+            body)
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index 71c3e37f..db343909 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -411,48 +411,54 @@ class StatusBlob(object):
         logger.verbose("Blob type: [{0}]", blob_type)
         return blob_type
 
+    def get_block_blob_headers(self, blob_size):
+        return {
+            "Content-Length": ustr(blob_size),
+            "x-ms-blob-type": "BlockBlob",
+            "x-ms-date": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+            "x-ms-version": self.__class__.__storage_version__
+        }
+
     def put_block_blob(self, url, data):
         logger.verbose("Put block blob")
-        timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-        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__
-            })
+        headers = self.get_block_blob_headers(len(data))
+        resp = self.client.call_storage_service(restutil.http_put, url, data, headers)
         if resp.status != httpclient.CREATED:
             raise UploadError(
                 "Failed to upload block blob: {0}".format(resp.status))
 
+    def get_page_blob_create_headers(self, blob_size):
+        return {
+            "Content-Length": "0",
+            "x-ms-blob-content-length": ustr(blob_size),
+            "x-ms-blob-type": "PageBlob",
+            "x-ms-date": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+            "x-ms-version": self.__class__.__storage_version__
+        }
+
+    def get_page_blob_page_headers(self, start, end):
+        return {
+            "Content-Length": ustr(end - start),
+            "x-ms-date": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+            "x-ms-range": "bytes={0}-{1}".format(start, end - 1),
+            "x-ms-page-write": "update",
+            "x-ms-version": self.__class__.__storage_version__
+        }
+
     def put_page_blob(self, url, data):
         logger.verbose("Put page blob")
 
-        # Convert string into bytes
+        # Convert string into bytes and align to 512 bytes
         data = bytearray(data, encoding='utf-8')
-        timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-
-        # Align to 512 bytes
         page_blob_size = int((len(data) + 511) / 512) * 512
-        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__
-            })
+
+        headers = self.get_page_blob_create_headers(page_blob_size)
+        resp = self.client.call_storage_service(restutil.http_put, url, "", headers)
         if resp.status != httpclient.CREATED:
             raise UploadError(
                 "Failed to clean up page blob: {0}".format(resp.status))
 
-        if url.count("?") < 0:
+        if url.count("?") <= 0:
             url = "{0}?comp=page".format(url)
         else:
             url = "{0}&comp=page".format(url)
@@ -469,17 +475,12 @@ class StatusBlob(object):
             buf_size = page_end - start
             buf = bytearray(buf_size)
             buf[0: content_size] = data[start: end]
+            headers = self.get_page_blob_page_headers(start, page_end)
             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)
-                })
+                headers)
             if resp is None or resp.status != httpclient.CREATED:
                 raise UploadError(
                     "Failed to upload page blob: {0}".format(resp.status))
diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py
index 30b751c0..0e3818aa 100644
--- a/azurelinuxagent/common/version.py
+++ b/azurelinuxagent/common/version.py
@@ -88,7 +88,7 @@ def get_distro():
 
 AGENT_NAME = "WALinuxAgent"
 AGENT_LONG_NAME = "Azure Linux Agent"
-AGENT_VERSION = '2.2.6'
+AGENT_VERSION = '2.2.7'
 AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION)
 AGENT_DESCRIPTION = """\
 The Azure Linux Agent supports the provisioning and running of Linux
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index 9b99d041..3d3807d3 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -299,7 +299,7 @@ class ExtHandlersHandler(object):
         try:
             self.protocol.report_vm_status(vm_status)
             if self.log_report:
-                logger.verbose("Successfully reported vm agent status")
+                logger.verbose("Completed vm agent status report")
         except ProtocolError as e:
             message = "Failed to report vm agent status: {0}".format(e)
             add_event(AGENT_NAME, version=CURRENT_VERSION, is_success=False, message=message)


Test patch:
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index 5277e594..46502cc7 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -522,7 +522,8 @@ class TestGuestAgent(UpdateTestCase):
         self.assertFalse(os.path.isdir(self.agent_path))
 
         mock_http_get.return_value = ResponseMock(
-            status=restutil.httpclient.SERVICE_UNAVAILABLE)
+            status=restutil.httpclient.SERVICE_UNAVAILABLE,
+            response="")
 
         ext_uri = 'ext_uri'
         host_uri = 'host_uri'
@@ -1371,9 +1372,10 @@ class ProtocolMock(object):
 
 
 class ResponseMock(Mock):
-    def __init__(self, status=restutil.httpclient.OK, response=None):
+    def __init__(self, status=restutil.httpclient.OK, response=None, reason=None):
         Mock.__init__(self)
         self.status = status
+        self.reason = reason
         self.response = response
         return
 
diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py
index 1298fdc7..078af706 100644
--- a/tests/protocol/test_hostplugin.py
+++ b/tests/protocol/test_hostplugin.py
@@ -15,21 +15,102 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+import base64
+import sys
+import json
 import unittest
 
+if sys.version_info[0]== 3:
+    import http.client as httpclient
+    bytebuffer = memoryview
+elif sys.version_info[0] == 2:
+    import httplib as httpclient
+    bytebuffer = buffer
+
 import azurelinuxagent.common.protocol.restapi as restapi
 import azurelinuxagent.common.protocol.wire as wire
 import azurelinuxagent.common.protocol.hostplugin as hostplugin
+
+from azurelinuxagent.common.protocol.hostplugin import API_VERSION
+from azurelinuxagent.common.utils import restutil
+from azurelinuxagent.common.utils import textutil
+from azurelinuxagent.common.version import PY_VERSION_MAJOR
+
 from tests.protocol.mockwiredata import WireProtocolData, DATA_FILE
 from tests.tools import *
 
-wireserver_url = "168.63.129.16"
+hostplugin_status_url = "http://168.63.129.16:32526/status"
 sas_url = "http://sas_url"
-testtype = 'BlockBlob'
+wireserver_url = "168.63.129.16"
+
+block_blob_type = 'BlockBlob'
+page_blob_type = 'PageBlob'
+
 api_versions = '["2015-09-01"]'
+storage_version = "2014-02-14"
 
+faux_status = "{ 'dummy' : 'data' }"
+faux_status_b64 = base64.b64encode(bytes(bytearray(faux_status, encoding='utf-8')))
+if PY_VERSION_MAJOR > 2:
+    faux_status_b64 = faux_status_b64.decode('utf-8')
 
 class TestHostPlugin(AgentTestCase):
+
+    def _compare_data(self, actual, expected):
+        for k in iter(expected.keys()):
+            if k == 'content' or k == 'requestUri':
+                if actual[k] != expected[k]:
+                    print("Mismatch: Actual '{0}'='{1}', " \
+                        "Expected '{0}'='{3}'".format(
+                            k, actual[k], expected[k]))
+                    return False
+            elif k == 'headers':
+                for h in expected['headers']:
+                    if not (h in actual['headers']):
+                        print("Missing Header: '{0}'".format(h))
+                        return False
+            else:
+                print("Unexpected Key: '{0}'".format(k))
+                return False
+        return True
+
+    def _hostplugin_data(self, blob_headers, content=None):
+        headers = []
+        for name in iter(blob_headers.keys()):
+            headers.append({
+                'headerName': name,
+                'headerValue': blob_headers[name]
+            })
+
+        data = {
+            'requestUri': sas_url,
+            'headers': headers
+        }
+        if not content is None:
+            s = base64.b64encode(bytes(content))
+            if PY_VERSION_MAJOR > 2:
+                s = s.decode('utf-8')
+            data['content'] = s
+        return data
+    
+    def _hostplugin_headers(self, goal_state):
+        return {
+            'x-ms-version': '2015-09-01',
+            'Content-type': 'application/json',
+            'x-ms-containerid': goal_state.container_id,
+            'x-ms-host-config-name': goal_state.role_config_name
+        }
+    
+    def _validate_hostplugin_args(self, args, goal_state, exp_method, exp_url, exp_data):
+        args, kwargs = args
+        self.assertEqual(exp_method, args[0])
+        self.assertEqual(exp_url, args[1])
+        self.assertTrue(self._compare_data(json.loads(args[2]), exp_data))
+
+        headers = kwargs['headers']
+        self.assertEqual(headers['x-ms-containerid'], goal_state.container_id)
+        self.assertEqual(headers['x-ms-host-config-name'], goal_state.role_config_name)
+
     def test_fallback(self):
         """
         Validate fallback to upload status using HostGAPlugin is happening when status reporting via
@@ -50,35 +131,33 @@ class TestHostPlugin(AgentTestCase):
 
     def test_validate_http_request(self):
         """Validate correct set of data is sent to HostGAPlugin when reporting VM status"""
-        from azurelinuxagent.common.protocol.hostplugin import API_VERSION
-        from azurelinuxagent.common.utils import restutil
-        exp_method = 'PUT'
-        exp_url = 'http://{0}:32526/status'.format(wireserver_url)
-        exp_data = '{"content": "eyJkdW1teSI6ICJkYXRhIn0=", "headers": [{"headerName": ' \
-                   '"x-ms-version", "headerValue": "2014-02-14"}, ' \
-                   '{"headerName": "x-ms-blob-type", "headerValue": "BlockBlob"}], ' \
-                   '"requestUri": "http://sas_url"}'
+
+        wire_protocol_client = wire.WireProtocol(wireserver_url).client
         test_goal_state = wire.GoalState(WireProtocolData(DATA_FILE).goal_state)
 
+        status_blob = wire_protocol_client.status_blob
+        status_blob.data = faux_status
+        status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready")
+
+        exp_method = 'PUT'
+        exp_url = hostplugin_status_url
+        exp_data = self._hostplugin_data(
+                        status_blob.get_block_blob_headers(len(faux_status)),
+                        bytearray(faux_status, encoding='utf-8'))
+
         with patch.object(restutil, "http_request") as patch_http:
-            wire_protocol_client = wire.WireProtocol(wireserver_url).client
             wire_protocol_client.get_goal_state = Mock(return_value=test_goal_state)
             plugin = wire_protocol_client.get_host_plugin()
-            blob = wire_protocol_client.status_blob
-            blob.vm_status = restapi.VMStatus(message="Ready", status="Ready")
-            blob.data = '{"dummy": "data"}'
+
             with patch.object(plugin, 'get_api_versions') as patch_api:
                 patch_api.return_value = API_VERSION
-                plugin.put_vm_status(blob, sas_url, testtype)
-                self.assertTrue(patch_http.call_count == 1)
-                self.assertTrue(patch_http.call_args[0][0] == exp_method)
-                self.assertTrue(patch_http.call_args[0][1] == exp_url)
-                self.assertTrue(patch_http.call_args[0][2] == exp_data)
+                plugin.put_vm_status(status_blob, sas_url, block_blob_type)
 
-                # Assert headers
-                headers = patch_http.call_args[1]['headers']
-                self.assertEqual(headers['x-ms-containerid'], test_goal_state.container_id)
-                self.assertEqual(headers['x-ms-host-config-name'], test_goal_state.role_config_name)
+                self.assertTrue(patch_http.call_count == 1)
+                self._validate_hostplugin_args(
+                    patch_http.call_args_list[0],
+                    test_goal_state,
+                    exp_method, exp_url, exp_data)
 
     def test_no_fallback(self):
         """
@@ -96,42 +175,93 @@ class TestHostPlugin(AgentTestCase):
                 self.assertTrue(patch_put.call_count == 0,
                                 "Fallback was engaged")
 
-    def test_validate_http_put(self):
+    def test_validate_block_blob(self):
         """Validate correct set of data is sent to HostGAPlugin when reporting VM status"""
+        wire_protocol_client = wire.WireProtocol(wireserver_url).client
         test_goal_state = wire.GoalState(WireProtocolData(DATA_FILE).goal_state)
-        expected_url = "http://168.63.129.16:32526/status"
-        expected_headers = {'x-ms-version': '2015-09-01',
-                            "Content-type": "application/json",
-                            "x-ms-containerid": test_goal_state.container_id,
-                            "x-ms-host-config-name": test_goal_state.role_config_name}
-        expected_content = '{"content": "eyJkdW1teSI6ICJkYXRhIn0=", ' \
-                           '"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,
                                               test_goal_state.container_id,
                                               test_goal_state.role_config_name)
         self.assertFalse(host_client.is_initialized)
         self.assertTrue(host_client.api_versions is None)
-        status_blob = wire.StatusBlob(None)
+
+        status_blob = wire_protocol_client.status_blob
+        status_blob.data = faux_status
+        status_blob.type = block_blob_type
         status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready")
-        status_blob.data = '{"dummy": "data"}'
-        status_blob.type = "BlockBlob"
-        with patch.object(wire.HostPluginProtocol,
+
+        exp_method = 'PUT'
+        exp_url = hostplugin_status_url
+        exp_data = self._hostplugin_data(
+                        status_blob.get_block_blob_headers(len(faux_status)),
+                        bytearray(faux_status, encoding='utf-8'))
+
+        with patch.object(restutil, "http_request") as patch_http:
+            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()
+                patch_get.return_value = api_versions
+                host_client.put_vm_status(status_blob, sas_url)
+
+                self.assertTrue(patch_http.call_count == 1)
+                self._validate_hostplugin_args(
+                    patch_http.call_args_list[0],
+                    test_goal_state,
+                    exp_method, exp_url, exp_data)
+    
+    def test_validate_page_blobs(self):
+        """Validate correct set of data is sent for page blobs"""
+        wire_protocol_client = wire.WireProtocol(wireserver_url).client
+        test_goal_state = wire.GoalState(WireProtocolData(DATA_FILE).goal_state)
+
+        host_client = wire.HostPluginProtocol(wireserver_url,
+                                              test_goal_state.container_id,
+                                              test_goal_state.role_config_name)
+
+        self.assertFalse(host_client.is_initialized)
+        self.assertTrue(host_client.api_versions is None)
+
+        status_blob = wire_protocol_client.status_blob
+        status_blob.data = faux_status
+        status_blob.type = page_blob_type
+        status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready")
+
+        exp_method = 'PUT'
+        exp_url = hostplugin_status_url
+
+        page_status = bytearray(status_blob.data, encoding='utf-8')
+        page_size = int((len(page_status) + 511) / 512) * 512
+        page_status = bytearray(status_blob.data.ljust(page_size), encoding='utf-8')
+        page = bytearray(page_size)
+        page[0: page_size] = page_status[0: len(page_status)]
+        mock_response = MockResponse('', httpclient.OK)
+
+        with patch.object(restutil, "http_request",
+                    return_value=mock_response) as patch_http:
+            with patch.object(wire.HostPluginProtocol,
+                            "get_api_versions") as patch_get:
+                patch_get.return_value = api_versions
                 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[1]['data'] == expected_content)
-                self.assertTrue(patch_put.call_args[1]['headers'] == expected_headers)
+
+                self.assertTrue(patch_http.call_count == 2)
+
+                exp_data = self._hostplugin_data(
+                                status_blob.get_page_blob_create_headers(
+                                page_size))
+                self._validate_hostplugin_args(
+                    patch_http.call_args_list[0],
+                    test_goal_state,
+                    exp_method, exp_url, exp_data)
+
+                exp_data = self._hostplugin_data(
+                                status_blob.get_page_blob_page_headers(
+                                    0, page_size),
+                                    page)
+                exp_data['requestUri'] += "?comp=page" 
+                self._validate_hostplugin_args(
+                    patch_http.call_args_list[1],
+                    test_goal_state,
+                    exp_method, exp_url, exp_data)
 
     def test_validate_get_extension_artifacts(self):
         test_goal_state = wire.GoalState(WireProtocolData(DATA_FILE).goal_state)
@@ -155,6 +285,7 @@ class TestHostPlugin(AgentTestCase):
             for k in expected_headers:
                 self.assertTrue(k in actual_headers)
                 self.assertEqual(expected_headers[k], actual_headers[k])
+    
 
 class MockResponse:
     def __init__(self, body, status_code):