Added rpc.xmlclient backend plugin for forwarding; added corresponding unit tests

This commit is contained in:
Jason Gerard DeRose 2009-01-19 21:10:42 -07:00
parent bae9dd7c07
commit 55fba5420d
5 changed files with 203 additions and 16 deletions

View File

@ -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')

View File

@ -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,
)

View File

@ -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)

View File

@ -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

View File

@ -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)