Also fix a typo in http/__init__.py and add unittests for the LUXI parsing and formatting functions. --- Makefile.am | 1 + daemons/ganeti-masterd | 24 +------- lib/http/__init__.py | 2 +- lib/luxi.py | 135 +++++++++++++++++++++++++++++++----------- test/ganeti.luxi_unittest.py | 116 ++++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 56 deletions(-) create mode 100755 test/ganeti.luxi_unittest.py
diff --git a/Makefile.am b/Makefile.am index 513d67b..45e82dc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -320,6 +320,7 @@ dist_TESTS = \ test/ganeti.hooks_unittest.py \ test/ganeti.http_unittest.py \ test/ganeti.locking_unittest.py \ + test/ganeti.luxi_unittest.py \ test/ganeti.mcpu_unittest.py \ test/ganeti.objects_unittest.py \ test/ganeti.rapi.resources_unittest.py \ diff --git a/daemons/ganeti-masterd b/daemons/ganeti-masterd index a789b7a..74fbf20 100755 --- a/daemons/ganeti-masterd +++ b/daemons/ganeti-masterd @@ -158,36 +158,19 @@ class ClientRqHandler(SocketServer.BaseRequestHandler): logging.debug("client closed connection") break - request = serializer.LoadJson(msg) - logging.debug("request: %s", request) - if not isinstance(request, dict): - logging.error("wrong request received: %s", msg) - break - - method = request.get(luxi.KEY_METHOD, None) - args = request.get(luxi.KEY_ARGS, None) - if method is None or args is None: - logging.error("no method or args in request") - break + (method, args) = luxi.ParseLuxiRequestMessage(msg) success = False try: result = self._ops.handle_request(method, args) success = True except errors.GenericError, err: - success = False result = errors.EncodeException(err) except: logging.error("Unexpected exception", exc_info=True) - err = sys.exc_info() - result = "Caught exception: %s" % str(err[1]) + result = "Caught exception: %s" % str(sys.exc_info()[1]) - response = { - luxi.KEY_SUCCESS: success, - luxi.KEY_RESULT: result, - } - logging.debug("response: %s", response) - self.send_message(serializer.DumpJson(response)) + self.send_message(luxi.FormatLuxiResponseMessage(success, result)) def read_message(self): while not self._msgs: @@ -200,7 +183,6 @@ class ClientRqHandler(SocketServer.BaseRequestHandler): return self._msgs.popleft() def send_message(self, msg): - #print "sending", msg # TODO: sendall is not guaranteed to send everything self.request.sendall(msg + self.EOM) diff --git a/lib/http/__init__.py b/lib/http/__init__.py index 6f0d95c..e1b3ba7 100644 --- a/lib/http/__init__.py +++ b/lib/http/__init__.py @@ -815,7 +815,7 @@ class HttpMessageReader(object): buf = self._ContinueParsing(buf, eof) # Must be done only after the buffer has been evaluated - # TODO: Connection-length < len(data read) and connection closed + # TODO: Content-Length < len(data read) and connection closed if (eof and self.parser_status in (self.PS_START_LINE, self.PS_HEADERS)): diff --git a/lib/luxi.py b/lib/luxi.py index 414be06..8e4763a 100644 --- a/lib/luxi.py +++ b/lib/luxi.py @@ -33,14 +33,15 @@ import socket import collections import time import errno +import logging from ganeti import serializer from ganeti import constants from ganeti import errors -KEY_METHOD = 'method' -KEY_ARGS = 'args' +KEY_METHOD = "method" +KEY_ARGS = "args" KEY_SUCCESS = "success" KEY_RESULT = "result" @@ -234,6 +235,98 @@ class Transport: self.socket = None +def ParseLuxiRequestMessage(msg): + """Parses a LUXI request message. + + """ + try: + request = serializer.LoadJson(msg) + except ValueError, err: + raise ProtocolError("Invalid LUXI request (parsing error): %s" % err) + + logging.debug("LUXI request: %s", request) + + if not isinstance(request, dict): + logging.error("LUXI request not a dict: %r", msg) + raise ProtocolError("Invalid LUXI request (not a dict)") + + method = request.get(KEY_METHOD, None) + args = request.get(KEY_ARGS, 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) + + +def ParseLuxiResponseMessage(msg): + """Parses a LUXI response message. + + """ + # Parse the result + try: + data = serializer.LoadJson(msg) + except Exception, err: + raise ProtocolError("Error while deserializing response: %s" % str(err)) + + # Validate response + if not (isinstance(data, dict) and + KEY_SUCCESS in data and + KEY_RESULT in data): + raise ProtocolError("Invalid response from server: %r" % data) + + return (data[KEY_SUCCESS], data[KEY_RESULT]) + + +def FormatLuxiResponseMessage(success, result): + """Formats a LUXI response message. + + """ + response = { + KEY_SUCCESS: success, + KEY_RESULT: result, + } + + logging.debug("LUXI response: %s", response) + + return serializer.DumpJson(response) + + +def FormatLuxiRequestMessage(method, args): + """Formats a LUXI request message. + + """ + # Build request + request = { + KEY_METHOD: method, + KEY_ARGS: args, + } + + # Serialize the request + return serializer.DumpJson(request, indent=False) + + +def CallLuxiMethod(transport_cb, method, args): + """Send a LUXI request via a transport and return the response. + + """ + assert callable(transport_cb) + + request_msg = FormatLuxiRequestMessage(method, args) + + # Send request and wait for response + response_msg = transport_cb(request_msg) + + (success, result) = ParseLuxiResponseMessage(response_msg) + + if success: + return result + + errors.MaybeRaise(result) + raise RequestError(result) + + class Client(object): """High-level client implementation. @@ -283,46 +376,20 @@ class Client(object): except Exception: # pylint: disable-msg=W0703 pass - def CallMethod(self, method, args): - """Send a generic request and return the response. - - """ - # Build request - request = { - KEY_METHOD: method, - KEY_ARGS: args, - } - - # Serialize the request - send_data = serializer.DumpJson(request, indent=False) - + def _SendMethodCall(self, data): # Send request and wait for response try: self._InitTransport() - result = self.transport.Call(send_data) + return self.transport.Call(data) except Exception: self._CloseTransport() raise - # Parse the result - try: - data = serializer.LoadJson(result) - except Exception, err: - raise ProtocolError("Error while deserializing response: %s" % str(err)) - - # Validate response - if (not isinstance(data, dict) or - KEY_SUCCESS not in data or - KEY_RESULT not in data): - raise ProtocolError("Invalid response from server: %s" % str(data)) - - result = data[KEY_RESULT] - - if not data[KEY_SUCCESS]: - errors.MaybeRaise(result) - raise RequestError(result) + def CallMethod(self, method, args): + """Send a generic request and return the response. - return result + """ + return CallLuxiMethod(self._SendMethodCall, method, args) def SetQueueDrainFlag(self, drain_flag): return self.CallMethod(REQ_QUEUE_SET_DRAIN_FLAG, drain_flag) diff --git a/test/ganeti.luxi_unittest.py b/test/ganeti.luxi_unittest.py new file mode 100755 index 0000000..7a97bdb --- /dev/null +++ b/test/ganeti.luxi_unittest.py @@ -0,0 +1,116 @@ +#!/usr/bin/python +# + +# Copyright (C) 2010 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Script for unittesting the luxi module""" + + +import unittest + +from ganeti import luxi +from ganeti import serializer + +import testutils + + +class TestLuxiParsing(testutils.GanetiTestCase): + def testParseLuxiRequestMessage(self): + msg = serializer.DumpJson({ + luxi.KEY_METHOD: "foo", + luxi.KEY_ARGS: ("bar", "baz", 123), + }) + + self.assertEqualValues(luxi.ParseLuxiRequestMessage(msg), + ("foo", ["bar", "baz", 123])) + + self.assertRaises(luxi.ProtocolError, + luxi.ParseLuxiRequestMessage, + "this\"is {invalid, ]json data") + + # No dict + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiRequestMessage, + serializer.DumpJson(123)) + + # Empty dict + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiRequestMessage, + serializer.DumpJson({ })) + + # No arguments + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiRequestMessage, + serializer.DumpJson({ luxi.KEY_METHOD: "foo", })) + + # No method + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiRequestMessage, + serializer.DumpJson({ luxi.KEY_ARGS: [], })) + + def testParseLuxiResponseMessage(self): + msg = serializer.DumpJson({ + luxi.KEY_SUCCESS: True, + luxi.KEY_RESULT: None, + }) + + self.assertEqual(luxi.ParseLuxiResponseMessage(msg), (True, None)) + + self.assertRaises(luxi.ProtocolError, + luxi.ParseLuxiResponseMessage, + "this\"is {invalid, ]json data") + + # No dict + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiResponseMessage, + serializer.DumpJson(123)) + + # Empty dict + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiResponseMessage, + serializer.DumpJson({ })) + + # No success + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiResponseMessage, + serializer.DumpJson({ luxi.KEY_RESULT: True, })) + + # No result + self.assertRaises(luxi.ProtocolError, luxi.ParseLuxiResponseMessage, + serializer.DumpJson({ luxi.KEY_SUCCESS: True, })) + + def testFormatLuxiResponseMessage(self): + for success, result in [(False, "error"), (True, "abc"), + (True, { "a": 123, "b": None, })]: + msg = luxi.FormatLuxiResponseMessage(success, result) + msgdata = serializer.LoadJson(msg) + self.assert_(luxi.KEY_SUCCESS in msgdata) + self.assert_(luxi.KEY_RESULT in msgdata) + self.assertEqualValues(msgdata, + { luxi.KEY_SUCCESS: success, + luxi.KEY_RESULT: result, + }) + + def testFormatLuxiRequestMessage(self): + for method, args in [("a", []), ("b", [1, 2, 3])]: + msg = luxi.FormatLuxiRequestMessage(method, args) + msgdata = serializer.LoadJson(msg) + self.assert_(luxi.KEY_METHOD in msgdata) + self.assert_(luxi.KEY_ARGS in msgdata) + self.assertEqualValues(msgdata, + { luxi.KEY_METHOD: method, + luxi.KEY_ARGS: args, + }) + + +if __name__ == "__main__": + testutils.GanetiTestProgram() -- 1.6.4.3