From f2d1493440b562ad1d3b32189a3dae111a9a84b6 Mon Sep 17 00:00:00 2001
From: Vinzenz Feenstra <vfeenstr@redhat.com>
Date: Wed, 17 Jul 2013 11:05:51 +0200
Subject: [PATCH 1/3] agent: Implement basics for API Versioning

This commit introduces the basics needed to support API versioning between
the ovirt-guest-agent and VDSM.

How the negotiation works:
- VDSM has as a default the apiVersion value set to _DISABLED_API_VALUE marking
  it as unsupported
- The guest agent sends its max supported version with every `heartbeat`
  message
- VDSM checks that `apiVersion` exists in the `heartbeat` message arguments
  - If it exists it retrieves the minimum common version and sends the
    `api-version` message with the common version as `apiVersion` argument
  - If it does NOT exist it and the apiVersion is not set to
    _DISABLED_API_VALUE it will set it to _DISABLED_API_VALUE, with the meaning
    that it is not supported at all by the guest agent on the other end.
- The guest agent on receiving this message makes it own check and sets the
  `apiVersion` to the commonVersion

- If VDSM sends the `refresh` command it also sends its `apiVersion` value
  however if it does not, capable guest agents are disabling the versioning
  support and will know it in pre-supported state as well.

NOTE: VDSM will never send an api-version message without being triggered.
      This is to ensure backwards compatibility.

Change-Id: I754d52009538914dd0143894b24ad48fbf13cb38
Signed-off-by: Vinzenz Feenstra <vfeenstr@redhat.com>
---
 ovirt-guest-agent/OVirtAgentLogic.py | 116 ++++++++++++++++++++++++++++-------
 tests/guest_agent_test.py            |  10 +++
 tests/message_validator.py           |  68 +++++++++++++++++++-
 3 files changed, 170 insertions(+), 24 deletions(-)

diff --git a/ovirt-guest-agent/OVirtAgentLogic.py b/ovirt-guest-agent/OVirtAgentLogic.py
index 52c3825..1c8fb30 100644
--- a/ovirt-guest-agent/OVirtAgentLogic.py
+++ b/ovirt-guest-agent/OVirtAgentLogic.py
@@ -24,6 +24,27 @@ import socket
 from threading import Event
 from VirtIoChannel import VirtIoChannel
 
+_MAX_SUPPORTED_API_VERSION = 0
+_DISABLED_API_VALUE = 0
+
+_MESSAGE_MIN_API_VERSION = {
+    'active-user': 0,
+    'applications': 0,
+    'disks-usage': 0,
+    'echo': 0,
+    'fqdn': 0,
+    'heartbeat': 0,
+    'host-name': 0,
+    'memory-stats': 0,
+    'network-interfaces': 0,
+    'os-version': 0,
+    'session-lock': 0,
+    'session-logoff': 0,
+    'session-logon': 0,
+    'session-shutdown': 0,
+    'session-startup': 0,
+    'session-unlock': 0}
+
 
 # Return a safe (password masked) repr of the credentials block.
 def safe_creds_repr(creds):
@@ -36,6 +57,7 @@ def safe_creds_repr(creds):
 
 class DataRetriverBase:
     def __init__(self):
+        self.apiVersion = _DISABLED_API_VALUE
         self.memStats = {
             'mem_total': 0,
             'mem_free': 0,
@@ -45,6 +67,39 @@ class DataRetriverBase:
             'pageflt': 0,
             'majflt': 0}
 
+    def onAPIVersionUpdated(self, oldVersion, newVersion):
+        pass
+
+    def getAPIVersion(self):
+        return self.apiVersion
+
+    def setAPIVersion(self, version):
+        oldVersion = self.apiVersion
+        try:
+            version = int(version)
+        except ValueError:
+            logging.info("Invalid api version value '%s' set. Version value "
+                         "not changed.", version)
+            return
+
+        if _MAX_SUPPORTED_API_VERSION < version:
+            logging.debug("API version requested (%d) higher than known (%d). "
+                          "Using max known version.", version,
+                          _MAX_SUPPORTED_API_VERSION)
+            version = _MAX_SUPPORTED_API_VERSION
+
+        if version == self.apiVersion:
+            logging.debug("API version %d already set, no update necessary",
+                          version)
+            return
+        self.apiVersion = version
+
+        logging.info("API Version updated from %d to %d", oldVersion, version)
+        try:
+            self.onAPIVersionUpdated(oldVersion, version)
+        except Exception:
+            logging.exception("onAPIVersionUpdated failed")
+
     def getMachineName(self):
         pass
 
