Add support and checks for version in LUXI
authorMichael Hanselmann <hansmi@google.com>
Thu, 28 Oct 2010 16:48:20 +0000 (18:48 +0200)
committerMichael Hanselmann <hansmi@google.com>
Thu, 28 Oct 2010 16:56:39 +0000 (18:56 +0200)
A new constant, LUXI_VERSION, is used to verify the peer's version. The
version is optional, so old(er) clients and servers talking to peers not
supporting it won't break. Example with mismatching library:

$ gnt-instance list
Unhandled Ganeti error: LUXI version mismatch, server 2020000, request
1010000

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

daemons/ganeti-masterd
doc/walkthrough.rst
lib/constants.py
lib/luxi.py
test/ganeti.luxi_unittest.py

index 7a86d46..266a889 100755 (executable)
@@ -72,7 +72,7 @@ class ClientRequestWorker(workerpool.BaseWorker):
     client_ops = ClientOps(server)
 
     try:
-      (method, args) = luxi.ParseRequest(message)
+      (method, args, version) = luxi.ParseRequest(message)
     except luxi.ProtocolError, err:
       logging.error("Protocol Error: %s", err)
       client.close_log()
@@ -80,6 +80,11 @@ class ClientRequestWorker(workerpool.BaseWorker):
 
     success = False
     try:
+      # Verify client's version if there was one in the request
+      if version is not None and version != constants.LUXI_VERSION:
+        raise errors.LuxiError("LUXI version mismatch, server %s, request %s" %
+                               (constants.LUXI_VERSION, version))
+
       result = client_ops.handle_request(method, args)
       success = True
     except errors.GenericError, err:
index f2cafa2..ce0edc3 100644 (file)
@@ -1023,6 +1023,25 @@ hexadecimal, and 0x93 represents DRBD's major number. Thus we can see
 from the above that instance2 has /dev/drbd0, instance3 /dev/drbd1, and
 instance4 /dev/drbd2.
 
+LUXI version mismatch
++++++++++++++++++++++
+
+LUXI is the protocol used for communication between clients and the
+master daemon. Starting in Ganeti 2.3, the peers exchange their version
+in each message. When they don't match, an error is raised::
+
+  $ gnt-node modify -O yes node3
+  Unhandled Ganeti error: LUXI version mismatch, server 2020000, request 2030000
+
+Usually this means that server and client are from different Ganeti
+versions or import their libraries from different, consistent paths
+(e.g. an older version installed in another place). You can print the
+import path for Ganeti's modules using the following command (note that
+depending on your setup you might have to use an explicit version in the
+Python command, e.g. ``python2.6``)::
+
+  python -c 'import ganeti; print ganeti.__file__'
+
 .. vim: set textwidth=72 :
 .. Local Variables:
 .. mode: rst
index 71abd6d..1935064 100644 (file)
@@ -199,6 +199,7 @@ PROC_MOUNTS = "/proc/mounts"
 
 # luxi related constants
 LUXI_EOM = "\3"
+LUXI_VERSION = CONFIG_VERSION
 
 # one of 'no', 'yes', 'only'
 SYSLOG_USAGE = _autoconf.SYSLOG_USAGE
index 54f5407..4b301e8 100644 (file)
@@ -45,6 +45,7 @@ KEY_METHOD = "method"
 KEY_ARGS = "args"
 KEY_SUCCESS = "success"
 KEY_RESULT = "result"
+KEY_VERSION = "version"
 
 REQ_SUBMIT_JOB = "SubmitJob"
 REQ_SUBMIT_MANY_JOBS = "SubmitManyJobs"
@@ -274,13 +275,14 @@ def ParseRequest(msg):
 
   method = request.get(KEY_METHOD, None) # pylint: disable-msg=E1103
   args = request.get(KEY_ARGS, None) # pylint: disable-msg=E1103
+  version = request.get(KEY_VERSION, None)
 
   if method is None or args is None:
     logging.error("LUXI request missing method or arguments: %r", msg)
     raise ProtocolError(("Invalid LUXI request (no method or arguments"
                          " in request): %r") % msg)
 
-  return (method, args)
+  return (method, args, version)
 
 
 def ParseResponse(msg):
@@ -299,10 +301,10 @@ def ParseResponse(msg):
           KEY_RESULT in data):
     raise ProtocolError("Invalid response from server: %r" % data)
 
-  return (data[KEY_SUCCESS], data[KEY_RESULT])
+  return (data[KEY_SUCCESS], data[KEY_RESULT], data.get(KEY_VERSION, None))
 
 
-def FormatResponse(success, result):
+def FormatResponse(success, result, version=None):
   """Formats a LUXI response message.
 
   """
@@ -311,12 +313,15 @@ def FormatResponse(success, result):
     KEY_RESULT: result,
     }
 
