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 |  114 +++++++++++++++++++++++++++++++++++
 5 files changed, 220 insertions(+), 56 deletions(-)
 create mode 100755 test/ganeti.luxi_unittest.py

diff --git a/Makefile.am b/Makefile.am
index 5b2ccb5..df4fa56 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -319,6 +319,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 200724c..53c71f6 100755
--- a/daemons/ganeti-masterd
+++ b/daemons/ganeti-masterd
@@ -157,36 +157,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.ParseRequest(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.FormatResponse(success, result))
 
   def read_message(self):
     while not self._msgs:
@@ -199,7 +182,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 776abf6..ad11aef 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"
 
@@ -233,6 +234,98 @@ class Transport:
       self.socket = None
 
 
+def ParseRequest(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 ParseResponse(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 FormatResponse(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 FormatRequest(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 = FormatRequest(method, args)
+
+  # Send request and wait for response
+  response_msg = transport_cb(request_msg)
+
+  (success, result) = ParseResponse(response_msg)
+
+  if success:
+    return result
+
+  errors.MaybeRaise(result)
+  raise RequestError(result)
+
+
 class Client(object):
   """High-level client implementation.
 
@@ -282,46 +375,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..ec54a47
--- /dev/null
+++ b/test/ganeti.luxi_unittest.py
@@ -0,0 +1,114 @@
+#!/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 testParseRequest(self):
+    msg = serializer.DumpJson({
+      luxi.KEY_METHOD: "foo",
+      luxi.KEY_ARGS: ("bar", "baz", 123),
+      })
+
+    self.assertEqualValues(luxi.ParseRequest(msg),
+                           ("foo", ["bar", "baz", 123]))
+
+    self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
+                      "this\"is {invalid, ]json data")
+
+    # No dict
+    self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
+                      serializer.DumpJson(123))
+
+    # Empty dict
+    self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
+                      serializer.DumpJson({ }))
+
+    # No arguments
+    self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
+                      serializer.DumpJson({ luxi.KEY_METHOD: "foo", }))
+
+    # No method
+    self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
+                      serializer.DumpJson({ luxi.KEY_ARGS: [], }))
+
+  def testParseResponse(self):
+    msg = serializer.DumpJson({
+      luxi.KEY_SUCCESS: True,
+      luxi.KEY_RESULT: None,
+      })
+
+    self.assertEqual(luxi.ParseResponse(msg), (True, None))
+
+    self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
+                      "this\"is {invalid, ]json data")
+
+    # No dict
+    self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
+                      serializer.DumpJson(123))
+
+    # Empty dict
+    self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
+                      serializer.DumpJson({ }))
+
+    # No success
+    self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
+                      serializer.DumpJson({ luxi.KEY_RESULT: True, }))
+
+    # No result
+    self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
+                      serializer.DumpJson({ luxi.KEY_SUCCESS: True, }))
+
+  def testFormatResponse(self):
+    for success, result in [(False, "error"), (True, "abc"),
+                            (True, { "a": 123, "b": None, })]:
+      msg = luxi.FormatResponse(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 testFormatRequest(self):
+    for method, args in [("a", []), ("b", [1, 2, 3])]:
+      msg = luxi.FormatRequest(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.6

Reply via email to