@@ -91,6 +146,16 @@ class AgentLogicBase:
         self.dr = None
         self.commandHandler = None
 
+    def _send(self, name, arguments=None):
+        version = _MESSAGE_MIN_API_VERSION.get(name, None)
+        if version is None:
+            logging.error('Undocumented message "%s"', name)
+        elif version <= self.dr.getAPIVersion():
+            self.vio.write(name, arguments or {})
+        else:
+            logging.debug("Message %s not supported by api version %d.",
+                          name, self.dr.getAPIVersion())
+
     def run(self):
         logging.debug("AgentLogicBase:: run() entered")
         thread.start_new_thread(self.doListen, ())
@@ -125,9 +190,10 @@ class AgentLogicBase:
                 counter += 1
                 hbsecs -= 1
                 if hbsecs <= 0:
-                    self.vio.write('heartbeat',
-                                   {'free-ram': self.dr.getAvailableRAM(),
-                                    'memory-stat': self.dr.getMemoryStats()})
+                    self._send('heartbeat',
+                               {'free-ram': self.dr.getAvailableRAM(),
+                                'memory-stat': self.dr.getMemoryStats(),
+                                'apiVersion': _MAX_SUPPORTED_API_VERSION})
                     hbsecs = self.heartBitRate
                 usersecs -= 1
                 if usersecs <= 0:
@@ -165,12 +231,17 @@ class AgentLogicBase:
                                   'channel.')
         logging.debug("AgentLogicBase::doListen() - exiting")
 
+    def _onApiVersion(self, args):
+        self.dr.setAPIVersion(args['apiVersion'])
+
     def parseCommand(self, command, args):
         logging.info("Received an external command: %s..." % (command))
         if command == 'lock-screen':
             self.commandHandler.lock_screen()
         elif command == 'log-off':
             self.commandHandler.logoff()
+        elif command == 'api-version':
+            self._onApiVersion(args)
         elif command == 'shutdown':
             try:
                 timeout = int(args['timeout'])
@@ -201,6 +272,10 @@ class AgentLogicBase:
                           % (safe_creds_repr(credentials)))
             self.commandHandler.login(credentials)
         elif command == 'refresh':
+            if not 'apiVersion' in args and self.dr.getAPIVersion() > 0:
+                logging.info('API versioning not supported by VDSM. Disabling '
+                             'versioning support.')
+                self.dr.setAPIVersion(_DISABLED_API_VALUE)
             self.sendUserInfo(True)
             self.sendAppList()
             self.sendInfo()
@@ -208,7 +283,7 @@ class AgentLogicBase:
             self.sendFQDN()
         elif command == 'echo':
             logging.debug("Echo: %s", args)
-            self.vio.write('echo', args)
+            self._send('echo', args)
         elif command == 'hibernate':
             state = args.get('state', 'disk')
             self.commandHandler.hibernate(state)
@@ -217,31 +292,30 @@ class AgentLogicBase:
                           % (command, args))
 
     def sendFQDN(self):
-        self.vio.write('fqdn', {'fqdn': self.dr.getFQDN()})
+        self._send('fqdn', {'fqdn': self.dr.getFQDN()})
 
     def sendUserInfo(self, force=False):
         cur_user = str(self.dr.getActiveUser())
         logging.debug("AgentLogicBase::sendUserInfo - cur_user = '%s'" %
                       (cur_user))
         if cur_user != self.activeUser or force:
-            self.vio.write('active-user', {'name': cur_user})
+            self._send('active-user', {'name': cur_user})
             self.activeUser = cur_user
 
     def sendInfo(self):
