Repository: Azure/WALinuxAgent
Pull request #579 (2017-01-27 23:55:21)

Extend vm status
- add guest os, version and hostname to the status blob
- updates status version to 1.1
- addresses #574 

Before:
```
{
    "aggregateStatus":
    {
        "guestAgentStatus":
        {
            "formattedMessage":
            {
                "lang": "en-US",
                "message": "Guest Agent is running"
            },
            "status": "Ready",
            "version": "2.1.5"
        },
        "handlerAggregateStatus": []
    },
    "timestampUTC": "2017-01-27T21:19:06Z",
    "version": "1.0"
}
```
After:
```
{
    "aggregateStatus":
    {
        "guestAgentStatus":
        {
            "status": "Ready",
            "formattedMessage":
            {
                "lang": "en-US",
                "message": "Guest Agent is running"
            },
            "hostname": "edp4cx2m6s",
            "osname": "suse",
            "version": "2.2.3",
            "osversion": "12"
        },
        "handlerAggregateStatus": []
    },
    "version": "1.1",
    "timestampUTC": "2017-01-27T23:14:45Z"
}
```

/cc @jinhyunr @brendandixon 



Patch:
diff --git a/azurelinuxagent/common/protocol/restapi.py b/azurelinuxagent/common/protocol/restapi.py
index 5f71cf25..a42db377 100644
--- a/azurelinuxagent/common/protocol/restapi.py
+++ b/azurelinuxagent/common/protocol/restapi.py
@@ -16,11 +16,12 @@
 #
 # Requires Python 2.4+ and Openssl 1.0+
 #
-
+import socket
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.utils.restutil as restutil
 from azurelinuxagent.common.exception import ProtocolError, HttpError
 from azurelinuxagent.common.future import ustr
+from azurelinuxagent.common.version import DISTRO_VERSION, DISTRO_NAME, CURRENT_VERSION
 
 
 def validate_param(name, val, expected_type):
@@ -87,8 +88,13 @@ Data contract between guest and host
 
 
 class VMInfo(DataContract):
-    def __init__(self, subscriptionId=None, vmName=None, containerId=None,
-                 roleName=None, roleInstanceName=None, tenantName=None):
+    def __init__(self,
+                 subscriptionId=None,
+                 vmName=None,
+                 containerId=None,
+                 roleName=None,
+                 roleInstanceName=None,
+                 tenantName=None):
         self.subscriptionId = subscriptionId
         self.vmName = vmName
         self.containerId = containerId
@@ -96,18 +102,26 @@ class VMInfo(DataContract):
         self.roleInstanceName = roleInstanceName
         self.tenantName = tenantName
 
+
 class CertificateData(DataContract):
     def __init__(self, certificateData=None):
         self.certificateData = certificateData
 
+
 class Cert(DataContract):
-    def __init__(self, name=None, thumbprint=None, certificateDataUri=None, storeName=None, storeLocation=None):
+    def __init__(self,
+                 name=None,
+                 thumbprint=None,
+                 certificateDataUri=None,
+                 storeName=None,
+                 storeLocation=None):
         self.name = name
         self.thumbprint = thumbprint
         self.certificateDataUri = certificateDataUri
         self.storeLocation = storeLocation
         self.storeName = storeName
 
+
 class CertList(DataContract):
     def __init__(self):
         self.certificates = DataContractList(Cert)
@@ -131,8 +145,12 @@ class VMAgentManifestList(DataContract):
 
 
 class Extension(DataContract):
-    def __init__(self, name=None, sequenceNumber=None, publicSettings=None,
-                 protectedSettings=None, certificateThumbprint=None):
+    def __init__(self,
+                 name=None,
+                 sequenceNumber=None,
+                 publicSettings=None,
+                 protectedSettings=None,
+                 certificateThumbprint=None):
         self.name = name
         self.sequenceNumber = sequenceNumber
         self.publicSettings = publicSettings
@@ -207,8 +225,13 @@ class ExtensionSubStatus(DataContract):
 
 
 class ExtensionStatus(DataContract):
-    def __init__(self, configurationAppliedTime=None, operation=None,
-                 status=None, seq_no=None, code=None, message=None):
+    def __init__(self,
+                 configurationAppliedTime=None,
+                 operation=None,
+                 status=None,
+                 seq_no=None,
+                 code=None,
+                 message=None):
         self.configurationAppliedTime = configurationAppliedTime
         self.operation = operation
         self.status = status
@@ -219,7 +242,11 @@ class ExtensionStatus(DataContract):
 
 
 class ExtHandlerStatus(DataContract):
