2015-06-02 07:04:25 -05:00
|
|
|
#
|
|
|
|
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
|
|
|
#
|
|
|
|
|
|
|
|
"""
|
|
|
|
The framework core.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import abc
|
|
|
|
import itertools
|
|
|
|
|
|
|
|
from ipapython.ipa_log_manager import root_logger
|
|
|
|
|
|
|
|
from . import util
|
|
|
|
from .util import from_
|
|
|
|
|
|
|
|
__all__ = ['InvalidStateError', 'KnobValueError', 'Property', 'Knob',
|
|
|
|
'Configurable', 'Group', 'Component', 'Composite']
|
|
|
|
|
|
|
|
# Configurable states
|
|
|
|
_VALIDATE_PENDING = 'VALIDATE_PENDING'
|
|
|
|
_VALIDATE_RUNNING = 'VALIDATE_RUNNING'
|
|
|
|
_EXECUTE_PENDING = 'EXECUTE_PENDING'
|
|
|
|
_EXECUTE_RUNNING = 'EXECUTE_RUNNING'
|
|
|
|
_STOPPED = 'STOPPED'
|
|
|
|
_FAILED = 'FAILED'
|
|
|
|
_CLOSED = 'CLOSED'
|
|
|
|
|
|
|
|
_missing = object()
|
|
|
|
_counter = itertools.count()
|
|
|
|
|
|
|
|
|
|
|
|
def _class_cmp(a, b):
|
|
|
|
if a is b:
|
|
|
|
return 0
|
|
|
|
elif issubclass(a, b):
|
|
|
|
return -1
|
|
|
|
elif issubclass(b, a):
|
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidStateError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class KnobValueError(ValueError):
|
|
|
|
def __init__(self, name, message):
|
|
|
|
super(KnobValueError, self).__init__(message)
|
|
|
|
self.name = name
|
|
|
|
|
|
|
|
|
|
|
|
class InnerClass(object):
|
|
|
|
__metaclass__ = util.InnerClassMeta
|
|
|
|
__outer_class__ = None
|
|
|
|
__outer_name__ = None
|
|
|
|
|
|
|
|
|
|
|
|
class PropertyBase(InnerClass):
|
|
|
|
@property
|
|
|
|
def default(self):
|
|
|
|
raise AttributeError('default')
|
|
|
|
|
|
|
|
def __init__(self, outer):
|
|
|
|
self.outer = outer
|
|
|
|
|
|
|
|
def __get__(self, obj, obj_type):
|
|
|
|
try:
|
|
|
|
return obj._get_property(self.__outer_name__)
|
|
|
|
except AttributeError:
|
|
|
|
if not hasattr(self, 'default'):
|
|
|
|
raise
|
|
|
|
return self.default
|
|
|
|
|
|
|
|
|
|
|
|
def Property(default=_missing):
|
|
|
|
class_dict = {}
|
|
|
|
if default is not _missing:
|
|
|
|
class_dict['default'] = default
|
|
|
|
|
|
|
|
return util.InnerClassMeta('Property', (PropertyBase,), class_dict)
|
|
|
|
|
|
|
|
|
|
|
|
class KnobBase(PropertyBase):
|
|
|
|
type = None
|
|
|
|
initializable = True
|
|
|
|
sensitive = False
|
|
|
|
deprecated = False
|
|
|
|
description = None
|
|
|
|
cli_name = None
|
|
|
|
cli_short_name = None
|
|
|
|
cli_aliases = None
|
|
|
|
cli_metavar = None
|
|
|
|
|
|
|
|
_order = None
|
|
|
|
|
|
|
|
def __set__(self, obj, value):
|
|
|
|
try:
|
|
|
|
self.validate(value)
|
|
|
|
except KnobValueError:
|
|
|
|
raise
|
|
|
|
except ValueError as e:
|
|
|
|
raise KnobValueError(self.__outer_name__, str(e))
|
|
|
|
|
|
|
|
obj.__dict__[self.__outer_name__] = value
|
|
|
|
|
|
|
|
def __delete__(self, obj):
|
|
|
|
try:
|
|
|
|
del obj.__dict__[self.__outer_name__]
|
|
|
|
except KeyError:
|
|
|
|
raise AttributeError(self.__outer_name__)
|
|
|
|
|
|
|
|
def validate(self, value):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def default_getter(cls, func):
|
|
|
|
@property
|
|
|
|
def default(self):
|
|
|
|
return func(self.outer)
|
|
|
|
cls.default = default
|
|
|
|
|
|
|
|
return cls
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def validator(cls, func):
|
|
|
|
def validate(self, value):
|
|
|
|
func(self.outer, value)
|
|
|
|
super(cls, self).validate(value)
|
|
|
|
cls.validate = validate
|
|
|
|
|
|
|
|
return cls
|
|
|
|
|
|
|
|
|
|
|
|
def Knob(type, default=_missing, initializable=_missing, sensitive=_missing,
|
|
|
|
deprecated=_missing, description=_missing, cli_name=_missing,
|
|
|
|
cli_short_name=_missing, cli_aliases=_missing, cli_metavar=_missing):
|
|
|
|
class_dict = {}
|
|
|
|
class_dict['_order'] = next(_counter)
|
|
|
|
class_dict['type'] = type
|
|
|
|
if default is not _missing:
|
|
|
|
class_dict['default'] = default
|
|
|
|
if sensitive is not _missing:
|
|
|
|
class_dict['sensitive'] = sensitive
|
|
|
|
if deprecated is not _missing:
|
|
|
|
class_dict['deprecated'] = deprecated
|
|
|
|
if description is not _missing:
|
|
|
|
class_dict['description'] = description
|
|
|
|
if cli_name is not _missing:
|
|
|
|
class_dict['cli_name'] = cli_name
|
|
|
|
if cli_short_name is not _missing:
|
|
|
|
class_dict['cli_short_name'] = cli_short_name
|
|
|
|
if cli_aliases is not _missing:
|
|
|
|
class_dict['cli_aliases'] = cli_aliases
|
|
|
|
if cli_metavar is not _missing:
|
|
|
|
class_dict['cli_metavar'] = cli_metavar
|
|
|
|
|
|
|
|
return util.InnerClassMeta('Knob', (KnobBase,), class_dict)
|
|
|
|
|
|
|
|
|
|
|
|
class Configurable(object):
|
|
|
|
"""
|
|
|
|
Base class of all configurables.
|
|
|
|
|
|
|
|
FIXME: details of validate/execute, properties and knobs
|
|
|
|
"""
|
|
|
|
|
|
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def knobs(cls):
|
|
|
|
"""
|
|
|
|
Iterate over knobs defined for the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
assert not hasattr(super(Configurable, cls), 'knobs')
|
|
|
|
|
|
|
|
result = []
|
|
|
|
for name in dir(cls):
|
|
|
|
knob_cls = getattr(cls, name)
|
|
|
|
if isinstance(knob_cls, type) and issubclass(knob_cls, KnobBase):
|
|
|
|
result.append(knob_cls)
|
|
|
|
result = sorted(result, key=lambda knob_cls: knob_cls._order)
|
|
|
|
for knob_cls in result:
|
|
|
|
yield knob_cls.__outer_class__, knob_cls.__outer_name__
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def group(cls):
|
|
|
|
assert not hasattr(super(Configurable, cls), 'group')
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Initialize the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.log = root_logger
|
|
|
|
|
|
|
|
for name in dir(self.__class__):
|
|
|
|
if name.startswith('_'):
|
|
|
|
continue
|
|
|
|
property_cls = getattr(self.__class__, name)
|
|
|
|
if not isinstance(property_cls, type):
|
|
|
|
continue
|
|
|
|
if not issubclass(property_cls, PropertyBase):
|
|
|
|
continue
|
|
|
|
if issubclass(property_cls, KnobBase):
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
value = kwargs.pop(name)
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
setattr(self, name, value)
|
|
|
|
|
|
|
|
for owner_cls, name in self.knobs():
|
|
|
|
knob_cls = getattr(owner_cls, name)
|
|
|
|
if not knob_cls.initializable:
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
value = kwargs.pop(name)
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
setattr(self, name, value)
|
|
|
|
|
|
|
|
if kwargs:
|
Use Python3-compatible dict method names
Python 2 has keys()/values()/items(), which return lists,
iterkeys()/itervalues()/iteritems(), which return iterators,
and viewkeys()/viewvalues()/viewitems() which return views.
Python 3 has only keys()/values()/items(), which return views.
To get iterators, one can use iter() or a for loop/comprehension;
for lists there's the list() constructor.
When iterating through the entire dict, without modifying the dict,
the difference between Python 2's items() and iteritems() is
negligible, especially on small dicts (the main overhead is
extra memory, not CPU time). In the interest of simpler code,
this patch changes many instances of iteritems() to items(),
iterkeys() to keys() etc.
In other cases, helpers like six.itervalues are used.
Reviewed-By: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
2015-08-11 06:51:14 -05:00
|
|
|
extra = sorted(kwargs)
|
2015-06-02 07:04:25 -05:00
|
|
|
raise TypeError(
|
|
|
|
"{0}() got {1} unexpected keyword arguments: {2}".format(
|
|
|
|
type(self).__name__,
|
|
|
|
len(extra),
|
|
|
|
', '.join(repr(name) for name in extra)))
|
|
|
|
|
|
|
|
self._reset()
|
|
|
|
|
|
|
|
def _reset(self):
|
|
|
|
assert not hasattr(super(Configurable, self), '_reset')
|
|
|
|
|
|
|
|
self.__state = _VALIDATE_PENDING
|
|
|
|
self.__gen = util.run_generator_with_yield_from(self._configure())
|
|
|
|
|
|
|
|
def _get_components(self):
|
|
|
|
assert not hasattr(super(Configurable, self), '_get_components')
|
|
|
|
|
|
|
|
raise TypeError("{0} is not composite".format(self))
|
|
|
|
|
|
|
|
def _get_property(self, name):
|
|
|
|
assert not hasattr(super(Configurable, self), '_get_property')
|
|
|
|
|
|
|
|
try:
|
|
|
|
return self.__dict__[name]
|
|
|
|
except KeyError:
|
|
|
|
raise AttributeError(name)
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def _configure(self):
|
|
|
|
"""
|
|
|
|
Coroutine which defines the logic of the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
assert not hasattr(super(Configurable, self), '_configure')
|
|
|
|
|
|
|
|
self.__transition(_VALIDATE_RUNNING, _EXECUTE_PENDING)
|
|
|
|
|
|
|
|
while self.__state != _EXECUTE_RUNNING:
|
|
|
|
yield
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
"""
|
|
|
|
Run the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.validate()
|
|
|
|
if self.__state == _EXECUTE_PENDING:
|
|
|
|
self.execute()
|
|
|
|
|
|
|
|
def validate(self):
|
|
|
|
"""
|
|
|
|
Run the validation part of the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
for nothing in self._validator():
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _validator(self):
|
|
|
|
"""
|
|
|
|
Coroutine which runs the validation part of the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return self.__runner(_VALIDATE_PENDING, _VALIDATE_RUNNING)
|
|
|
|
|
|
|
|
def execute(self):
|
|
|
|
"""
|
|
|
|
Run the execution part of the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
for nothing in self._executor():
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _executor(self):
|
|
|
|
"""
|
|
|
|
Coroutine which runs the execution part of the configurable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return self.__runner(_EXECUTE_PENDING, _EXECUTE_RUNNING)
|
|
|
|
|
|
|
|
def done(self):
|
|
|
|
"""
|
|
|
|
Return True if the configurable has finished.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return self.__state in (_STOPPED, _FAILED, _CLOSED)
|
|
|
|
|
|
|
|
def run_until_executing(self, gen):
|
|
|
|
while self.__state != _EXECUTE_RUNNING:
|
|
|
|
try:
|
|
|
|
yield gen.next()
|
|
|
|
except StopIteration:
|
|
|
|
break
|
|
|
|
|
|
|
|
def __runner(self, pending_state, running_state):
|
|
|
|
self.__transition(pending_state, running_state)
|
|
|
|
|
|
|
|
step = self.__gen.next
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
step()
|
|
|
|
except StopIteration:
|
|
|
|
self.__transition(running_state, _STOPPED)
|
|
|
|
break
|
|
|
|
except GeneratorExit:
|
|
|
|
self.__transition(running_state, _CLOSED)
|
|
|
|
break
|
|
|
|
except BaseException:
|
|
|
|
exc_info = sys.exc_info()
|
|
|
|
try:
|
|
|
|
self._handle_exception(exc_info)
|
|
|
|
except BaseException:
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
finally:
|
|
|
|
self.__transition(running_state, _FAILED)
|
|
|
|
|
|
|
|
if self.__state != running_state:
|
|
|
|
break
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
except BaseException:
|
|
|
|
exc_info = sys.exc_info()
|
|
|
|
step = lambda: self.__gen.throw(*exc_info)
|
|
|
|
else:
|
|
|
|
step = self.__gen.next
|
|
|
|
|
|
|
|
def _handle_exception(self, exc_info):
|
|
|
|
assert not hasattr(super(Configurable, self), '_handle_exception')
|
|
|
|
|
|
|
|
util.raise_exc_info(exc_info)
|
|
|
|
|
|
|
|
def __transition(self, from_state, to_state):
|
|
|
|
if self.__state != from_state:
|
|
|
|
raise InvalidStateError(self.__state)
|
|
|
|
|
|
|
|
self.__state = to_state
|
|
|
|
|
|
|
|
|
|
|
|
class Group(Configurable):
|
|
|
|
@classmethod
|
|
|
|
def group(cls):
|
|
|
|
return cls
|
|
|
|
|
|
|
|
|
|
|
|
class ComponentMeta(util.InnerClassMeta, abc.ABCMeta):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class ComponentBase(InnerClass, Configurable):
|
|
|
|
__metaclass__ = ComponentMeta
|
|
|
|
|
|
|
|
_order = None
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def group(cls):
|
|
|
|
result = super(ComponentBase, cls).group()
|
|
|
|
if result is not None:
|
|
|
|
return result
|
|
|
|
else:
|
|
|
|
return cls.__outer_class__.group()
|
|
|
|
|
|
|
|
def __init__(self, parent, **kwargs):
|
|
|
|
self.__parent = parent
|
|
|
|
|
|
|
|
super(ComponentBase, self).__init__(**kwargs)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def parent(self):
|
|
|
|
return self.__parent
|
|
|
|
|
|
|
|
def __get__(self, obj, obj_type):
|
|
|
|
obj.__dict__[self.__outer_name__] = self
|
|
|
|
return self
|
|
|
|
|
|
|
|
def _get_property(self, name):
|
|
|
|
try:
|
|
|
|
return super(ComponentBase, self)._get_property(name)
|
|
|
|
except AttributeError:
|
|
|
|
return self.__parent._get_property(name)
|
|
|
|
|
|
|
|
def _handle_exception(self, exc_info):
|
|
|
|
try:
|
|
|
|
super(ComponentBase, self)._handle_exception(exc_info)
|
|
|
|
except BaseException:
|
|
|
|
exc_info = sys.exc_info()
|
|
|
|
self.__parent._handle_exception(exc_info)
|
|
|
|
|
|
|
|
|
|
|
|
def Component(cls):
|
|
|
|
class_dict = {}
|
|
|
|
class_dict['_order'] = next(_counter)
|
|
|
|
|
|
|
|
return ComponentMeta('Component', (ComponentBase, cls), class_dict)
|
|
|
|
|
|
|
|
|
|
|
|
class Composite(Configurable):
|
|
|
|
"""
|
|
|
|
Configurable composed of any number of components.
|
|
|
|
|
|
|
|
Provides knobs of all child components.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def knobs(cls):
|
|
|
|
name_dict = {}
|
|
|
|
owner_dict = {}
|
|
|
|
|
|
|
|
for owner_cls, name in super(Composite, cls).knobs():
|
|
|
|
knob_cls = getattr(owner_cls, name)
|
|
|
|
name_dict[name] = owner_cls
|
|
|
|
owner_dict.setdefault(owner_cls, []).append(knob_cls)
|
|
|
|
|
|
|
|
for owner_cls, name in cls.components():
|
|
|
|
comp_cls = getattr(cls, name)
|
|
|
|
for owner_cls, name in comp_cls.knobs():
|
|
|
|
if hasattr(cls, name):
|
|
|
|
continue
|
|
|
|
|
|
|
|
knob_cls = getattr(owner_cls, name)
|
|
|
|
try:
|
|
|
|
last_owner_cls = name_dict[name]
|
|
|
|
except KeyError:
|
|
|
|
name_dict[name] = owner_cls
|
|
|
|
owner_dict.setdefault(owner_cls, []).append(knob_cls)
|
|
|
|
else:
|
|
|
|
if last_owner_cls is not owner_cls:
|
|
|
|
raise TypeError("{0}.knobs(): conflicting definitions "
|
|
|
|
"of '{1}' in {2} and {3}".format(
|
|
|
|
cls.__name__,
|
|
|
|
name,
|
|
|
|
last_owner_cls.__name__,
|
|
|
|
owner_cls.__name__))
|
|
|
|
|
|
|
|
for owner_cls in sorted(owner_dict, _class_cmp):
|
|
|
|
for knob_cls in owner_dict[owner_cls]:
|
|
|
|
yield knob_cls.__outer_class__, knob_cls.__outer_name__
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def components(cls):
|
|
|
|
assert not hasattr(super(Composite, cls), 'components')
|
|
|
|
|
|
|
|
result = []
|
|
|
|
for name in dir(cls):
|
|
|
|
comp_cls = getattr(cls, name)
|
|
|
|
if (isinstance(comp_cls, type) and
|
|
|
|
issubclass(comp_cls, ComponentBase)):
|
|
|
|
result.append(comp_cls)
|
|
|
|
result = sorted(result, key=lambda comp_cls: comp_cls._order)
|
|
|
|
for comp_cls in result:
|
|
|
|
yield comp_cls.__outer_class__, comp_cls.__outer_name__
|
|
|
|
|
|
|
|
def _reset(self):
|
|
|
|
self.__components = list(self._get_components())
|
|
|
|
|
|
|
|
super(Composite, self)._reset()
|
|
|
|
|
|
|
|
def _get_components(self):
|
|
|
|
for owner_cls, name in self.components():
|
|
|
|
yield getattr(self, name)
|
|
|
|
|
|
|
|
def _configure(self):
|
|
|
|
validate = [(c, c._validator()) for c in self.__components]
|
|
|
|
while True:
|
|
|
|
new_validate = []
|
|
|
|
for child, validator in validate:
|
|
|
|
try:
|
|
|
|
validator.next()
|
|
|
|
except StopIteration:
|
|
|
|
if child.done():
|
|
|
|
self.__components.remove(child)
|
|
|
|
else:
|
|
|
|
new_validate.append((child, validator))
|
|
|
|
if not new_validate:
|
|
|
|
break
|
|
|
|
validate = new_validate
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
if not self.__components:
|
|
|
|
return
|
|
|
|
|
|
|
|
yield from_(super(Composite, self)._configure())
|
|
|
|
|
|
|
|
execute = [(c, c._executor()) for c in self.__components]
|
|
|
|
while True:
|
|
|
|
new_execute = []
|
|
|
|
for child, executor in execute:
|
|
|
|
try:
|
|
|
|
executor.next()
|
|
|
|
except StopIteration:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
new_execute.append((child, executor))
|
|
|
|
if not new_execute:
|
|
|
|
break
|
|
|
|
execute = new_execute
|
|
|
|
|
|
|
|
yield
|