-        self.vio.write('host-name', {'name': self.dr.getMachineName()})
-        self.vio.write('os-version', {'version': self.dr.getOsVersion()})
-        self.vio.write('network-interfaces',
-                       {'interfaces': self.dr.getAllNetworkInterfaces()})
+        self._send('host-name', {'name': self.dr.getMachineName()})
+        self._send('os-version', {'version': self.dr.getOsVersion()})
+        self._send('network-interfaces',
+                   {'interfaces': self.dr.getAllNetworkInterfaces()})
 
     def sendAppList(self):
-        self.vio.write('applications',
-                       {'applications': self.dr.getApplications()})
+        self._send('applications', {'applications': self.dr.getApplications()})
 
     def sendDisksUsages(self):
-        self.vio.write('disks-usage', {'disks': self.dr.getDisksUsage()})
+        self._send('disks-usage', {'disks': self.dr.getDisksUsage()})
 
     def sendMemoryStats(self):
-        self.vio.write('memory-stats', {'memory': self.dr.getMemoryStats()})
+        self._send('memory-stats', {'memory': self.dr.getMemoryStats()})
 
     def sessionLogon(self):
         logging.debug("AgentLogicBase::sessionLogon: user logs on the system.")
@@ -252,29 +326,29 @@ class AgentLogicBase:
             cur_user = self.dr.getActiveUser()
             retries = retries + 1
         self.sendUserInfo()
-        self.vio.write('session-logon')
+        self._send('session-logon')
 
     def sessionLogoff(self):
         logging.debug("AgentLogicBase::sessionLogoff: "
                       "user logs off from the system.")
         self.activeUser = 'None'
-        self.vio.write('session-logoff')
-        self.vio.write('active-user', {'name': self.activeUser})
+        self._send('session-logoff')
+        self._send('active-user', {'name': self.activeUser})
 
     def sessionLock(self):
         logging.debug("AgentLogicBase::sessionLock: "
                       "user locks the workstation.")
-        self.vio.write('session-lock')
+        self._send('session-lock')
 
     def sessionUnlock(self):
         logging.debug("AgentLogicBase::sessionUnlock: "
                       "user unlocks the workstation.")
-        self.vio.write('session-unlock')
+        self._send('session-unlock')
 
     def sessionStartup(self):
         logging.debug("AgentLogicBase::sessionStartup: system starts up.")
-        self.vio.write('session-startup')
+        self._send('session-startup')
 
     def sessionShutdown(self):
         logging.debug("AgentLogicBase::sessionShutdown: system shuts down.")
-        self.vio.write('session-shutdown')
+        self._send('session-shutdown')
diff --git a/tests/guest_agent_test.py b/tests/guest_agent_test.py
index b3939ad..bd4a7f4 100644
--- a/tests/guest_agent_test.py
+++ b/tests/guest_agent_test.py
@@ -56,6 +56,10 @@ class FunctionalTest(GuestAgentTestCase):
 
         self.vdsAgent = agent_class(self._config)
 
+    def testRefresh(self):
+        self._validator.verifyRefreshReply(self.vdsAgent)
+        self._validator.verifyRefreshReply2(self.vdsAgent)
+
     def testSendInfo(self):
         self._validator.verifySendInfo(self.vdsAgent)
 
@@ -91,3 +95,9 @@ class FunctionalTest(GuestAgentTestCase):
 
     def testSessionShutdown(self):
         self._validator.verifySessionShutdown(self.vdsAgent)
+
+    def testAPIVersion(self):
+        self._validator.verifyAPIVersion(self.vdsAgent)
+
+    def testAPIVersion2(self):
+        self._validator.verifyAPIVersion2(self.vdsAgent)
diff --git a/tests/message_validator.py b/tests/message_validator.py
index 95c379d..20954b9 100644
--- a/tests/message_validator.py
+++ b/tests/message_validator.py
@@ -5,6 +5,7 @@
 import test_port
 import json
 import logging
+import OVirtAgentLogic
 
 
 class TestPortWriteBuffer(test_port.TestPort):