-    def __init__(self, name=None, version=None, status=None, code=0,
+    def __init__(self,
+                 name=None,
+                 version=None,
+                 status=None,
+                 code=0,
                  message=None):
         self.name = name
         self.version = version
@@ -230,16 +257,19 @@ class ExtHandlerStatus(DataContract):
 
 
 class VMAgentStatus(DataContract):
-    def __init__(self, version=None, status=None, message=None):
-        self.version = version
+    def __init__(self, status=None, message=None):
         self.status = status
         self.message = message
+        self.hostname = socket.gethostname()
+        self.version = str(CURRENT_VERSION)
+        self.osname = DISTRO_NAME
+        self.osversion = DISTRO_VERSION
         self.extensionHandlers = DataContractList(ExtHandlerStatus)
 
 
 class VMStatus(DataContract):
-    def __init__(self):
-        self.vmAgent = VMAgentStatus()
+    def __init__(self, status, message):
+        self.vmAgent = VMAgentStatus(status=status, message=message)
 
 
 class TelemetryEventParam(DataContract):
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index ae38e646..292c6348 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -250,6 +250,9 @@ def ga_status_to_v1(ga_status):
     v1_ga_status = {
         'version': ga_status.version,
         'status': ga_status.status,
+        'osversion': ga_status.osversion,
+        'osname': ga_status.osname,
+        'hostname': ga_status.hostname,
         'formattedMessage': formatted_msg
     }
     return v1_ga_status
@@ -338,7 +341,7 @@ def vm_status_to_v1(vm_status, ext_statuses):
         'handlerAggregateStatus': v1_handler_status_list
     }
     v1_vm_status = {
-        'version': '1.0',
+        'version': '1.1',
         'timestampUTC': timestamp,
         'aggregateStatus': v1_agg_status
     }
@@ -762,7 +765,7 @@ 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)
@@ -770,7 +773,7 @@ class WireClient(object):
         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)
@@ -778,7 +781,7 @@ class WireClient(object):
         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)
@@ -1407,7 +1410,6 @@ class InVMArtifactsProfile(object):
     * encryptedHealthChecks (optional)
     * encryptedApplicationProfile (optional)
     """
-
     def __init__(self, artifacts_profile):
         if not textutil.is_str_none_or_whitespace(artifacts_profile):
             self.__dict__.update(parse_json(artifacts_profile))
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index ecca441c..eaacfe06 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -282,12 +282,8 @@ class ExtHandlersHandler(object):
         ext_handler_i.rm_ext_handler_dir()
     
     def report_ext_handlers_status(self):
-        """Go thru handler_state dir, collect and report status"""
-        vm_status = VMStatus()
-        vm_status.vmAgent.version = str(CURRENT_VERSION)
-        vm_status.vmAgent.status = "Ready"
-        vm_status.vmAgent.message = "Guest Agent is running"
-
+        """Go through handler_state dir, collect and report status"""
+        vm_status = VMStatus(status="Ready", message="Guest Agent is running")
         if self.ext_handlers is not None:
             for ext_handler in self.ext_handlers.extHandlers:
                 try:
@@ -298,7 +294,7 @@ class ExtHandlersHandler(object):
                         version=CURRENT_VERSION,
                         is_success=False,
                         message=ustr(e))
-        
+
         logger.verbose("Report vm agent status")
         try:
             self.protocol.report_vm_status(vm_status)
@@ -331,7 +327,8 @@ class ExtHandlersHandler(object):
                 ext_handler_i.set_handler_status(message=ustr(e), code=-1)
 
         vm_status.vmAgent.extensionHandlers.append(handler_status)
-        
+
+
 class ExtHandlerInstance(object):
     def __init__(self, ext_handler, protocol):
         self.ext_handler = ext_handler


Test patch:
diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py
index 3b990502..1298fdc7 100644
--- a/tests/protocol/test_hostplugin.py
+++ b/tests/protocol/test_hostplugin.py
@@ -65,7 +65,7 @@ class TestHostPlugin(AgentTestCase):
             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()
+            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
@@ -117,7 +117,7 @@ class TestHostPlugin(AgentTestCase):
         self.assertFalse(host_client.is_initialized)
         self.assertTrue(host_client.api_versions is None)
         status_blob = wire.StatusBlob(None)
-        status_blob.vm_status = restapi.VMStatus()
+        status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready")
         status_blob.data = '{"dummy": "data"}'
         status_blob.type = "BlockBlob"
         with patch.object(wire.HostPluginProtocol,
diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py
index 8c9cc02f..e083678b 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -270,6 +270,40 @@ class TestWireProtocolGetters(AgentTestCase):
             self.assertTrue(in_vm_artifacts_profile.is_on_hold())
             artifact_request.assert_called_once_with(testurl)
 
+    @patch("socket.gethostname", return_value="hostname")
+    @patch("time.gmtime", return_value=time.localtime(1485543256))
+    def test_report_vm_status(self, *args):
+        status = 'status'
+        message = 'message'
+
+        client = WireProtocol(wireserver_url).client
+        actual = StatusBlob(client=client)
+        actual.set_vm_status(VMStatus(status=status, message=message))
+        timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+
+        formatted_msg = {
+            'lang': 'en-US',
+            'message': message
+        }
+        v1_ga_status = {
+            'version': str(CURRENT_VERSION),
+            'status': status,
+            'osversion': DISTRO_VERSION,
+            'osname': DISTRO_NAME,
+            'hostname': socket.gethostname(),
+            'formattedMessage': formatted_msg
+        }
+        v1_agg_status = {
+            'guestAgentStatus': v1_ga_status,
+            'handlerAggregateStatus': []
+        }
+        v1_vm_status = {
+            'version': '1.1',
+            'timestampUTC': timestamp,
+            'aggregateStatus': v1_agg_status
+        }
+        self.assertEqual(json.dumps(v1_vm_status), actual.to_json())
+
 
 class MockResponse:
     def __init__(self, body, status_code):