Adam Litke has uploaded a new change for review.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
jsonrpc: Implement the JsonRPC server for the next-gen API
This patch implements a new Binding plugin to serve the next generation vdsm API over a JsonRPC wire protocol. The basic format of a message is:
<size><json-data>
<size> is an unsigned 64 bit integer in big endian format that indicates the length of <json-data> in bytes. <json-data> is either a request or a response in Javascript object notation (JSON).
A request object has the following fields: id: An integer which will be repeated in the matching response methodName: The name of the API method to be called args (optional): A JSON object containing arguments to the method
A response object has the following fields: id: An integer which will be the same as the matching request result: The return value of the method (defined in the API schema) error: A JSON object containing error information code: An integer error code message: Error context information
Method calls are dispatched to vdsm using a MethodBridge. The DynamicBridge dispatches calls based on an API schema document and a set of schema exceptions. In this way, the API can be expanded without the need to add more code to this server infrastructure. When a request is made, the DynamicBridge attempts to resolve 'methodName' to a schema defined method. If found, the necessary arguments are collected from the request and the function call is dispatched to the internal vdsm API.
Schema exceptions: Currently, vdsm does not completely conform to its own API schema. It is not possible to correct all of the discrepencies due to the need to maintain backwards compatibility with current API users. To facilitate migration to this API, the DynamicBridge implements a set of overrides to translate between the schema-defined API and what is implemented in vdsm today. There are three types of method overrides and one type of argument override:
Method overrides: 1. Custom call function: If a new API does not map directly to an existing vdsm API (or multiple functions must be called to get the result), a custom 'call' function can be defined for the API. This function will be called instead of trying to find a vdsm API to call.
2. Return field name: Many vdsm functions' return values are nested in a dictionary. This override specifies the key to use when accessing the result so that it can be returned un-nested to the caller.
3. Custom result post-processing function: Some vdsm APIs return results in a non-standard format and a special function must be used to reformat the data to comply with the schema.
Type override: Some data types have a different representation internally to vdsm than we have defined in the schema. A type override can be used to convert between the two formats. Currently, the only kind of translation we do is to rename fields within a given type.
Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Signed-off-by: Adam Litke agl@us.ibm.com --- M Makefile.am M configure.ac M tests/Makefile.am A tests/apiData.py A tests/apiTests.py M tests/run_tests_local.sh.in M vdsm.spec.in M vdsm/clientIF.py M vdsm/config.py.in A vdsm_api/BindingJsonRpc.py A vdsm_api/Bridge.py A vdsm_api/Makefile.am 12 files changed, 725 insertions(+), 4 deletions(-)
git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/14/8614/1
diff --git a/Makefile.am b/Makefile.am index 7548f42..b1a0418 100644 --- a/Makefile.am +++ b/Makefile.am @@ -18,7 +18,7 @@ # Refer to the README and COPYING files for full details of the license #
-SUBDIRS = vdsm vdsm_cli vds_bootstrap vdsm_reg vdsm_hooks tests vdsm-tool +SUBDIRS = vdsm vdsm_cli vds_bootstrap vdsm_reg vdsm_hooks tests vdsm-tool vdsm_api
include $(top_srcdir)/build-aux/Makefile.subs
@@ -112,8 +112,7 @@ vds_bootstrap/miniyum.py \ $(NULL)
-PEP8_BLACKLIST = \ - restData.py +PEP8_BLACKLIST = apiData.py,restData.py
check-local: find . -path './.git' -prune -type f -o \ diff --git a/configure.ac b/configure.ac index 450f714..679ad04 100644 --- a/configure.ac +++ b/configure.ac @@ -79,6 +79,7 @@ AC_SUBST([vdsmpylibdir], ['${pyexecdir}/vdsm']) AC_SUBST([vdsmtooldir], ['${vdsmpylibdir}/tool']) AC_SUBST([vdsmtestsdir], ['${datarootdir}/vdsm/tests']) +AC_SUBST([vdsmapidir], ['${datarootdir}/vdsm_api'])
# VDSM registration default paths AC_SUBST([vdsmregdir], ['${datarootdir}/vdsm-reg']) @@ -191,6 +192,7 @@ tests/Makefile tests/functional/Makefile vds_bootstrap/Makefile + vdsm_api/Makefile vdsm_cli/Makefile vdsm_hooks/directlun/Makefile vdsm_hooks/faqemu/Makefile diff --git a/tests/Makefile.am b/tests/Makefile.am index 2b61dde..d1fca34 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -22,6 +22,7 @@
test_modules = \ alignmentScanTests.py \ + apiTests.py \ betterPopenTests.py \ betterThreadingTests.py \ capsTests.py \ @@ -66,6 +67,7 @@
dist_vdsmtests_PYTHON = \ $(test_modules) \ + apiData.py \ monkeypatch.py \ testrunner.py \ testValidation.py diff --git a/tests/apiData.py b/tests/apiData.py new file mode 100644 index 0000000..fdb2611 --- /dev/null +++ b/tests/apiData.py @@ -0,0 +1,10 @@ +class APIData(object): + def __init__(self, obj, meth, data): + self.obj = obj + self.meth = meth + self.data = data + +testPing_apidata = [ + APIData('Global', 'ping', { + 'status': {'code': 0, 'message': 'OK'}}) +] diff --git a/tests/apiTests.py b/tests/apiTests.py new file mode 100644 index 0000000..9bfe347 --- /dev/null +++ b/tests/apiTests.py @@ -0,0 +1,228 @@ +# +# Copyright 2012 Adam Litke, IBM Corporation +# +# 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 +# +# Refer to the README and COPYING files for full details of the license +# + +import logging +import os +import os.path +import socket +import json +import struct +import time + +from testrunner import VdsmTestCase as TestCaseBase +from vdsm import constants +import apiData + +ip = '0.0.0.0' +port = 9824 +_fakeret = {} + +apiWhitelist = ('StorageDomain.Classes', 'StorageDomain.Types', + 'Volume.Formats', 'Volume.Types', 'Volume.Roles', 'Image.DiskTypes') + + +def getFakeAPI(): + """ + Create a Mock API module for testing. Mock API will return data from + the _fakeret global variable instead of calling into vdsm. _fakeret is + expected to have the following format: + + { + '<class1>': { + '<func1>': [ <ret1>, <ret2>, ... ], + '<func2>': [ ... ], + }, '<class2>': { + ... + } + } + """ + class FakeObj(object): + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def default(self, *args, **kwargs): + try: + return _fakeret[self.type][self.lastFunc].pop(0) + except (KeyError, IndexError): + raise Exception("No API data avilable for %s.%s" % \ + (self.type, self.lastFunc)) + + def __getattr__(self, name): + # While we are constructing the API module, use the normal getattr + if 'API' not in sys.modules: + return object.__getattr__(name) + self.lastFunc = name + return self.default + + import sys + import imp + from new import classobj + + _API = __import__('API', globals(), locals(), {}, -1) + _newAPI = imp.new_module('API') + + for obj in ('Global', 'ConnectionRefs', 'StorageDomain', 'Image', 'Volume', + 'Task', 'StoragePool', 'VM'): + cls = classobj(obj, (FakeObj,), {'type': obj}) + setattr(_newAPI, obj, cls) + + # Apply the whitelist to our version of API + for name in apiWhitelist: + parts = name.split('.') + dstObj = _newAPI + srcObj = _API + # Walk the object hierarchy copying each component of the whitelisted + # attribute from the real API to our fake one + for obj in parts: + srcObj = getattr(srcObj, obj) + try: + dstObj = getattr(dstObj, obj) + except AttributeError: + setattr(dstObj, obj, srcObj) + + # Install our fake API into the module table for use by the whole program + sys.modules['API'] = _newAPI + + +def findSchema(): + """ + Find the API schema file whether we are running tests from the source dir + or from the tests install location + """ + scriptdir = os.path.dirname(__file__) + localpath = os.path.join(scriptdir, '../vdsm_api/vdsmapi-schema.json') + installedpath = os.path.join(constants.P_VDSM, 'vdsmapi-schema.json') + for f in localpath, installedpath: + if os.access(f, os.R_OK): + return f + raise Exception("Unable to find schema in %s or %s", + localpath, installedpath) + +def setUpModule(): + """ + Set up the environment for all tests: + 1. Override the API so we can program our own return values + 2. Start an embedded server to process our requests + """ + log = logging.getLogger('apiTests') + handler = logging.StreamHandler() + fmt_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + formatter = logging.Formatter(fmt_str) + handler.setFormatter(formatter) + handler.setLevel(logging.INFO) + log.addHandler(handler) + + schema = findSchema() + getFakeAPI() + import BindingJsonRpc + import Bridge + bridge = Bridge.DynamicBridge(schema) + server = BindingJsonRpc.BindingJsonRpc(bridge, ip, port) + server.start() + + +class APITest(TestCaseBase): + def expectAPI(self, obj, meth, retval): + global _fakeret + if obj not in _fakeret: + _fakeret[obj] = {} + if meth not in _fakeret[obj]: + _fakeret[obj][meth] = [] + _fakeret[obj][meth].append(retval) + + def programAPI(self, key): + key += '_apidata' + for item in getattr(apiData, key): + self.expectAPI(item.obj, item.meth, item.data) + + def clearAPI(self): + global _fakeret + _fakeret = {} + +class JsonRawTest(APITest): + _Size = struct.Struct("!Q") + + def buildMessage(self, data): + msg = json.dumps(data) + msg = msg.encode('utf-8') + msize = JsonRawTest._Size.pack(len(msg)) + resp = msize + msg + return resp + + def sendMessage(self, msg): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((ip, port)) + sock.sendall(msg) + data = sock.recv(JsonRawTest._Size.size) + msgLen = JsonRawTest._Size.unpack(data)[0] + data = sock.recv(msgLen) + return json.loads(data) + finally: + sock.close() + + def testPing(self): + self.clearAPI() + self.programAPI("testPing") + msg = self.buildMessage({'id': 1, 'methodName': 'Host.ping', + 'args': {}}) + reply = self.sendMessage(msg) + self.assertEquals(0, reply['error']['code']) + self.assertEquals('Success', reply['error']['message']) + + def testNoMethod(self): + msg = self.buildMessage({'id': 1, 'methodName': 'Host.fake'}) + reply = self.sendMessage(msg) + self.assertEquals(4, reply['error']['code']) + + def testBadMethod(self): + msg = self.buildMessage({'id': 1, 'methodName': 'malformed''}) + reply = self.sendMessage(msg) + self.assertEquals(4, reply['error']['code']) + + def testMissingSize(self): + self.assertRaises(struct.error, self.sendMessage, + "malformed message") + + def testNotJson(self): + msg = "malformed message" + msize = JsonRawTest._Size.pack(len(msg)) + msg = msize + msg + self.assertRaises(struct.error, self.sendMessage, msg) + + def testSynchronization(self): + def doPing(msg): + self.clearAPI() + self.programAPI("testPing") + return self.sendMessage(msg)['error']['code'] + + msg = self.buildMessage({'id': 1, 'methodName': 'Host.ping'}) + # Send Truncated message + self.assertRaises(struct.error, doPing, msg[:-1]) + + # Test that the server recovers + self.assertEquals(0, doPing(msg)) + + # Send too much data + self.assertEquals(0, doPing(msg + "Hello")) + + # Test that the server recovers + self.assertEquals(0, doPing(msg)) + diff --git a/tests/run_tests_local.sh.in b/tests/run_tests_local.sh.in index fc4ac37..8f668f9 100644 --- a/tests/run_tests_local.sh.in +++ b/tests/run_tests_local.sh.in @@ -1,2 +1,2 @@ #!/bin/sh -PYTHONDONTWRITEBYTECODE=1 LC_ALL=C PYTHONPATH="@builddir@/vdsm:@top_srcdir@/vdsm:@top_srcdir@/vdsm_cli:$PYTHONPATH" @PYTHON@ @top_srcdir@/tests/testrunner.py --local-modules $@ +PYTHONDONTWRITEBYTECODE=1 LC_ALL=C PYTHONPATH="@builddir@/vdsm:@top_srcdir@/vdsm:@top_srcdir@/vdsm_cli:@top_srcdir@/vdsm_api:$PYTHONPATH" @PYTHON@ @top_srcdir@/tests/testrunner.py --local-modules $@ diff --git a/vdsm.spec.in b/vdsm.spec.in index dce14a9..8e7fa6c 100644 --- a/vdsm.spec.in +++ b/vdsm.spec.in @@ -154,6 +154,15 @@ %description rest A REST interface for interacting with vdsmd.
+%package jsonrpc +Summary: VDSM API Server +BuildArch: noarch + +Requires: %{name}-python = %{version}-%{release} + +%description jsonrpc +A Json-based RPC interface that serves as the protocol for libvdsm. + %package bootstrap Summary: VDSM bootstrapping package BuildArch: noarch @@ -912,6 +921,12 @@ %{_datadir}/%{vdsm_name}/rest/templates/api.xsd %{_datadir}/%{vdsm_name}/rest/templates/rsdl.xml
+%files jsonrpc +%{_datadir}/%{vdsm_name}/BindingJsonRpc.py* +%{_datadir}/%{vdsm_name}/Bridge.py* +%{_datadir}/%{vdsm_name}/vdsmapi.py* +%{_datadir}/%{vdsm_name}/vdsmapi-schema.json + %files bootstrap %defattr(-, root, root, -) %doc COPYING diff --git a/vdsm/clientIF.py b/vdsm/clientIF.py index e9ca7c9..cf8468c 100644 --- a/vdsm/clientIF.py +++ b/vdsm/clientIF.py @@ -158,6 +158,15 @@ self.bindings['rest'] = BindingREST(self, self.log, ip, rest_port, templatePath)
+ def _loadBindingJsonRpc(self): + from BindingJsonRpc import BindingJsonRpc + from Bridge import DynamicBridge + schema = os.path.join(constants.P_VDSM, 'vdsmapi-schema.json') + ip = self._getServerIP(config.get('addresses', 'management_ip')) + port = config.getint('addresses', 'json_port') + self.bindings['json'] = BindingJsonRpc(DynamicBridge(schema), + ip, port) + def _prepareBindings(self): self.bindings = {} if config.getboolean('vars', 'xmlrpc_enable'): @@ -172,6 +181,12 @@ except ImportError: self.log.warn('Unable to load the rest server module. ' 'Please make sure it is installed.') + if config.getboolean('vars', 'jsonrpc_enable'): + try: + self._loadBindingJsonRpc() + except ImportError: + self.log.warn('Unable to load the json rpc server module. ' + 'Please make sure it is installed.')
def _prepareMOM(self): try: diff --git a/vdsm/config.py.in b/vdsm/config.py.in index df85e7e..51a7ad6 100644 --- a/vdsm/config.py.in +++ b/vdsm/config.py.in @@ -130,6 +130,8 @@
('rest_enable', 'true', 'Enable the REST server'),
+ ('jsonrpc_enable', 'true', 'Enable the JSON RPC server'), + ('report_host_threads_as_cores', 'false', 'Count each cpu hyperthread as an individual core'), ]), @@ -251,6 +253,10 @@ 'Port on which the vdsmd REST server listens to network ' 'clients.'),
+ ('json_port', '4044', + 'Port on which the vdsmd Json RPC server listens to network ' + 'clients.'), + ('management_ip', '', None),
('guests_gateway_ip', '', None), diff --git a/vdsm_api/BindingJsonRpc.py b/vdsm_api/BindingJsonRpc.py new file mode 100644 index 0000000..9ac8b5d --- /dev/null +++ b/vdsm_api/BindingJsonRpc.py @@ -0,0 +1,99 @@ +# VDSM JsonRPC Server +# Copyright (C) 2012 Adam Litke, IBM Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# 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 St, Fifth Floor, Boston, MA 02110-1301 USA +import threading +import SocketServer +import json +import logging + +import struct + +_Size = struct.Struct("!Q") + +__bridge__ = None + +class BindingJsonRpc: + def __init__(self, bridge, ip, port): + self.bridge = bridge + self.serverPort = port + self.serverIP = ip + self.log = logging.getLogger('BindingJsonRpc') + self._createServer() + + def _createServer(self): + global __bridge__ + __bridge__ = self.bridge + ip = self.serverIP or '0.0.0.0' + self.server = JsonRpcServer((ip, self.serverPort), JsonRpcTCPHandler) + + def start(self): + def threaded_start(): + self.server.serve_forever() + t = threading.Thread(target=threaded_start, + name='JsonRpc') + t.setDaemon(True) + t.start() + + def prepareForShutdown(self): + self.server.shutdown() + + +class JsonRpcServer(SocketServer.TCPServer): + def __init__(self, addrInfo, handler): + self.allow_reuse_address = True + SocketServer.TCPServer.__init__(self, addrInfo, handler) + + +class JsonRpcTCPHandler(SocketServer.StreamRequestHandler): + """ + The RequestHandler class for our server. + + It is instantiated once per connection to the server, and must + override the handle() method to implement communication to the + client. + """ + + def handle(self): + bridge = __bridge__ + log = logging.getLogger('JsonRpcTCPHandler') + while True: + # self.request is the TCP socket connected to the client + try: + data = self.request.recv(_Size.size) + if len(data) == 0: + log.debug("Connection closed") + return + msgLen = _Size.unpack(data)[0] + msg = json.loads(self.request.recv(msgLen)) + except: + log.warn("Unexpected exception", exc_info=True) + return + log.debug('--> %s', msg) + + try: + ret = bridge._dispatch(msg['methodName'], msg.get('args', {})) + except Exception: + log.error("Dispatch error", exc_info=True) + continue + ret['id'] = msg['id'] + msg = json.dumps(ret) + msg = msg.encode('utf-8') + msize = _Size.pack(len(msg)) + resp = msize + msg + log.debug('<-- %s', msg) + + self.wfile.write(resp) + self.wfile.flush() + diff --git a/vdsm_api/Bridge.py b/vdsm_api/Bridge.py new file mode 100644 index 0000000..7402e08 --- /dev/null +++ b/vdsm_api/Bridge.py @@ -0,0 +1,332 @@ +# VDSM API Connector +# Copyright (C) 2012 Adam Litke, IBM Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# 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 St, Fifth Floor, Boston, MA 02110-1301 USA + +from functools import partial +import vdsmapi +import logging +import types +import API + +class VdsmError(Exception): + def __init__(self, code, message): + self.code = code + self.message = message + +class MethodBridge(object): + + def _dispatch(self, name, argobj): + method = name.replace('.', '_') + result = None + error = {'code': 0, 'message': 'Success'} + try: + fn = getattr(self, method) + except AttributeError: + error = {'code': 4, + 'message': "Operation '%s' not supported" % name} + return {'result': result, 'error': error} + try: + result = fn(argobj) + except VdsmError, e: + error = {'code': e.code, 'message': e.message} + except Exception, e: + error = {'code': 5, 'message': 'Internal error: %s' % e} + return {'result': result, 'error': error} + + def getArgs(self, argobj, arglist): + ret = () + for arg in arglist: + if arg in argobj: + ret += (argobj[arg],) + return ret + + def getResult(self, response, member=None): + if member is None: + return None + try: + return response[member] + except KeyError: + raise VdsmError(5, "Response is missing '%s' member" % member) + +class DynamicBridge(MethodBridge): + def __init__(self, schema): + self.parseSchema(schema) + + + def parseSchema(self, schema): + self.commands = {} + self.classes = {} + self.types = {} + with open(schema) as f: + symbols = vdsmapi.parse_schema(f) + for s in symbols: + if 'command' in s: + key = "%s_%s" % (s['command']['class'], + s['command']['name']) + self.commands[key] = s + elif 'class' in s: + cls = s['class'] + self.classes[cls] = s + elif 'type' in s: + t = s['type'] + self.types[t] = s + + + def __getattr__(self, attr): + if attr in self.commands: + cls, name = attr.split('_') + return partial(self.dynamicMethod, cls, name) + else: + raise AttributeError + + + def getArgList(self, cmd): + return self.commands[cmd].get('data', {}).keys() + + + def getObjargList(self, cls): + return self.classes[cls]['data'].keys() + + def getApiClass(self, cls): + name_map = { 'Host': 'Global' } + try: + cls = name_map[cls] + except KeyError: + pass + return getattr(API, cls) + + def typeFixup(self, sym_name, sym_type, obj): + logging.info("typeFixup: '%s': '%s'" % (sym_name, sym_type)) + isList = False + if type(sym_type) is list: + sym_type = sym_type[0] + isList = True + if sym_name[0] == '*': + sym_name = sym_name[1:] + + try: + symbol = self.types[sym_type] + except KeyError: + return + + if isList: + itemList = obj + else: + itemList = [obj] + + for item in itemList: + if sym_type in typefixups: + logging.error("Fixing up type %s", sym_type) + typefixups[sym_type](item) + for (k, v) in symbol.get('data', {}).items(): + if k[0] == '*': + k = k[1:] + if k in item: + self.typeFixup(k, v, item[k]) + + + def fixupArgs(self, cmd, args): + argInfo = zip(self.commands[cmd].get('data', {}).items(), args) + for typeInfo, val in argInfo: + argName, argType = typeInfo + if argType not in self.types: + continue + self.typeFixup(argName, argType, val) + + def fixupRet(self, cmd, result): + retType = self.commands[cmd].get('returns', None) + if retType is not None: + self.typeFixup('return', retType, result) + return result + + def dynamicMethod(self, cls, name, argobj): + cmd = '%s_%s' % (cls, name) + ctorArgObj = argobj.get('__obj__', {}) + ctorargs = self.getArgs(ctorArgObj, self.getObjargList(cls)) + args = self.getArgs(argobj, self.getArgList(cmd)) + apiobj = self.getApiClass(cls) + api = apiobj(*ctorargs) + + self.fixupArgs(cmd, args) + + # Call the override function (if given). Otherwise, just call directly + fn = command_info.get(cmd, {}).get('call') + if fn: + result = fn(api, argobj) + else: + fn = getattr(api, name) + result = fn(*args) + + if result['status']['code']: + code = result['status']['code'] + msg = result['status']['message'] + return {'code': code, 'message': msg} + + retfield = command_info.get(cmd, {}).get('ret') + if type(retfield) == types.FunctionType: + ret = retfield(result) + else: + ret = self.getResult(result, retfield) + return self.fixupRet(cmd, ret) + + +def Host_getStorageRepoStats_Ret(ret): + """ + The returned dictionary doesn't separate the stats from the status code + so we need to rebuild the result. + """ + del ret['status'] + return ret + + +def Host_getVMList_Call(api, args): + """ + This call is only interested in returning the VM UUIDs so pass False for + the first argument in order to suppress verbose results. + """ + vmList = args.get('vmList', []) + return API.Global().getVMList(False, vmList) + +def Host_getVMList_Ret(ret): + """ + Just return a list of VM UUIDs + """ + return [v['vmId'] for v in ret['vmList']] + + +def StoragePool_getInfo_Ret(ret): + """ + The result contains two data structures which must be merged + """ + return {'info': ret['info'], 'dominfo': ret['dominfo']} + + +def VM_getInfo_Call(api, args): + """ + The VM object has no getInfo method. We use the method from 'Global' and + pass arguments to get verbose information for only this one VM. + """ + vmId = api._UUID + return API.Global().getVMList(True, [vmId]) + +def VM_getInfo_Ret(ret): + """ + The result will be a list with only one element. + """ + return ret['vmList'][0] + +def VM_migrationCreate_Ret(ret): + """ + The result contains two data structures which must be merged + """ + return {'params': ret['params'], 'migrationPort': ret['migrationPort']} + + +def Volume_getsize_Ret(ret): + """ + Merge the two sizes into a single dictionary result. + """ + return {'truesize': ret['truesize'], 'apparentsize': ret['apparentsize']} + + +## +# Possible ways to override a command: +# - Supply a custom call function if the function name doesn't map directly to +# a vdsm API. +# - Specify the name of the field in the result that is the return value +# - Specify a custom function to post-process the result into a return value +## +command_info = { + 'ConnectionRefs_acquire': {'ret': 'results'}, + 'ConnectionRefs_release': {'ret': 'results'}, + 'ConnectionRefs_statuses': {'ret': 'connectionslist'}, + 'Host_fenceNode': {'ret': 'power'}, + 'Host_getAllTasksInfo': {'ret': 'allTasksInfo'}, + 'Host_getAllTasksStatuses': {'ret': 'allTasksStatus'}, + 'Host_getCapabilities': {'ret': 'info'}, + 'Host_getConnectedStoragePools': {'ret': 'poollist'}, + 'Host_getDeviceInfo': {'ret': 'info'}, + 'Host_getDeviceList': {'ret': 'devList'}, + 'Host_getDevicesVisibility': {'ret': 'visibility'}, + 'Host_getLVMVolumeGroups': {'ret': 'vglist'}, + 'Host_getStats': {'ret': 'info'}, + 'Host_getStorageDomains': {'ret': 'domlist'}, + 'Host_getStorageRepoStats': {'ret': Host_getStorageRepoStats_Ret}, + 'Host_getVMList': {'call': Host_getVMList_Call, 'ret': Host_getVMList_Ret}, + 'Image_delete': {'ret': 'uuid'}, + 'Image_deleteVolumes': {'ret': 'uuid'}, + 'Image_getVolumes': {'ret': 'uuidlist'}, + 'Image_mergeSnapshots': {'ret': 'uuid'}, + 'Image_move': {'ret': 'uuid'}, + 'ISCSIConnection_discoverSendTargets': {'ret': 'fullTargets'}, + 'LVMVolumeGroup_create': {'ret': 'uuid'}, + 'LVMVolumeGroup_getInfo': {'ret': 'info'}, + 'StorageDomain_getFileList': {'ret': 'files'}, + 'StorageDomain_getImages': {'ret': 'imageslist'}, + 'StorageDomain_getInfo': {'ret': 'info'}, + 'StorageDomain_getStats': {'ret': 'stats'}, + 'StorageDomain_getVolumes': {'ret': 'uuidlist'}, + 'StoragePool_connectStorageServer': {'ret': 'statuslist'}, + 'StoragePool_disconnectStorageServer': {'ret': 'statuslist'}, + 'StoragePool_fence': {'ret': 'spm_st'}, + 'StoragePool_getBackedUpVmsInfo': {'ret': 'vmlist'}, + 'StoragePool_getBackedUpVmsList': {'ret': 'vmlist'}, + 'StoragePool_getDomainsContainingImage': {'ret': 'domainslist'}, + 'StoragePool_getFloppyList': {'ret': 'isolist'}, + 'StoragePool_getInfo': {'ret': StoragePool_getInfo_Ret}, + 'StoragePool_getIsoList': {'ret': 'isolist'}, + 'StoragePool_getSpmStatus': {'ret': 'spm_st'}, + 'StoragePool_spmStart': {'ret': 'uuid'}, + 'StoragePool_upgrade': {'ret': 'upgradeStatus'}, + 'StoragePool_validateStorageServerConnection': {'ret': 'statuslist'}, + 'Task_getInfo': {'ret': 'TaskInfo'}, + 'Task_getStatus': {'ret': 'taskStatus'}, + 'VM_changeCD': {'ret': 'vmList'}, + 'VM_changeFloppy': {'ret': 'vmList'}, + 'VM_create': {'ret': 'vmList'}, + 'VM_getInfo': {'call': VM_getInfo_Call, 'ret': VM_getInfo_Ret}, + 'VM_getStats': {'ret': 'statsList'}, + 'VM_hotplugDisk': {'ret': 'vmList'}, + 'VM_hotplugNic': {'ret': 'vmList'}, + 'VM_hotUnplugDisk': {'ret': 'vmList'}, + 'VM_hotUnplugNic': {'ret': 'vmList'}, + 'VM_mergeStatus': {'ret': 'mergeStatus'}, + 'VM_migrationCreate': {'ret': VM_migrationCreate_Ret}, + 'Volume_copy': {'ret': 'uuid'}, + 'Volume_create': {'ret': 'uuid'}, + 'Volume_delete': {'ret': 'uuid'}, + 'Volume_getInfo': {'ret': 'info'}, + 'Volume_getPath': {'ret': 'path'}, + 'Volume_getSize': {'ret': Volume_getsize_Ret}, + 'Host_getAllTasks': {'ret': 'TasksDetails'}, +} + + +def fieldClone(oldName, newName, obj): + logging.warning("fieldClone: %s -> %s", oldName, newName) + if oldName in obj: + obj[newName] = obj[oldName] + elif newName in obj: + obj[oldName] = obj[newName] + +typefixups = { + 'VmDevice': partial(fieldClone, 'type', 'deviceType'), + 'BlockDevicePathInfo': partial(fieldClone, 'type', 'deviceType'), + 'VolumeGroupInfo': partial(fieldClone, 'type', 'deviceType'), + 'VmDeviceAddress': partial(fieldClone, 'type', 'addressType'), + 'IscsiCredentials': partial(fieldClone, 'type', 'authType'), + 'ConnectionRefArgs': partial(fieldClone, 'type', 'connType'), + 'VolumeInfo': partial(fieldClone, 'type', 'allocType'), +} diff --git a/vdsm_api/Makefile.am b/vdsm_api/Makefile.am new file mode 100644 index 0000000..655586f --- /dev/null +++ b/vdsm_api/Makefile.am @@ -0,0 +1,13 @@ +EXTRA_DIST = \ + vdsmapi-schema.json \ + $(NULL) + +dist_vdsm_PYTHON = \ + BindingJsonRpc.py \ + Bridge.py \ + vdsmapi.py \ + $(NULL) + +dist_vdsm_DATA = \ + vdsmapi-schema.json \ + $(NULL)
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: newchange Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 1 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com
Saggi Mizrahi has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 1: I would prefer that you didn't submit this
(21 inline comments)
Sorry for being pedantic about variable names
cls VS className
In python, because you don't have types to hint you, you have to be very specific when choosing a variable name.
Also, using cls for something other then the current type object is as confusing as using self for something other then ... well... self.
It's as confusing as using i, and j for anything other then iteration counters in a loop (or worse, using n). These are just patterns people come to expect.
Make sure all you files pass pep8 (they currently don't). New code should always conform to pep8. Just add the entire folder to the WHITELIST. (just install syntastic and flake8 to have vim warn you about any violations)
Make sure parameter names are in mixedCase.
.................................................... File tests/apiData.py Line 1: class APIData(object): Copyright? Line 2: def __init__(self, obj, meth, data): Line 3: self.obj = obj Line 4: self.meth = meth Line 5: self.data = data
.................................................... File tests/apiTests.py Line 131: Line 132: schema = findSchema() Line 133: getFakeAPI() Line 134: import BindingJsonRpc Line 135: import Bridge Why is that done here? Line 136: bridge = Bridge.DynamicBridge(schema) Line 137: server = BindingJsonRpc.BindingJsonRpc(bridge, ip, port) Line 138: server.start() Line 139:
.................................................... File vdsm_api/BindingJsonRpc.py Line 21: import struct Line 22: Line 23: _Size = struct.Struct("!Q") Line 24: Line 25: __bridge__ = None Globals are evil
Request handlers can access server attributes with the `server` field.
http://docs.python.org/library/socketserver.html#requesthandler-objects Line 26: Line 27: class BindingJsonRpc: Line 28: def __init__(self, bridge, ip, port): Line 29: self.bridge = bridge
Line 28: def __init__(self, bridge, ip, port): Line 29: self.bridge = bridge Line 30: self.serverPort = port Line 31: self.serverIP = ip Line 32: self.log = logging.getLogger('BindingJsonRpc') Since every instance of the class gets the same logger you should make it a class attribute Line 33: self._createServer() Line 34: Line 35: def _createServer(self): Line 36: global __bridge__
Line 45: name='JsonRpc') Line 46: t.setDaemon(True) Line 47: t.start() Line 48: Line 49: def prepareForShutdown(self): Why not just call it `shutdown`. That is what it does from the point of view of the class Line 50: self.server.shutdown() Line 51: Line 52: Line 53: class JsonRpcServer(SocketServer.TCPServer):
Line 61: The RequestHandler class for our server. Line 62: Line 63: It is instantiated once per connection to the server, and must Line 64: override the handle() method to implement communication to the Line 65: client. I don't think you need to paste the class documentation here. Line 66: """ Line 67: Line 68: def handle(self): Line 69: bridge = __bridge__
Line 66: """ Line 67: Line 68: def handle(self): Line 69: bridge = __bridge__ Line 70: log = logging.getLogger('JsonRpcTCPHandler') class attribute Line 71: while True: Line 72: # self.request is the TCP socket connected to the client Line 73: try: Line 74: data = self.request.recv(_Size.size)
Line 79: msg = json.loads(self.request.recv(msgLen)) Line 80: except: Line 81: log.warn("Unexpected exception", exc_info=True) Line 82: return Line 83: log.debug('--> %s', msg) Not really a very descriptive log message Line 84: Line 85: try: Line 86: ret = bridge._dispatch(msg['methodName'], msg.get('args', {})) Line 87: except Exception:
Line 87: except Exception: Line 88: log.error("Dispatch error", exc_info=True) Line 89: continue Line 90: ret['id'] = msg['id'] Line 91: msg = json.dumps(ret) This can throw an exception. Line 92: msg = msg.encode('utf-8') Line 93: msize = _Size.pack(len(msg)) Line 94: resp = msize + msg Line 95: log.debug('<-- %s', msg)
Line 91: msg = json.dumps(ret) Line 92: msg = msg.encode('utf-8') Line 93: msize = _Size.pack(len(msg)) Line 94: resp = msize + msg Line 95: log.debug('<-- %s', msg) again Line 96: Line 97: self.wfile.write(resp) Line 98: self.wfile.flush()
.................................................... File vdsm_api/Bridge.py Line 27: Line 28: class MethodBridge(object): Line 29: Line 30: def _dispatch(self, name, argobj): Line 31: method = name.replace('.', '_') I would name it methodName as it's not a method Line 32: result = None Line 33: error = {'code': 0, 'message': 'Success'} Line 34: try: Line 35: fn = getattr(self, method)
Line 44: except Exception, e: Line 45: error = {'code': 5, 'message': 'Internal error: %s' % e} Line 46: return {'result': result, 'error': error} Line 47: Line 48: def getArgs(self, argobj, arglist): return tuple(argobj[arg] for arg in arglist if arg in argobj) Line 49: ret = () Line 50: for arg in arglist: Line 51: if arg in argobj: Line 52: ret += (argobj[arg],)
Line 59: return response[member] Line 60: except KeyError: Line 61: raise VdsmError(5, "Response is missing '%s' member" % member) Line 62: Line 63: class DynamicBridge(MethodBridge): A lot of the methods look like they shouldn't be public Line 64: def __init__(self, schema): Line 65: self.parseSchema(schema) Line 66: Line 67:
Line 85: Line 86: Line 87: def __getattr__(self, attr): Line 88: if attr in self.commands: Line 89: cls, name = attr.split('_') cls is usually reserved as a version of 'self' but for class method. Usually people use klass to avoid overriding a key word
anyway this is neither because it's a className. Line 90: return partial(self.dynamicMethod, cls, name) Line 91: else: Line 92: raise AttributeError Line 93:
Line 107: pass Line 108: return getattr(API, cls) Line 109: Line 110: def typeFixup(self, sym_name, sym_type, obj): Line 111: logging.info("typeFixup: '%s': '%s'" % (sym_name, sym_type)) warning Line 112: isList = False Line 113: if type(sym_type) is list: Line 114: sym_type = sym_type[0] Line 115: isList = True
Line 109: Line 110: def typeFixup(self, sym_name, sym_type, obj): Line 111: logging.info("typeFixup: '%s': '%s'" % (sym_name, sym_type)) Line 112: isList = False Line 113: if type(sym_type) is list: you should use if isinstance(sym_type, list)
I also think the variable name should be changed to symTypeName
http://www.python.org/dev/peps/pep-0008/
Object type comparisons should always use isinstance() instead of comparing types directly. Line 114: sym_type = sym_type[0] Line 115: isList = True Line 116: if sym_name[0] == '*': Line 117: sym_name = sym_name[1:]
Line 127: itemList = [obj] Line 128: Line 129: for item in itemList: Line 130: if sym_type in typefixups: Line 131: logging.error("Fixing up type %s", sym_type) warning
errors occur only when the system can't recover gracefully Line 132: typefixups[sym_type](item) Line 133: for (k, v) in symbol.get('data', {}).items(): Line 134: if k[0] == '*': Line 135: k = k[1:]
Line 130: if sym_type in typefixups: Line 131: logging.error("Fixing up type %s", sym_type) Line 132: typefixups[sym_type](item) Line 133: for (k, v) in symbol.get('data', {}).items(): Line 134: if k[0] == '*': This bit seems to repeat
Maybe extract to a method? Line 135: k = k[1:] Line 136: if k in item: Line 137: self.typeFixup(k, v, item[k]) Line 138:
Line 140: def fixupArgs(self, cmd, args): Line 141: argInfo = zip(self.commands[cmd].get('data', {}).items(), args) Line 142: for typeInfo, val in argInfo: Line 143: argName, argType = typeInfo Line 144: if argType not in self.types: Should this even happen? Print warning? Line 145: continue Line 146: self.typeFixup(argName, argType, val) Line 147: Line 148: def fixupRet(self, cmd, result):
Line 150: if retType is not None: Line 151: self.typeFixup('return', retType, result) Line 152: return result Line 153: Line 154: def dynamicMethod(self, cls, name, argobj): cls in python usually denotes current class, especially when put as a method parameter
in any case it's a className Line 155: cmd = '%s_%s' % (cls, name) Line 156: ctorArgObj = argobj.get('__obj__', {}) Line 157: ctorargs = self.getArgs(ctorArgObj, self.getObjargList(cls)) Line 158: args = self.getArgs(argobj, self.getArgList(cmd))
Line 174: msg = result['status']['message'] Line 175: return {'code': code, 'message': msg} Line 176: Line 177: retfield = command_info.get(cmd, {}).get('ret') Line 178: if type(retfield) == types.FunctionType: again isisntance() Line 179: ret = retfield(result) Line 180: else: Line 181: ret = self.getResult(result, retfield) Line 182: return self.fixupRet(cmd, ret)
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 1 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com
Adam Litke has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 1: (20 inline comments)
Thanks for your review Saggi. New series coming shortly with all of your comments addressed.
.................................................... File tests/apiData.py Line 1: class APIData(object): Done Line 2: def __init__(self, obj, meth, data): Line 3: self.obj = obj Line 4: self.meth = meth Line 5: self.data = data
.................................................... File tests/apiTests.py Line 131: Line 132: schema = findSchema() Line 133: getFakeAPI() Line 134: import BindingJsonRpc Line 135: import Bridge They must be imported after setting up the fake API. I will add a comment stating this. Line 136: bridge = Bridge.DynamicBridge(schema) Line 137: server = BindingJsonRpc.BindingJsonRpc(bridge, ip, port) Line 138: server.start() Line 139:
.................................................... File vdsm_api/BindingJsonRpc.py Line 21: import struct Line 22: Line 23: _Size = struct.Struct("!Q") Line 24: Line 25: __bridge__ = None Done Line 26: Line 27: class BindingJsonRpc: Line 28: def __init__(self, bridge, ip, port): Line 29: self.bridge = bridge
Line 28: def __init__(self, bridge, ip, port): Line 29: self.bridge = bridge Line 30: self.serverPort = port Line 31: self.serverIP = ip Line 32: self.log = logging.getLogger('BindingJsonRpc') Done Line 33: self._createServer() Line 34: Line 35: def _createServer(self): Line 36: global __bridge__
Line 45: name='JsonRpc') Line 46: t.setDaemon(True) Line 47: t.start() Line 48: Line 49: def prepareForShutdown(self): Because it's a Binding that is loaded by clientIF and prepareForShutdown is the name of the API that clientIF wants to call. Line 50: self.server.shutdown() Line 51: Line 52: Line 53: class JsonRpcServer(SocketServer.TCPServer):
Line 61: The RequestHandler class for our server. Line 62: Line 63: It is instantiated once per connection to the server, and must Line 64: override the handle() method to implement communication to the Line 65: client. Done Line 66: """ Line 67: Line 68: def handle(self): Line 69: bridge = __bridge__
Line 66: """ Line 67: Line 68: def handle(self): Line 69: bridge = __bridge__ Line 70: log = logging.getLogger('JsonRpcTCPHandler') Done Line 71: while True: Line 72: # self.request is the TCP socket connected to the client Line 73: try: Line 74: data = self.request.recv(_Size.size)
Line 79: msg = json.loads(self.request.recv(msgLen)) Line 80: except: Line 81: log.warn("Unexpected exception", exc_info=True) Line 82: return Line 83: log.debug('--> %s', msg) Done Line 84: Line 85: try: Line 86: ret = bridge._dispatch(msg['methodName'], msg.get('args', {})) Line 87: except Exception:
Line 87: except Exception: Line 88: log.error("Dispatch error", exc_info=True) Line 89: continue Line 90: ret['id'] = msg['id'] Line 91: msg = json.dumps(ret) Done Line 92: msg = msg.encode('utf-8') Line 93: msize = _Size.pack(len(msg)) Line 94: resp = msize + msg Line 95: log.debug('<-- %s', msg)
Line 91: msg = json.dumps(ret) Line 92: msg = msg.encode('utf-8') Line 93: msize = _Size.pack(len(msg)) Line 94: resp = msize + msg Line 95: log.debug('<-- %s', msg) Done Line 96: Line 97: self.wfile.write(resp) Line 98: self.wfile.flush()
.................................................... File vdsm_api/Bridge.py Line 27: Line 28: class MethodBridge(object): Line 29: Line 30: def _dispatch(self, name, argobj): Line 31: method = name.replace('.', '_') Done Line 32: result = None Line 33: error = {'code': 0, 'message': 'Success'} Line 34: try: Line 35: fn = getattr(self, method)
Line 44: except Exception, e: Line 45: error = {'code': 5, 'message': 'Internal error: %s' % e} Line 46: return {'result': result, 'error': error} Line 47: Line 48: def getArgs(self, argobj, arglist): Done Line 49: ret = () Line 50: for arg in arglist: Line 51: if arg in argobj: Line 52: ret += (argobj[arg],)
Line 59: return response[member] Line 60: except KeyError: Line 61: raise VdsmError(5, "Response is missing '%s' member" % member) Line 62: Line 63: class DynamicBridge(MethodBridge): Done Line 64: def __init__(self, schema): Line 65: self.parseSchema(schema) Line 66: Line 67:
Line 85: Line 86: Line 87: def __getattr__(self, attr): Line 88: if attr in self.commands: Line 89: cls, name = attr.split('_') Done Line 90: return partial(self.dynamicMethod, cls, name) Line 91: else: Line 92: raise AttributeError Line 93:
Line 109: Line 110: def typeFixup(self, sym_name, sym_type, obj): Line 111: logging.info("typeFixup: '%s': '%s'" % (sym_name, sym_type)) Line 112: isList = False Line 113: if type(sym_type) is list: Done Line 114: sym_type = sym_type[0] Line 115: isList = True Line 116: if sym_name[0] == '*': Line 117: sym_name = sym_name[1:]
Line 127: itemList = [obj] Line 128: Line 129: for item in itemList: Line 130: if sym_type in typefixups: Line 131: logging.error("Fixing up type %s", sym_type) Done Line 132: typefixups[sym_type](item) Line 133: for (k, v) in symbol.get('data', {}).items(): Line 134: if k[0] == '*': Line 135: k = k[1:]
Line 130: if sym_type in typefixups: Line 131: logging.error("Fixing up type %s", sym_type) Line 132: typefixups[sym_type](item) Line 133: for (k, v) in symbol.get('data', {}).items(): Line 134: if k[0] == '*': Done Line 135: k = k[1:] Line 136: if k in item: Line 137: self.typeFixup(k, v, item[k]) Line 138:
Line 140: def fixupArgs(self, cmd, args): Line 141: argInfo = zip(self.commands[cmd].get('data', {}).items(), args) Line 142: for typeInfo, val in argInfo: Line 143: argName, argType = typeInfo Line 144: if argType not in self.types: Yes, if argtype is a string, uint, or an array of these it will happen. In these cases, we know there is no type fixing to do. Note that we could do str->int conversions here but we chose to handle those in the client code for now. Line 145: continue Line 146: self.typeFixup(argName, argType, val) Line 147: Line 148: def fixupRet(self, cmd, result):
Line 150: if retType is not None: Line 151: self.typeFixup('return', retType, result) Line 152: return result Line 153: Line 154: def dynamicMethod(self, cls, name, argobj): Done Line 155: cmd = '%s_%s' % (cls, name) Line 156: ctorArgObj = argobj.get('__obj__', {}) Line 157: ctorargs = self.getArgs(ctorArgObj, self.getObjargList(cls)) Line 158: args = self.getArgs(argobj, self.getArgList(cmd))
Line 174: msg = result['status']['message'] Line 175: return {'code': code, 'message': msg} Line 176: Line 177: retfield = command_info.get(cmd, {}).get('ret') Line 178: if type(retfield) == types.FunctionType: Done Line 179: ret = retfield(result) Line 180: else: Line 181: ret = self.getResult(result, retfield) Line 182: return self.fixupRet(cmd, ret)
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 1 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com
Saggi Mizrahi has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 2: (6 inline comments)
Sorry, if it wasn't for the exception raising bug I would have given you a +1 with optional comments.
.................................................... File vdsm_api/BindingJsonRpc.py Line 40: Line 41: def start(self): Line 42: def threaded_start(): Line 43: self.server.serve_forever() Line 44: t = threading.Thread(target=threaded_start, target=self.server.serve_forever Line 45: name='JsonRpc') Line 46: t.setDaemon(True) Line 47: t.start() Line 48:
Line 64: while True: Line 65: # self.request is the TCP socket connected to the client Line 66: try: Line 67: data = self.request.recv(_Size.size) Line 68: if len(data) == 0: != _Size.size Line 69: self.log.debug("Connection closed") Line 70: return Line 71: msgLen = _Size.unpack(data)[0] Line 72: msg = json.loads(self.request.recv(msgLen))
Line 76: self.log.debug('Received request: %s', msg) Line 77: Line 78: try: Line 79: ret = self.server.bridge.dispatch(msg['methodName'], Line 80: msg.get('args', {})) 1 rouge space Line 81: resp = self.buildResponse(msg['id'], ret) Line 82: except Exception: Line 83: self.log.warn("Dispatch error", exc_info=True) Line 84: err = {'code': 5, 'message': 'Dispatch error'}
.................................................... File vdsm_api/Bridge.py Line 35: try: Line 36: fn = getattr(self, methodName) Line 37: except AttributeError: Line 38: error = {'code': 4, Line 39: 'message': "Operation '%s' not supported" % name} Method not found is -32601 according to json-rpc spec
Not that this server is in anyway compliant :) Line 40: return {'result': result, 'error': error} Line 41: try: Line 42: result = fn(argobj) Line 43: except VdsmError, e:
Line 84: if attr in self.commands: Line 85: className, methodName = attr.split('_') Line 86: return partial(self._dynamicMethod, className, methodName) Line 87: else: Line 88: raise AttributeError You are raising the type and not an instance of the type Line 89: Line 90: def _getArgList(self, cmd): Line 91: return self.commands[cmd].get('data', {}).keys() Line 92:
Line 105: """ Line 106: The schema prefixes symbol names with '*' if they are optional. Strip Line 107: that annotation to get the correct symbol name. Line 108: """ Line 109: if symName[0] == '*': pep8
Use ''.startswith() and ''.endswith() instead of string slicing to check for prefixes or suffixes.
startswith() and endswith() are cleaner and less error prone. For example:
Yes: if foo.startswith('bar'): No: if foo[:3] == 'bar': Line 110: symName = symName[1:] Line 111: return symName Line 112: Line 113: # TODO: Add support for map types
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 2 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com
Saggi Mizrahi has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 2: I would prefer that you didn't submit this
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 2 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com
Adam Litke has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 2: (6 inline comments)
.................................................... File vdsm_api/BindingJsonRpc.py Line 40: Line 41: def start(self): Line 42: def threaded_start(): Line 43: self.server.serve_forever() Line 44: t = threading.Thread(target=threaded_start, Done Line 45: name='JsonRpc') Line 46: t.setDaemon(True) Line 47: t.start() Line 48:
Line 64: while True: Line 65: # self.request is the TCP socket connected to the client Line 66: try: Line 67: data = self.request.recv(_Size.size) Line 68: if len(data) == 0: Done Line 69: self.log.debug("Connection closed") Line 70: return Line 71: msgLen = _Size.unpack(data)[0] Line 72: msg = json.loads(self.request.recv(msgLen))
Line 76: self.log.debug('Received request: %s', msg) Line 77: Line 78: try: Line 79: ret = self.server.bridge.dispatch(msg['methodName'], Line 80: msg.get('args', {})) Done Line 81: resp = self.buildResponse(msg['id'], ret) Line 82: except Exception: Line 83: self.log.warn("Dispatch error", exc_info=True) Line 84: err = {'code': 5, 'message': 'Dispatch error'}
.................................................... File vdsm_api/Bridge.py Line 35: try: Line 36: fn = getattr(self, methodName) Line 37: except AttributeError: Line 38: error = {'code': 4, Line 39: 'message': "Operation '%s' not supported" % name} Yeah. I figured we'd define our own error codes in the API schema. I did look at the spec and decided to change the format of a request to match the spec. It should have 'id', 'method', and 'params'. We had 'id', 'methodName', and 'args'. Line 40: return {'result': result, 'error': error} Line 41: try: Line 42: result = fn(argobj) Line 43: except VdsmError, e:
Line 84: if attr in self.commands: Line 85: className, methodName = attr.split('_') Line 86: return partial(self._dynamicMethod, className, methodName) Line 87: else: Line 88: raise AttributeError Done Line 89: Line 90: def _getArgList(self, cmd): Line 91: return self.commands[cmd].get('data', {}).keys() Line 92:
Line 105: """ Line 106: The schema prefixes symbol names with '*' if they are optional. Strip Line 107: that annotation to get the correct symbol name. Line 108: """ Line 109: if symName[0] == '*': Done Line 110: symName = symName[1:] Line 111: return symName Line 112: Line 113: # TODO: Add support for map types
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 2 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com
Saggi Mizrahi has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 3: Looks good to me, but someone else must approve
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 3 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com
Ryan Harper has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 3: Looks good to me, but someone else must approve
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 3 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Barak Azulay bazulay@redhat.com Gerrit-Reviewer: Dan Kenigsberg danken@redhat.com Gerrit-Reviewer: Ryan Harper ryanh@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com Gerrit-Reviewer: Yaniv Bronhaim ybronhei@redhat.com Gerrit-Reviewer: Zhou Zheng Sheng zhshzhou@linux.vnet.ibm.com
Dan Kenigsberg has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 3: I would prefer that you didn't submit this
(6 inline comments)
I have really minor issue, I'm setting -1 for visibility.
p.s. please note that now that you've added the directory under pep8 wings, I get
vdsm_api/process-schema.py:37:1: E302 expected 2 blank lines, found 1
.................................................... Commit Message Line 10: over a JsonRPC wire protocol. The basic format of a message is: Line 11: Line 12: <size><json-data> Line 13: Line 14: <size> is an unsigned 64 bit integer in big endian format that indicates the I would love to hear your motivation for using this kind of packet-splitting of a stream, over using newline as a separator. I thought that the textual nature of json is better maintained with the latter. Line 15: length of <json-data> in bytes. <json-data> is either a request or a response Line 16: in Javascript object notation (JSON). Line 17: Line 18: A request object has the following fields:
.................................................... File Makefile.am Line 17: # Line 18: # Refer to the README and COPYING files for full details of the license Line 19: # Line 20: Line 21: SUBDIRS = vdsm vdsm_cli vds_bootstrap vdsm_reg vdsm_hooks tests vdsm-tool vdsm_api backslash, maybe? Line 22: Line 23: include $(top_srcdir)/build-aux/Makefile.subs Line 24: Line 25: # This is an *exception*, we ship also vdsm.spec so it's possible to build the
.................................................... File tests/apiTests.py Line 37: apiWhitelist = ('StorageDomain.Classes', 'StorageDomain.Types', Line 38: 'Volume.Formats', 'Volume.Types', 'Volume.Roles', 'Image.DiskTypes') Line 39: Line 40: Line 41: def getFakeAPI(): As this function returns None, the "get" prefix is a bit misleading. Line 42: """ Line 43: Create a Mock API module for testing. Mock API will return data from Line 44: the _fakeret global variable instead of calling into vdsm. _fakeret is Line 45: expected to have the following format:
Line 60: def default(self, *args, **kwargs): Line 61: try: Line 62: return _fakeret[self.type][self.lastFunc].pop(0) Line 63: except (KeyError, IndexError): Line 64: raise Exception("No API data avilable for %s.%s" % \ Saggi agreed to a general "Exception"?
redundant trailing backslash Line 65: (self.type, self.lastFunc)) Line 66: Line 67: def __getattr__(self, name): Line 68: # While we are constructing the API module, use the normal getattr
Line 159: _fakeret = {} Line 160: Line 161: Line 162: class JsonRawTest(APITest): Line 163: _Size = struct.Struct("!Q") to me, a function pair of packsize/unpacksize would have been more intuitive. Line 164: Line 165: def buildMessage(self, data): Line 166: msg = json.dumps(data) Line 167: msg = msg.encode('utf-8')
.................................................... File vdsm_api/Bridge.py Line 1: # VDSM API Connector Naming issue: is this a Connector, or a Bridge? and why is the leading Upercase? Line 2: # Copyright (C) 2012 Adam Litke, IBM Corporation Line 3: # Line 4: # This program is free software; you can redistribute it and/or modify Line 5: # it under the terms of the GNU General Public License version 2 as
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 3 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Barak Azulay bazulay@redhat.com Gerrit-Reviewer: Dan Kenigsberg danken@redhat.com Gerrit-Reviewer: Ryan Harper ryanh@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com Gerrit-Reviewer: Yaniv Bronhaim ybronhei@redhat.com Gerrit-Reviewer: Zhou Zheng Sheng zhshzhou@linux.vnet.ibm.com
Dan Kenigsberg has submitted this change and it was merged.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
jsonrpc: Implement the JsonRPC server for the next-gen API
This patch implements a new Binding plugin to serve the next generation vdsm API over a JsonRPC wire protocol. The basic format of a message is:
<size><json-data>
<size> is an unsigned 64 bit integer in big endian format that indicates the length of <json-data> in bytes. <json-data> is either a request or a response in Javascript object notation (JSON).
A request object has the following fields: id: An integer which will be repeated in the matching response methodName: The name of the API method to be called args (optional): A JSON object containing arguments to the method
A response object has the following fields: id: An integer which will be the same as the matching request result: The return value of the method (defined in the API schema) error: A JSON object containing error information code: An integer error code message: Error context information
Method calls are dispatched to vdsm using a MethodBridge. The DynamicBridge dispatches calls based on an API schema document and a set of schema exceptions. In this way, the API can be expanded without the need to add more code to this server infrastructure. When a request is made, the DynamicBridge attempts to resolve 'methodName' to a schema defined method. If found, the necessary arguments are collected from the request and the function call is dispatched to the internal vdsm API.
Schema exceptions: Currently, vdsm does not completely conform to its own API schema. It is not possible to correct all of the discrepencies due to the need to maintain backwards compatibility with current API users. To facilitate migration to this API, the DynamicBridge implements a set of overrides to translate between the schema-defined API and what is implemented in vdsm today. There are three types of method overrides and one type of argument override:
Method overrides: 1. Custom call function: If a new API does not map directly to an existing vdsm API (or multiple functions must be called to get the result), a custom 'call' function can be defined for the API. This function will be called instead of trying to find a vdsm API to call.
2. Return field name: Many vdsm functions' return values are nested in a dictionary. This override specifies the key to use when accessing the result so that it can be returned un-nested to the caller.
3. Custom result post-processing function: Some vdsm APIs return results in a non-standard format and a special function must be used to reformat the data to comply with the schema.
Type override: Some data types have a different representation internally to vdsm than we have defined in the schema. A type override can be used to convert between the two formats. Currently, the only kind of translation we do is to rename fields within a given type.
Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Signed-off-by: Adam Litke agl@us.ibm.com --- M Makefile.am M configure.ac M tests/Makefile.am A tests/apiData.py A tests/apiTests.py M tests/run_tests_local.sh.in M vdsm.spec.in M vdsm/clientIF.py M vdsm/config.py.in A vdsm_api/BindingJsonRpc.py A vdsm_api/Bridge.py A vdsm_api/Makefile.am M vdsm_api/process-schema.py 13 files changed, 780 insertions(+), 4 deletions(-)
Approvals: Dan Kenigsberg: Verified; Looks good to me, approved
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: merged Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 5 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Barak Azulay bazulay@redhat.com Gerrit-Reviewer: Dan Kenigsberg danken@redhat.com Gerrit-Reviewer: Ryan Harper ryanh@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com Gerrit-Reviewer: Yaniv Bronhaim ybronhei@redhat.com Gerrit-Reviewer: Zhou Zheng Sheng zhshzhou@linux.vnet.ibm.com
Dan Kenigsberg has posted comments on this change.
Change subject: jsonrpc: Implement the JsonRPC server for the next-gen API ......................................................................
Patch Set 5: Verified; Looks good to me, approved
I give up - at least for now - in my quest to have a pure textual API. I've rebased the patch and fixed two pep8 issues.
-- To view, visit http://gerrit.ovirt.org/8614 To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: comment Gerrit-Change-Id: Idae0faa80ffc6a5af002a8a7151aa40dc9a6673d Gerrit-PatchSet: 5 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Adam Litke agl@us.ibm.com Gerrit-Reviewer: Barak Azulay bazulay@redhat.com Gerrit-Reviewer: Dan Kenigsberg danken@redhat.com Gerrit-Reviewer: Ryan Harper ryanh@us.ibm.com Gerrit-Reviewer: Saggi Mizrahi smizrahi@redhat.com Gerrit-Reviewer: Yaniv Bronhaim ybronhei@redhat.com Gerrit-Reviewer: Zhou Zheng Sheng zhshzhou@linux.vnet.ibm.com
vdsm-patches@lists.fedorahosted.org