@@ -23,19 +24,39 @@ class TestPortWriteBuffer(test_port.TestPort):
         self._buffer = ''
 
 
+def _ensure_no_messages(f):
+    def fun(self, *args, **kwargs):
+        result = f(self, *args, **kwargs)
+        parsed = self._get_messages()
+        assert(len(parsed) == 0)
+        return result
+    return fun
+
+
+def assertIn(m, n):
+    if not m in n:
+        raise Exception("%s not in %s" % (m, str(n)))
+
+
+def assertEqual(a, b, msg=None):
+    if a != b:
+        raise Exception(msg or '%s != %s' % (str(a), str(b)))
+
+
 def _ensure_messages(*messages):
     def wrapped(f):
         def fun(self, *args, **kwargs):
             result = f(self, *args, **kwargs)
             names = []
             parsed = self._get_messages()
-            assert(len(parsed) == len(messages))
             for m in parsed:
-                assert('__name__' in m)
+                assertIn('__name__', m)
                 names.append(m['__name__'])
                 self._check(m)
             for m in messages:
-                assert(m in names)
+                assertIn(m, names)
+            for n in names:
+                assertIn(n, messages)
             return result
         return fun
     return wrapped
@@ -69,6 +90,13 @@ def _name_and_one_str_param(msg_name, param_name):
     return wrapped
 
 
+def _name_and_one_integral_param(msg_name, param_name):
+    def wrapped(o):
+        assert(o['__name__'] == msg_name)
+        assert_integral_param(o, param_name)
+    return wrapped
+
+
 def assert_is_string_list(o):
     assert(isinstance(o, list))
     for s in o:
@@ -134,6 +162,7 @@ _MSG_VALIDATORS = {
     'session-shutdown': _name_only('session-shutdown'),
     'session-startup': _name_only('session-startup'),
     'session-unlock': _name_only('session-unlock'),
+    'api-version': _name_and_one_integral_param('api-version', 'apiVersion')
 }
 
 
@@ -208,3 +237,36 @@ class MessageValidator(object):
     @_ensure_messages('session-shutdown')
     def verifySessionShutdown(self, agent):
         agent.sessionShutdown()
+
+    def verifyAPIVersion(self, agent):
+        # If not yet activated, monkey patch to support the versioning
+        if OVirtAgentLogic._MAX_SUPPORTED_API_VERSION == 0:
+            OVirtAgentLogic._MAX_SUPPORTED_API_VERSION = 1
+        # Pretend VDSM told us it would support a higher version
+        useVersion = OVirtAgentLogic._MAX_SUPPORTED_API_VERSION + 1
+        agent._onApiVersion({'apiVersion': useVersion})
+
+    def verifyAPIVersion2(self, agent):
+        if OVirtAgentLogic._MAX_SUPPORTED_API_VERSION == 0:
+            OVirtAgentLogic._MAX_SUPPORTED_API_VERSION = 1
+        # Pretend VDSM told us nothing or 0
+        agent.dr.setAPIVersion(0)
+
+    @_ensure_messages('applications', 'host-name', 'os-version', 'active-user',
+                      'network-interfaces', 'disks-usage', 'fqdn')
+    def verifyRefreshReply(self, agent):
+        # If not yet activated, monkey patch to support the versioning
+        if OVirtAgentLogic._MAX_SUPPORTED_API_VERSION == 0:
+            OVirtAgentLogic._MAX_SUPPORTED_API_VERSION = 1
+        agent.dr.setAPIVersion(1)
+        assert(agent.dr.getAPIVersion() == 1)
+        agent.parseCommand('refresh', {'apiVersion': 1})
+        assert(agent.dr.getAPIVersion() == 1)
+
+    @_ensure_messages('applications', 'host-name', 'os-version', 'active-user',
+                      'network-interfaces', 'disks-usage', 'fqdn')
+    def verifyRefreshReply2(self, agent):
+        agent.dr.setAPIVersion(1)
+        assert(agent.dr.getAPIVersion() == 1)
+        agent.parseCommand('refresh', {})
+        assert(agent.dr.getAPIVersion() == 0)
-- 
1.9.0