+  if version is not None:
+    response[KEY_VERSION] = version
+
   logging.debug("LUXI response: %s", response)
 
   return serializer.DumpJson(response)
 
 
-def FormatRequest(method, args):
+def FormatRequest(method, args, version=None):
   """Formats a LUXI request message.
 
   """
@@ -326,22 +331,30 @@ def FormatRequest(method, args):
     KEY_ARGS: args,
     }
 
+  if version is not None:
+    request[KEY_VERSION] = version
+
   # Serialize the request
   return serializer.DumpJson(request, indent=False)
 
 
-def CallLuxiMethod(transport_cb, method, args):
+def CallLuxiMethod(transport_cb, method, args, version=None):
   """Send a LUXI request via a transport and return the response.
 
   """
   assert callable(transport_cb)
 
-  request_msg = FormatRequest(method, args)
+  request_msg = FormatRequest(method, args, version=version)
 
   # Send request and wait for response
   response_msg = transport_cb(request_msg)
 
-  (success, result) = ParseResponse(response_msg)
+  (success, result, resp_version) = ParseResponse(response_msg)
+
+  # Verify version if there was one in the response
+  if resp_version is not None and resp_version != version:
+    raise errors.LuxiError("LUXI version mismatch, client %s, response %s" %
+                           (version, resp_version))
 
   if success:
     return result
@@ -412,7 +425,8 @@ class Client(object):
     """Send a generic request and return the response.
 
     """
-    return CallLuxiMethod(self._SendMethodCall, method, args)
+    return CallLuxiMethod(self._SendMethodCall, method, args,
+                          version=constants.LUXI_VERSION)
 
   def SetQueueDrainFlag(self, drain_flag):
     return self.CallMethod(REQ_QUEUE_SET_DRAIN_FLAG, drain_flag)
index ec54a47..66116d1 100755 (executable)
@@ -24,6 +24,8 @@
 
 import unittest
 
+from ganeti import constants
+from ganeti import errors
 from ganeti import luxi
 from ganeti import serializer
 
@@ -38,7 +40,7 @@ class TestLuxiParsing(testutils.GanetiTestCase):
       })
 
     self.assertEqualValues(luxi.ParseRequest(msg),
-                           ("foo", ["bar", "baz", 123]))
+                           ("foo", ["bar", "baz", 123], None))
 
     self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
                       "this\"is {invalid, ]json data")
@@ -59,13 +61,27 @@ class TestLuxiParsing(testutils.GanetiTestCase):
     self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
                       serializer.DumpJson({ luxi.KEY_ARGS: [], }))
 
+    # No method or arguments
+    self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
+                      serializer.DumpJson({ luxi.KEY_VERSION: 1, }))
+
+  def testParseRequestWithVersion(self):
+    msg = serializer.DumpJson({
+      luxi.KEY_METHOD: "version",
+      luxi.KEY_ARGS: (["some"], "args", 0, "here"),
+      luxi.KEY_VERSION: 20100101,
+      })
+
+    self.assertEqualValues(luxi.ParseRequest(msg),
+                           ("version", [["some"], "args", 0, "here"], 20100101))
+
   def testParseResponse(self):
     msg = serializer.DumpJson({
       luxi.KEY_SUCCESS: True,
       luxi.KEY_RESULT: None,
       })
 
-    self.assertEqual(luxi.ParseResponse(msg), (True, None))
+    self.assertEqual(luxi.ParseResponse(msg), (True, None, None))
 
     self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
                       "this\"is {invalid, ]json data")
@@ -86,6 +102,19 @@ class TestLuxiParsing(testutils.GanetiTestCase):
     self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
                       serializer.DumpJson({ luxi.KEY_SUCCESS: True, }))
 
+    # No result or success
+    self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
+                      serializer.DumpJson({ luxi.KEY_VERSION: 123, }))
+
+  def testParseResponseWithVersion(self):
+    msg = serializer.DumpJson({
+      luxi.KEY_SUCCESS: True,
+      luxi.KEY_RESULT: "Hello World",
+      luxi.KEY_VERSION: 19991234,
+      })
+
+    self.assertEqual(luxi.ParseResponse(msg), (True, "Hello World", 19991234))
+
   def testFormatResponse(self):
     for success, result in [(False, "error"), (True, "abc"),
                             (True, { "a": 123, "b": None, })]:
@@ -93,9 +122,24 @@ class TestLuxiParsing(testutils.GanetiTestCase):
       msgdata = serializer.LoadJson(msg)
       self.assert_(luxi.KEY_SUCCESS in msgdata)
       self.assert_(luxi.KEY_RESULT in msgdata)
