Commit e986f20c authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

Add support and checks for version in LUXI



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: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent 7a8bda3f
......@@ -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:
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment