mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Added stuff for managing connections and new Executioner backend base class
This commit is contained in:
committed by
Rob Crittenden
parent
e0b00d5981
commit
f7375bb609
@@ -21,7 +21,10 @@
|
||||
Base classes for all backed-end plugins.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import plugable
|
||||
from errors2 import PublicError, InternalError, CommandError
|
||||
from request import context, Connection, destroy_context
|
||||
|
||||
|
||||
class Backend(plugable.Plugin):
|
||||
@@ -29,7 +32,70 @@ class Backend(plugable.Plugin):
|
||||
Base class for all backend plugins.
|
||||
"""
|
||||
|
||||
__proxy__ = False # Backend plugins are not wrapped in a PluginProxy
|
||||
__proxy__ = False # Backend plugins are not wrapped in a PluginProxy
|
||||
|
||||
|
||||
class Connectible(Backend):
|
||||
# Override in subclass:
|
||||
connection_klass = None
|
||||
|
||||
def connect(self, *args, **kw):
|
||||
"""
|
||||
Create thread-local connection.
|
||||
"""
|
||||
if hasattr(context, self.name):
|
||||
raise StandardError(
|
||||
"connection 'context.%s' already exists in thread %r" % (
|
||||
self.name, threading.currentThread().getName()
|
||||
)
|
||||
)
|
||||
if not issubclass(self.connection_klass, Connection):
|
||||
raise ValueError(
|
||||
'%s.connection_klass must be a request.Connection subclass' % self.name
|
||||
)
|
||||
conn = self.connection_klass(*args, **kw)
|
||||
setattr(context, self.name, conn)
|
||||
assert self.conn is conn.conn
|
||||
|
||||
def isconnected(self):
|
||||
"""
|
||||
Return ``True`` if thread-local connection on `request.context` exists.
|
||||
"""
|
||||
return hasattr(context, self.name)
|
||||
|
||||
def __get_conn(self):
|
||||
"""
|
||||
Return thread-local connection.
|
||||
"""
|
||||
if not hasattr(context, self.name):
|
||||
raise AttributeError('no context.%s in thread %r' % (
|
||||
self.name, threading.currentThread().getName())
|
||||
)
|
||||
return getattr(context, self.name).conn
|
||||
conn = property(__get_conn)
|
||||
|
||||
|
||||
class Executioner(Backend):
|
||||
|
||||
def execute(self, name, *args, **options):
|
||||
error = None
|
||||
try:
|
||||
if name not in self.Command:
|
||||
raise CommandError(name=name)
|
||||
result = self.Command[name](*args, **options)
|
||||
except PublicError, e:
|
||||
error = e
|
||||
except StandardError, e:
|
||||
self.exception(
|
||||
'non-public: %s: %s', e.__class__.__name__, str(e)
|
||||
)
|
||||
error = InternalError()
|
||||
destroy_context()
|
||||
if error is None:
|
||||
return result
|
||||
assert isinstance(error, PublicError)
|
||||
raise error
|
||||
|
||||
|
||||
|
||||
class Context(plugable.Plugin):
|
||||
|
||||
@@ -25,6 +25,7 @@ Per-request thread-local data.
|
||||
import threading
|
||||
import locale
|
||||
import gettext
|
||||
from base import ReadOnly, lock
|
||||
from constants import OVERRIDE_ERROR
|
||||
|
||||
|
||||
@@ -32,6 +33,38 @@ from constants import OVERRIDE_ERROR
|
||||
context = threading.local()
|
||||
|
||||
|
||||
class Connection(ReadOnly):
|
||||
"""
|
||||
Base class for connection objects stored on `request.context`.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
self.conn = self.create(*args, **kw)
|
||||
lock(self)
|
||||
|
||||
def create(self, *args, **kw):
|
||||
"""
|
||||
Create and return the connection (implement in subclass).
|
||||
"""
|
||||
raise NotImplementedError('%s.create()' % self.__class__.__name__)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the connection (implement in subclass).
|
||||
"""
|
||||
raise NotImplementedError('%s.close()' % self.__class__.__name__)
|
||||
|
||||
|
||||
def destroy_context():
|
||||
"""
|
||||
Delete all attributes on thread-local `request.context`.
|
||||
"""
|
||||
for (name, value) in context.__dict__.items():
|
||||
if isinstance(value, Connection):
|
||||
value.close()
|
||||
delattr(context, name)
|
||||
|
||||
|
||||
def ugettext(message):
|
||||
if hasattr(context, 'ugettext'):
|
||||
return context.ugettext(message)
|
||||
|
||||
@@ -21,8 +21,13 @@
|
||||
Test the `ipalib.backend` module.
|
||||
"""
|
||||
|
||||
from ipalib import backend, plugable, errors
|
||||
from tests.util import ClassChecker, raises
|
||||
import threading
|
||||
from tests.util import ClassChecker, raises, create_test_api
|
||||
from tests.data import unicode_str
|
||||
from ipalib.request import context, Connection
|
||||
from ipalib.frontend import Command
|
||||
from ipalib import backend, plugable, errors2, base
|
||||
|
||||
|
||||
|
||||
class test_Backend(ClassChecker):
|
||||
@@ -37,6 +42,176 @@ class test_Backend(ClassChecker):
|
||||
assert self.cls.__proxy__ is False
|
||||
|
||||
|
||||
class DummyConnection(Connection):
|
||||
|
||||
def create(self, *args, **kw):
|
||||
self.args = args
|
||||
self.kw = kw
|
||||
self.closed = False
|
||||
return 'The connection'
|
||||
|
||||
def close(self):
|
||||
assert self.closed is False
|
||||
object.__setattr__(self, 'closed', True)
|
||||
|
||||
|
||||
class test_Connectible(ClassChecker):
|
||||
"""
|
||||
Test the `ipalib.backend.Connectible` class.
|
||||
"""
|
||||
|
||||
_cls = backend.Connectible
|
||||
|
||||
def test_connect(self):
|
||||
"""
|
||||
Test the `ipalib.backend.Connectible.connect` method.
|
||||
"""
|
||||
# Test that TypeError is raised when connection_klass isn't a
|
||||
# Connection subclass:
|
||||
class bad(self.cls):
|
||||
connection_klass = base.ReadOnly
|
||||
o = bad()
|
||||
m = '%s.connection_klass must be a request.Connection subclass'
|
||||
e = raises(ValueError, o.connect)
|
||||
assert str(e) == m % 'bad'
|
||||
|
||||
# Test that connection is created:
|
||||
class example(self.cls):
|
||||
connection_klass = DummyConnection
|
||||
o = example()
|
||||
args = ('Arg1', 'Arg2', 'Arg3')
|
||||
kw = dict(key1='Val1', key2='Val2', key3='Val3')
|
||||
assert not hasattr(context, 'example')
|
||||
assert o.connect(*args, **kw) is None
|
||||
conn = context.example
|
||||
assert type(conn) is DummyConnection
|
||||
assert conn.args == args
|
||||
assert conn.kw == kw
|
||||
assert conn.conn == 'The connection'
|
||||
|
||||
# Test that StandardError is raised if already connected:
|
||||
m = "connection 'context.%s' already exists in thread %r"
|
||||
e = raises(StandardError, o.connect, *args, **kw)
|
||||
assert str(e) == m % ('example', threading.currentThread().getName())
|
||||
|
||||
# Double check that it works after deleting context.example:
|
||||
del context.example
|
||||
assert o.connect(*args, **kw) is None
|
||||
|
||||
def test_isconnected(self):
|
||||
"""
|
||||
Test the `ipalib.backend.Connectible.isconnected` method.
|
||||
"""
|
||||
class example(self.cls):
|
||||
pass
|
||||
for klass in (self.cls, example):
|
||||
o = klass()
|
||||
assert o.isconnected() is False
|
||||
conn = DummyConnection()
|
||||
setattr(context, klass.__name__, conn)
|
||||
assert o.isconnected() is True
|
||||
delattr(context, klass.__name__)
|
||||
|
||||
def test_conn(self):
|
||||
"""
|
||||
Test the `ipalib.backend.Connectible.conn` property.
|
||||
"""
|
||||
msg = 'no context.%s in thread %r'
|
||||
class example(self.cls):
|
||||
pass
|
||||
for klass in (self.cls, example):
|
||||
o = klass()
|
||||
e = raises(AttributeError, getattr, o, 'conn')
|
||||
assert str(e) == msg % (
|
||||
klass.__name__, threading.currentThread().getName()
|
||||
)
|
||||
conn = DummyConnection()
|
||||
setattr(context, klass.__name__, conn)
|
||||
assert o.conn is conn.conn
|
||||
delattr(context, klass.__name__)
|
||||
|
||||
|
||||
class test_Executioner(ClassChecker):
|
||||
"""
|
||||
Test the `ipalib.backend.Executioner` class.
|
||||
"""
|
||||
_cls = backend.Executioner
|
||||
|
||||
def test_execute(self):
|
||||
"""
|
||||
Test the `ipalib.backend.Executioner.execute` method.
|
||||
"""
|
||||
(api, home) = create_test_api(in_server=True)
|
||||
|
||||
class echo(Command):
|
||||
takes_args = ['arg1', 'arg2+']
|
||||
takes_options = ['option1?', 'option2?']
|
||||
def execute(self, *args, **options):
|
||||
assert type(args[1]) is tuple
|
||||
return args + (options,)
|
||||
api.register(echo)
|
||||
|
||||
class good(Command):
|
||||
def execute(self):
|
||||
raise errors2.ValidationError(
|
||||
name='nurse',
|
||||
error=u'Not naughty!',
|
||||
)
|
||||
api.register(good)
|
||||
|
||||
class bad(Command):
|
||||
def execute(self):
|
||||
raise ValueError('This is private.')
|
||||
api.register(bad)
|
||||
|
||||
api.finalize()
|
||||
o = self.cls()
|
||||
o.set_api(api)
|
||||
o.finalize()
|
||||
|
||||
# Test that CommandError is raised:
|
||||
conn = DummyConnection()
|
||||
context.someconn = conn
|
||||
e = raises(errors2.CommandError, o.execute, 'nope')
|
||||
assert e.name == 'nope'
|
||||
assert conn.closed is True # Make sure destroy_context() was called
|
||||
assert context.__dict__.keys() == []
|
||||
|
||||
# Test with echo command:
|
||||
arg1 = unicode_str
|
||||
arg2 = (u'Hello', unicode_str, u'world!')
|
||||
args = (arg1,) + arg2
|
||||
options = dict(option1=u'How are you?', option2=unicode_str)
|
||||
|
||||
conn = DummyConnection()
|
||||
context.someconn = conn
|
||||
assert o.execute('echo', arg1, arg2, **options) == (arg1, arg2, options)
|
||||
assert conn.closed is True # Make sure destroy_context() was called
|
||||
assert context.__dict__.keys() == []
|
||||
|
||||
conn = DummyConnection()
|
||||
context.someconn = conn
|
||||
assert o.execute('echo', *args, **options) == (arg1, arg2, options)
|
||||
assert conn.closed is True # Make sure destroy_context() was called
|
||||
assert context.__dict__.keys() == []
|
||||
|
||||
# Test with good command:
|
||||
conn = DummyConnection()
|
||||
context.someconn = conn
|
||||
e = raises(errors2.ValidationError, o.execute, 'good')
|
||||
assert e.name == 'nurse'
|
||||
assert e.error == u'Not naughty!'
|
||||
assert conn.closed is True # Make sure destroy_context() was called
|
||||
assert context.__dict__.keys() == []
|
||||
|
||||
# Test with bad command:
|
||||
conn = DummyConnection()
|
||||
context.someconn = conn
|
||||
e = raises(errors2.InternalError, o.execute, 'bad')
|
||||
assert conn.closed is True # Make sure destroy_context() was called
|
||||
assert context.__dict__.keys() == []
|
||||
|
||||
|
||||
class test_Context(ClassChecker):
|
||||
"""
|
||||
Test the `ipalib.backend.Context` class.
|
||||
|
||||
@@ -207,9 +207,9 @@ class ClassChecker(object):
|
||||
"""
|
||||
nose tear-down fixture.
|
||||
"""
|
||||
for name in ('ugettext', 'ungettext'):
|
||||
if hasattr(context, name):
|
||||
delattr(context, name)
|
||||
for name in context.__dict__.keys():
|
||||
delattr(context, name)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user