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') 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): class InternalError(PublicError):
""" """
**902** Raised to conceal a non-public exception. **903** Raised to conceal a non-public exception.
For example: For example:
@ -288,7 +305,7 @@ class InternalError(PublicError):
InternalError: an internal error has occured InternalError: an internal error has occured
""" """
errno = 902 errno = 903
format = _('an internal error has occured') format = _('an internal error has occured')
def __init__(self, message=None): def __init__(self, message=None):
@ -300,7 +317,7 @@ class InternalError(PublicError):
class ServerInternalError(PublicError): class ServerInternalError(PublicError):
""" """
**903** Raised when client catches an `InternalError` from server. **904** Raised when client catches an `InternalError` from server.
For example: For example:
@ -310,13 +327,13 @@ class ServerInternalError(PublicError):
ServerInternalError: an internal error has occured on server at 'https://localhost' 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') format = _('an internal error has occured on server at %(server)r')
class CommandError(PublicError): class CommandError(PublicError):
""" """
**904** Raised when an unknown command is called. **905** Raised when an unknown command is called.
For example: For example:
@ -326,13 +343,13 @@ class CommandError(PublicError):
CommandError: unknown command 'foobar' CommandError: unknown command 'foobar'
""" """
errno = 904 errno = 905
format = _('unknown command %(name)r') format = _('unknown command %(name)r')
class ServerCommandError(PublicError): class ServerCommandError(PublicError):
""" """
**905** Raised when client catches a `CommandError` from server. **906** Raised when client catches a `CommandError` from server.
For example: For example:
@ -343,13 +360,13 @@ class ServerCommandError(PublicError):
ServerCommandError: error on server 'https://localhost': unknown command 'foobar' ServerCommandError: error on server 'https://localhost': unknown command 'foobar'
""" """
errno = 905 errno = 906
format = _('error on server %(server)r: %(error)s') format = _('error on server %(server)r: %(error)s')
class NetworkError(PublicError): class NetworkError(PublicError):
""" """
**906** Raised when a network connection cannot be created. **907** Raised when a network connection cannot be created.
For example: For example:
@ -359,13 +376,13 @@ class NetworkError(PublicError):
NetworkError: cannot connect to 'ldap://localhost:389' NetworkError: cannot connect to 'ldap://localhost:389'
""" """
errno = 906 errno = 907
format = _('cannot connect to %(uri)r') format = _('cannot connect to %(uri)r')
class ServerNetworkError(PublicError): class ServerNetworkError(PublicError):
""" """
**907** Raised when client catches a `NetworkError` from server. **908** Raised when client catches a `NetworkError` from server.
For example: For example:
@ -376,7 +393,7 @@ class ServerNetworkError(PublicError):
ServerNetworkError: error on server 'https://localhost': cannot connect to 'ldap://localhost:389' ServerNetworkError: error on server 'https://localhost': cannot connect to 'ldap://localhost:389'
""" """
errno = 907 errno = 908
format = _('error on server %(server)r: %(error)s') format = _('error on server %(server)r: %(error)s')

View File

@ -30,7 +30,11 @@ Also see the `ipaserver.rpcserver` module.
""" """
from types import NoneType from types import NoneType
import threading
from xmlrpclib import Binary, Fault, dumps, loads 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): def xml_wrap(value):
@ -155,3 +159,49 @@ def xml_loads(data):
""" """
(params, method) = loads(data) (params, method) = loads(data)
return (xml_unwrap(params), method) 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): def dispatch(self, method, params):
assert type(method) is str
assert type(params) is tuple
self.debug('Received RPC call to %r', method) self.debug('Received RPC call to %r', method)
if method not in self.Command: if method not in self.Command:
raise CommandError(name=method) raise CommandError(name=method)

View File

@ -21,10 +21,16 @@
Test the `ipalib.rpc` module. Test the `ipalib.rpc` module.
""" """
import threading
from xmlrpclib import Binary, Fault, dumps, loads 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 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): def dump_n_load(value):
@ -170,3 +176,74 @@ def test_xml_loads():
e = raises(Fault, f, data) e = raises(Fault, f, data)
assert e.faultCode == 69 assert e.faultCode == 69
assert_equal(e.faultString, unicode_str) 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: if n == 1:
return self.translation_singular return self.translation_singular
return self.translation_plural 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)