From 6bdc75ea24d0798c6779130451e47e569900ff4e Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 14 Jan 2014 13:41:19 +0100 Subject: Implement XML introspection https://fedorahosted.org/freeipa/ticket/2937 --- ipaserver/rpcserver.py | 57 ++++++++++++++++++++++--- ipatests/test_ipalib/test_rpc.py | 92 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 140 insertions(+), 9 deletions(-) diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index a2a9db8ae..a58c85355 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -31,7 +31,7 @@ import urlparse import time import json -from ipalib import plugable, capabilities +from ipalib import plugable, capabilities, errors from ipalib.backend import Executioner from ipalib.errors import (PublicError, InternalError, CommandError, JSONError, CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError, @@ -290,6 +290,8 @@ class WSGIExecutioner(Executioner): content_type = None key = '' + _system_commands = {} + def set_api(self, api): super(WSGIExecutioner, self).set_api(api) if 'wsgi_dispatch' in self.api.Backend: @@ -331,9 +333,12 @@ class WSGIExecutioner(Executioner): (name, args, options, _id) = self.unmarshal(data) else: (name, args, options, _id) = self.simple_unmarshal(environ) - if name not in self.Command: + if name in self._system_commands: + result = self._system_commands[name](self, *args, **options) + elif name not in self.Command: raise CommandError(name=name) - result = self.Command[name](*args, **options) + else: + result = self.Command[name](*args, **options) except PublicError, e: error = e except StandardError, e: @@ -650,16 +655,56 @@ class xmlserver(KerberosWSGIExecutioner): key = '/xml' def listMethods(self, *params): - return tuple(name.decode('UTF-8') for name in self.Command) + """list methods for XML-RPC introspection""" + if params: + raise errors.ZeroArgumentError(name='system.listMethods') + return (tuple(unicode(name) for name in self.Command) + + tuple(unicode(name) for name in self._system_commands)) + + def _get_method_name(self, name, *params): + """Get a method name for XML-RPC introspection commands""" + if not params: + raise errors.RequirementError(name='method name') + elif len(params) > 1: + raise errors.MaxArgumentError(name=name, count=1) + [method_name] = params + return method_name def methodSignature(self, *params): - return u'methodSignature not implemented' + """get method signature for XML-RPC introspection""" + method_name = self._get_method_name('system.methodSignature', *params) + if method_name in self._system_commands: + # TODO + # for now let's not go out of our way to document standard XML-RPC + return u'undef' + elif method_name in self.Command: + # All IPA commands return a dict (struct), + # and take a params, options - list and dict (array, struct) + return [[u'struct', u'array', u'struct']] + else: + raise errors.CommandError(name=method_name) def methodHelp(self, *params): - return u'methodHelp not implemented' + """get method docstring for XML-RPC introspection""" + method_name = self._get_method_name('system.methodHelp', *params) + if method_name in self._system_commands: + return u'' + elif method_name in self.Command: + return unicode(self.Command[method_name].__doc__ or '') + else: + raise errors.CommandError(name=method_name) + + _system_commands = { + 'system.listMethods': listMethods, + 'system.methodSignature': methodSignature, + 'system.methodHelp': methodHelp, + } def unmarshal(self, data): (params, name) = xml_loads(data) + if name in self._system_commands: + # For XML-RPC introspection, return params directly + return (name, params, {}, None) (args, options) = params_2_args_options(params) if 'version' not in options: # Keep backwards compatibility with client containing diff --git a/ipatests/test_ipalib/test_rpc.py b/ipatests/test_ipalib/test_rpc.py index 56b8184cf..0369d04c4 100644 --- a/ipatests/test_ipalib/test_rpc.py +++ b/ipatests/test_ipalib/test_rpc.py @@ -21,13 +21,14 @@ Test the `ipalib.rpc` module. """ -import threading -from xmlrpclib import Binary, Fault, dumps, loads, ServerProxy +from xmlrpclib import Binary, Fault, dumps, loads + +import nose from ipatests.util import raises, assert_equal, PluginTester, DummyClass from ipatests.data import binary_bytes, utf8_bytes, unicode_str from ipalib.frontend import Command from ipalib.request import context, Connection -from ipalib import rpc, errors +from ipalib import rpc, errors, api, request std_compound = (binary_bytes, utf8_bytes, unicode_str) @@ -242,3 +243,88 @@ class test_xmlclient(PluginTester): assert_equal(e.error, u'no such error') assert context.xmlclient.conn._calledall() is True + + +class test_xml_introspection(object): + @classmethod + def setUpClass(self): + try: + api.Backend.xmlclient.connect(fallback=False) + except (errors.NetworkError, IOError): + raise nose.SkipTest('%r: Server not available: %r' % + (__name__, api.env.xmlrpc_uri)) + + @classmethod + def tearDownClass(self): + request.destroy_context() + + def test_list_methods(self): + result = api.Backend.xmlclient.conn.system.listMethods() + assert len(result) + assert 'ping' in result + assert 'user_add' in result + assert 'system.listMethods' in result + assert 'system.methodSignature' in result + assert 'system.methodHelp' in result + + def test_list_methods_many_params(self): + try: + result = api.Backend.xmlclient.conn.system.listMethods('foo') + except Fault, f: + print f + assert f.faultCode == 3003 + assert f.faultString == ( + "command 'system.listMethods' takes no arguments") + else: + raise AssertionError('did not raise') + + def test_ping_signature(self): + result = api.Backend.xmlclient.conn.system.methodSignature('ping') + assert result == [['struct', 'array', 'struct']] + + + def test_ping_help(self): + result = api.Backend.xmlclient.conn.system.methodHelp('ping') + assert result == 'Ping a remote server.' + + def test_signature_no_params(self): + try: + result = api.Backend.xmlclient.conn.system.methodSignature() + except Fault, f: + print f + assert f.faultCode == 3007 + assert f.faultString == "'method name' is required" + else: + raise AssertionError('did not raise') + + def test_signature_many_params(self): + try: + result = api.Backend.xmlclient.conn.system.methodSignature('a', 'b') + except Fault, f: + print f + assert f.faultCode == 3004 + assert f.faultString == ( + "command 'system.methodSignature' takes at most 1 argument") + else: + raise AssertionError('did not raise') + + def test_help_no_params(self): + try: + result = api.Backend.xmlclient.conn.system.methodHelp() + except Fault, f: + print f + assert f.faultCode == 3007 + assert f.faultString == "'method name' is required" + else: + raise AssertionError('did not raise') + + def test_help_many_params(self): + try: + result = api.Backend.xmlclient.conn.system.methodHelp('a', 'b') + except Fault, f: + print f + assert f.faultCode == 3004 + assert f.faultString == ( + "command 'system.methodHelp' takes at most 1 argument") + else: + raise AssertionError('did not raise') -- cgit