+      self.assert_(luxi.KEY_VERSION not in msgdata)
+      self.assertEqualValues(msgdata,
+                             { luxi.KEY_SUCCESS: success,
+                               luxi.KEY_RESULT: result,
+                             })
+
+  def testFormatResponseWithVersion(self):
+    for success, result, version in [(False, "error", 123), (True, "abc", 999),
+                                     (True, { "a": 123, "b": None, }, 2010)]:
+      msg = luxi.FormatResponse(success, result, version=version)
+      msgdata = serializer.LoadJson(msg)
+      self.assert_(luxi.KEY_SUCCESS in msgdata)
+      self.assert_(luxi.KEY_RESULT in msgdata)
+      self.assert_(luxi.KEY_VERSION in msgdata)
       self.assertEqualValues(msgdata,
                              { luxi.KEY_SUCCESS: success,
                                luxi.KEY_RESULT: result,
+                               luxi.KEY_VERSION: version,
                              })
 
   def testFormatRequest(self):
@@ -104,11 +148,106 @@ class TestLuxiParsing(testutils.GanetiTestCase):
       msgdata = serializer.LoadJson(msg)
       self.assert_(luxi.KEY_METHOD in msgdata)
       self.assert_(luxi.KEY_ARGS in msgdata)
+      self.assert_(luxi.KEY_VERSION not in msgdata)
+      self.assertEqualValues(msgdata,
+                             { luxi.KEY_METHOD: method,
+                               luxi.KEY_ARGS: args,
+                             })
+
+  def testFormatRequestWithVersion(self):
+    for method, args, version in [("fn1", [], 123), ("fn2", [1, 2, 3], 999)]:
+      msg = luxi.FormatRequest(method, args, version=version)
+      msgdata = serializer.LoadJson(msg)
+      self.assert_(luxi.KEY_METHOD in msgdata)
+      self.assert_(luxi.KEY_ARGS in msgdata)
+      self.assert_(luxi.KEY_VERSION in msgdata)
       self.assertEqualValues(msgdata,
                              { luxi.KEY_METHOD: method,
                                luxi.KEY_ARGS: args,
+                               luxi.KEY_VERSION: version,
                              })
 
 
+class TestCallLuxiMethod(unittest.TestCase):
+  MY_LUXI_VERSION = 1234
+  assert constants.LUXI_VERSION != MY_LUXI_VERSION
+
+  def testSuccessNoVersion(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn1")
+      self.assertEqual(args, "Hello World")
+      return luxi.FormatResponse(True, "x")
+
+    result = luxi.CallLuxiMethod(_Cb, "fn1", "Hello World")
+
+  def testServerVersionOnly(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn1")
+      self.assertEqual(args, "Hello World")
+      return luxi.FormatResponse(True, "x", version=self.MY_LUXI_VERSION)
+
+    self.assertRaises(errors.LuxiError, luxi.CallLuxiMethod,
+                      _Cb, "fn1", "Hello World")
+
+  def testWithVersion(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn99")
+      self.assertEqual(args, "xyz")
+      return luxi.FormatResponse(True, "y", version=self.MY_LUXI_VERSION)
+
+    self.assertEqual("y", luxi.CallLuxiMethod(_Cb, "fn99", "xyz",
+                                              version=self.MY_LUXI_VERSION))
+
+  def testVersionMismatch(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn5")
+      self.assertEqual(args, "xyz")
+      return luxi.FormatResponse(True, "F", version=self.MY_LUXI_VERSION * 2)
+
+    self.assertRaises(errors.LuxiError, luxi.CallLuxiMethod,
+                      _Cb, "fn5", "xyz", version=self.MY_LUXI_VERSION)
+
+  def testError(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fnErr")
+      self.assertEqual(args, [])
+      err = errors.OpPrereqError("Test")
+      return luxi.FormatResponse(False, errors.EncodeException(err))
+
+    self.assertRaises(errors.OpPrereqError, luxi.CallLuxiMethod,
+                      _Cb, "fnErr", [])
+
+  def testErrorWithVersionMismatch(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fnErr")
+      self.assertEqual(args, [])
+      err = errors.OpPrereqError("TestVer")
+      return luxi.FormatResponse(False, errors.EncodeException(err),
+                                 version=self.MY_LUXI_VERSION * 2)
+
+    self.assertRaises(errors.LuxiError, luxi.CallLuxiMethod,
+                      _Cb, "fnErr", [],
+                      version=self.MY_LUXI_VERSION)
+
+  def testErrorWithVersion(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn9")
+      self.assertEqual(args, [])
+      err = errors.OpPrereqError("TestVer")
+      return luxi.FormatResponse(False, errors.EncodeException(err),
+                                 version=self.MY_LUXI_VERSION)
+
+    self.assertRaises(errors.OpPrereqError, luxi.CallLuxiMethod,
+                      _Cb, "fn9", [],
+                      version=self.MY_LUXI_VERSION)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()