diff options
-rw-r--r-- | ipalib/errors2.py | 41 | ||||
-rw-r--r-- | ipalib/rpc.py | 50 | ||||
-rw-r--r-- | ipaserver/rpcserver.py | 2 | ||||
-rw-r--r-- | tests/test_ipalib/test_rpc.py | 81 | ||||
-rw-r--r-- | tests/util.py | 45 |
5 files changed, 203 insertions, 16 deletions
diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 4c8acd5d8..7e2eea058 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -275,10 +275,27 @@ class VersionError(PublicError): format = _('%(cver)s client incompatible with %(sver)s server at %(server)r') +class UnknownError(PublicError): + """ + **902** Raised when client does not know error it caught from server. + + For example: + + >>> raise UnknownError(code=57, server='localhost', error=u'a new error') + ... + Traceback (most recent call last): + ... + UnknownError: unknown error 57 from localhost: a new error + + """ + + errno = 902 + format = _('unknown error %(code)d from %(server)s: %(error)s') + class InternalError(PublicError): """ - **902** Raised to conceal a non-public exception. + **903** Raised to conceal a non-public exception. For example: @@ -288,7 +305,7 @@ class InternalError(PublicError): InternalError: an internal error has occured """ - errno = 902 + errno = 903 format = _('an internal error has occured') def __init__(self, message=None): @@ -300,7 +317,7 @@ class InternalError(PublicError): class ServerInternalError(PublicError): """ - **903** Raised when client catches an `InternalError` from server. + **904** Raised when client catches an `InternalError` from server. For example: @@ -310,13 +327,13 @@ class ServerInternalError(PublicError): ServerInternalError: an internal error has occured on server at 'https://localhost' """ - errno = 903 + errno = 904 format = _('an internal error has occured on server at %(server)r') class CommandError(PublicError): """ - **904** Raised when an unknown command is called. + **905** Raised when an unknown command is called. For example: @@ -326,13 +343,13 @@ class CommandError(PublicError): CommandError: unknown command 'foobar' """ - errno = 904 + errno = 905 format = _('unknown command %(name)r') class ServerCommandError(PublicError): """ - **905** Raised when client catches a `CommandError` from server. + **906** Raised when client catches a `CommandError` from server. For example: @@ -343,13 +360,13 @@ class ServerCommandError(PublicError): ServerCommandError: error on server 'https://localhost': unknown command 'foobar' """ - errno = 905 + errno = 906 format = _('error on server %(server)r: %(error)s') class NetworkError(PublicError): """ - **906** Raised when a network connection cannot be created. + **907** Raised when a network connection cannot be created. For example: @@ -359,13 +376,13 @@ class NetworkError(PublicError): NetworkError: cannot connect to 'ldap://localhost:389' """ - errno = 906 + errno = 907 format = _('cannot connect to %(uri)r') class ServerNetworkError(PublicError): """ - **907** Raised when client catches a `NetworkError` from server. + **908** Raised when client catches a `NetworkError` from server. For example: @@ -376,7 +393,7 @@ class ServerNetworkError(PublicError): ServerNetworkError: error on server 'https://localhost': cannot connect to 'ldap://localhost:389' """ - errno = 907 + errno = 908 format = _('error on server %(server)r: %(error)s') diff --git a/ipalib/rpc.py b/ipalib/rpc.py index acfdae95d..e7823ef95 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -30,7 +30,11 @@ Also see the `ipaserver.rpcserver` module. """ from types import NoneType +import threading from xmlrpclib import Binary, Fault, dumps, loads +from ipalib.backend import Backend +from ipalib.errors2 import public_errors, PublicError, UnknownError +from ipalib.request import context def xml_wrap(value): @@ -155,3 +159,49 @@ def xml_loads(data): """ (params, method) = loads(data) return (xml_unwrap(params), method) + + +class xmlclient(Backend): + """ + Forwarding backend for XML-RPC client. + """ + + def __init__(self): + super(xmlclient, self).__init__() + self.__errors = dict((e.errno, e) for e in public_errors) + + def forward(self, name, *args, **kw): + """ + Forward call to command named ``name`` over XML-RPC. + + This method will encode and forward an XML-RPC request, and will then + decode and return the corresponding XML-RPC response. + + :param command: The name of the command being forwarded. + :param args: Positional arguments to pass to remote command. + :param kw: Keyword arguments to pass to remote command. + """ + if name not in self.Command: + raise ValueError( + '%s.forward(): %r not in api.Command' % (self.name, name) + ) + if not hasattr(context, 'xmlconn'): + raise StandardError( + '%s.forward(%r): need context.xmlconn in thread %r' % ( + self.name, name, threading.currentThread().getName() + ) + ) + command = getattr(context.xmlconn, name) + params = args + (kw,) + try: + response = command(xml_wrap(params)) + return xml_unwrap(response) + except Fault, e: + if e.faultCode in self.__errors: + error = self.__errors[e.faultCode] + raise error(message=e.faultString) + raise UnknownError( + code=e.faultCode, + error=e.faultString, + server=self.env.xmlrpc_uri, + ) diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 8b16eda93..225173675 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -44,8 +44,6 @@ class xmlserver(Backend): """ def dispatch(self, method, params): - assert type(method) is str - assert type(params) is tuple self.debug('Received RPC call to %r', method) if method not in self.Command: raise CommandError(name=method) diff --git a/tests/test_ipalib/test_rpc.py b/tests/test_ipalib/test_rpc.py index eba35261b..296e9bc1c 100644 --- a/tests/test_ipalib/test_rpc.py +++ b/tests/test_ipalib/test_rpc.py @@ -21,10 +21,16 @@ Test the `ipalib.rpc` module. """ +import threading from xmlrpclib import Binary, Fault, dumps, loads -from tests.util import raises, assert_equal +from tests.util import raises, assert_equal, PluginTester, DummyClass from tests.data import binary_bytes, utf8_bytes, unicode_str -from ipalib import rpc +from ipalib.frontend import Command +from ipalib.request import context +from ipalib import rpc, errors2 + + +std_compound = (binary_bytes, utf8_bytes, unicode_str) def dump_n_load(value): @@ -170,3 +176,74 @@ def test_xml_loads(): e = raises(Fault, f, data) assert e.faultCode == 69 assert_equal(e.faultString, unicode_str) + + +class test_xmlclient(PluginTester): + """ + Test the `ipalib.rpc.xmlclient` plugin. + """ + _plugin = rpc.xmlclient + + def test_forward(self): + """ + Test the `ipalib.rpc.xmlclient.forward` method. + """ + class user_add(Command): + pass + + # Test that ValueError is raised when forwarding a command that is not + # in api.Command: + (o, api, home) = self.instance('Backend', in_server=False) + e = raises(ValueError, o.forward, 'user_add') + assert str(e) == '%s.forward(): %r not in api.Command' % ( + 'xmlclient', 'user_add' + ) + + # Test that StandardError is raised when context.xmlconn does not exist: + (o, api, home) = self.instance('Backend', user_add, in_server=False) + e = raises(StandardError, o.forward, 'user_add') + assert str(e) == '%s.forward(%r): need context.xmlconn in thread %r' % ( + 'xmlclient', 'user_add', threading.currentThread().getName() + ) + + args = (binary_bytes, utf8_bytes, unicode_str) + kw = dict(one=binary_bytes, two=utf8_bytes, three=unicode_str) + params = args + (kw,) + result = (unicode_str, binary_bytes, utf8_bytes) + context.xmlconn = DummyClass( + ( + 'user_add', + (rpc.xml_wrap(params),), + {}, + rpc.xml_wrap(result), + ), + ( + 'user_add', + (rpc.xml_wrap(params),), + {}, + Fault(3005, u"'four' is required"), # RequirementError + ), + ( + 'user_add', + (rpc.xml_wrap(params),), + {}, + Fault(700, u'no such error'), # There is no error 700 + ), + + ) + + # Test with a successful return value: + assert o.forward('user_add', *args, **kw) == result + + # Test with an errno the client knows: + e = raises(errors2.RequirementError, o.forward, 'user_add', *args, **kw) + assert_equal(e.message, u"'four' is required") + + # Test with an errno the client doesn't know + e = raises(errors2.UnknownError, o.forward, 'user_add', *args, **kw) + assert_equal(e.code, 700) + assert_equal(e.error, u'no such error') + + assert context.xmlconn._calledall() is True + + del context.xmlconn diff --git a/tests/util.py b/tests/util.py index af4c23934..f5899dfab 100644 --- a/tests/util.py +++ b/tests/util.py @@ -344,3 +344,48 @@ class dummy_ungettext(object): if n == 1: return self.translation_singular return self.translation_plural + + +class DummyMethod(object): + def __init__(self, callback, name): + self.__callback = callback + self.__name = name + + def __call__(self, *args, **kw): + return self.__callback(self.__name, args, kw) + + +class DummyClass(object): + def __init__(self, *calls): + self.__calls = calls + self.__i = 0 + for (name, args, kw, result) in calls: + method = DummyMethod(self.__process, name) + setattr(self, name, method) + + def __process(self, name_, args_, kw_): + if self.__i >= len(self.__calls): + raise AssertionError( + 'extra call: %s, %r, %r' % (name, args, kw) + ) + (name, args, kw, result) = self.__calls[self.__i] + self.__i += 1 + i = self.__i + if name_ != name: + raise AssertionError( + 'call %d should be to method %r; got %r' % (i, name, name_) + ) + if args_ != args: + raise AssertionError( + 'call %d to %r should have args %r; got %r' % (i, name, args, args_) + ) + if kw_ != kw: + raise AssertionError( + 'call %d to %r should have kw %r, got %r' % (i, name, kw, kw_) + ) + if isinstance(result, Exception): + raise result + return result + + def _calledall(self): + return self.__i == len(self.__calls) |