Source code for armonic.client.smart

"""Smart module offers a high level way to call a provide. Function
:func:`smart_call` generates steps to help the user to

* define LifecycleManager,
* specialize xpath provide,
* specify variable value,
* ...

To use this module, you have to create a
:py:class:`armonic.client.smart.Provide`, and call
:py:func:`armonic.client.smart.smart_call`. In the following, the
classical code to use this library.

First, we define by inheritance global behavior of provides. In this
example, we want to 'manage' all provides::

    from armonic.client.smart import Provide, smart_call

    class MyProvide(Provide):
        def on_manage(self, data):
            return True


Then, we can build a provide from this classe and call smart_call on
it which returns a generator. We use this generator to walk on provides::

    my_provide = MyProvide("//a/xpath)
    generator = smart_call(my_provide)
    data = None
    while True:
        provide, step, args = generator.send(data)
        data = None

        if step == "manage":
            print "Provide %s is managed!" % provide.generic_xpath
        elif step == "specialize":
            # Do others stuffs on specialize step
            # ...
        elif step == ....


Some tips about how it works...
-------------------------------

About provide_ret validation:
Since provide_ret variables's values are known at runtime, we need to
do special thing to pass agent validation before deployement. Smart
ignore validation errors returned by agents if error occurs on
variables that belongs to provide_ret.
"""

# Limitiations
#
# It's not able to manage several require (nargs) variable.
# It's not able to manage path

import logging
import json

from armonic.client.utils import require_validation_error
import armonic.common

logger = logging.getLogger(__name__)

# The name of the special require name created the xapth that
# represents provide_ret value.
SPECIAL_REQUIRE_RETURN_NAME = "return"

# Describe the step that sent (through the generator) values used for
# the deployment
STEP_DEPLOYMENT_VALUES = "deployment_values"

STEPS = ["manage",
         "lfm",
         "specialize",
         "multiplicity",
         "validation",
         "call",
         "done"]


def generate_pre_post_steps(step):
    return ["pre_" + step, step, "post_" + step]
STEPS = reduce(lambda acc, steps: acc + steps,
               map(generate_pre_post_steps, STEPS),
               [])


class ValidationError(Exception):
    pass


class SmartException(Exception):

    @property
    def name(self):
        return self.__class__.__name__


class PathNotFound(SmartException):
    pass


class Variable(object):
    """
    :param from_require: The require that holds this variable.
    :param belongs_provide_ret: True if this variable belongs to the provide_ret variable list of the from_require.
    """

    def __init__(self, name, from_require, xpath, from_xpath, default, value, required, type, error, belongs_provide_ret, modifier, extra):
        self.from_require = from_require
        self.name = name
        self.xpath = xpath
        self.from_xpath = from_xpath
        self._default = default
        self._value = value
        self.required = required
        self.type = type
        self.error = error
        self.extra = extra
        self.modifier = modifier
        self._is_skel = True

        self.belongs_provide_ret = belongs_provide_ret

        # Capture the xpath that provides the value for this
        # variable. The format of this XPath is
        # <location>/<lifecycle>/<state>/<provide>/return/variable_name
        #
        # If its value is None, that means the provide is not managed
        # by smart and the value has to be manually provided.
        #
        # You should use the property provided_by_xpath in order to auto
        # update this field.
        self._provided_by_xpath = None

        # Capture the variable used to resolve self
        self._resolved_by = None
        self._set_by = None
        self._suggested_by = None

        # Used as a guard to break cycles during variable resolution
        self._resolving = False

        # Used for debugging.
        self._has_value = False

        logger.debug("Created variable @%s %s" % (id(self), self.xpath))

    def copy(self, from_require):
        var = Variable(
            name=self.name,
            from_require=from_require,
            xpath=self.xpath,
            from_xpath=self.from_xpath,
            default=self._default,
            value=self._value,
            required=self.required,
            type=self.type,
            error=self.error,
            belongs_provide_ret=self.belongs_provide_ret,
            modifier=self.modifier,
            extra=self.extra)

        var._is_skel = False

        # All variable are added to a global list
        self.from_require.from_provide.Variables.append(var)

        return var

    @classmethod
    def from_json(cls, dct_json, from_require, belongs_provide_ret=False):
        """
        :param dct_json: a dict that contains values from agent
        :param from_require: the require that declares this variables
        :param belongs_provide_ret: True if this variable belongs to the provide_ret variable list of the from_require
        """
        this = cls(dct_json['name'],
                   from_require=from_require,
                   xpath=dct_json['xpath'],
                   from_xpath=dct_json['from_xpath'],
                   default=dct_json['default'],
                   value=None,
                   required=dct_json['required'],
                   type=dct_json['type'],
                   error=dct_json['error'],
                   belongs_provide_ret=belongs_provide_ret,
                   modifier=dct_json['modifier'],
                   extra=dct_json['extra'])
        return this

    def update_from_json(self, dct_json):
        for key, value in dct_json.items():
            # We don't update the value from the json generated by
            # agent. Indeed, the agent doen't have to change this
            # value. In fact, we don't do this because the json
            # variable specification is not sufficient to manage
            # multiplicity of variables. See commit message also.
            if key not in "value":
                try:
                    setattr(self, key, value)
                except AttributeError:
                    logger.error("Error: Failed to update attr %s to %s" % (key, value))

    @property
    def provided_by_xpath(self):
        """If this variable belongs to a provide_ret, return None if the
        Provide that has to provide the value is not managed by
        smart. In this case, user will have to fill it manually.
        """
        if (self.belongs_provide_ret and
                self.from_require.provide is not None and
                self.from_require.provide.manage):
            return "/".join([
                self.from_require.provide._node_id.to_str(),
                self.from_require.provide.xpath,
                SPECIAL_REQUIRE_RETURN_NAME,
                self.name])
        else:
            return None

    @property
    def default(self):
        return self._default

    @default.setter
    def default(self, default):
        self._default = default

    @property
    def default_resolved(self):
        """Returns the value resolved."""
        return self._resolved_break_cycles(
            lambda a: a._default,
            lambda a: a.default_resolved)

    def _apply_modifier(self, value):
        """If self.modifier is not None, we currently apply it only on
        VString."""
        if value is not None and self.modifier is not None:
            if self.type == 'str':
                return self.modifier % value
        return value

    @property
    def value(self):
        v = self.value_resolved
        if self._has_value is False and v is not None:
            self._has_value = True
            logger.debug("Variable (@%s) %s gets the value %s" % ((id(self)), self.xpath, v))
        return self._apply_modifier(v)

    @value.setter
    def value(self, value):
        self._value = value

    def value_get_one(self):
        """Try to get a value from default, or resolved."""
        return (self.value_resolved or
                self.value or
                self.default_resolved or
                self.default)

    @property
    def value_resolved(self):
        """Returns the value resolved."""
        return self._apply_modifier(
            self._resolved_break_cycles(
                lambda a: a._value,
                lambda a: a.value_resolved))

    def _resolved_break_cycles(self, f_value, f_resolved):
        """Returns the value resolved and breaks potential cycles.
        :param f_value: is a function returing the local value.
        :param f_resolved: is a function returing the value resolved by other variables.
        """
        if self._resolving:
            # If this variable resolution is started, we stop the
            # recursion here without returning any value.
            # The self value can be use at the first iteration.
            return None
        else:
            self._resolving = True
            self._bind()
            resolved = self._resolve()
            if resolved is self:
                value = f_value(self)
            else:
                value = f_resolved(resolved)
                # If the resolved value is None, we try the self value
                # which may be not None!
                if value is None:
                    value = f_value(self)

            self._resolving = False
            return value

    def _resolve(self):
        """When bindings have been created, this method can be used to get a
        bound variable. First, we try to get the variable set_by, then
        suggested_by, then resolved_by and finally, we use self."""
        if self._set_by is not None:
            return self._set_by
        elif self._suggested_by is not None:
            return self._suggested_by
        elif self._resolved_by is not None:
            return self._resolved_by
        else:
            return self

    def _bind(self):
        """Try to bind this variable to another one by assigning attributes
        _resolved_by, _set_by or _suggested_by.

        If from_xpath is not None, it tries to to find back the
        corresponding variable. Otherwise, it tries to find a value in
        the scope.

        'host' variables are particular cases.
        """
        scope = self.from_require._scope_variables

        if self.type == 'armonic_this_host' and self.from_require.from_provide is not None:
            self._value = self.from_require.from_provide.host
            return

        # Variables type armonic_hosts is a special kind of variable. To
        # fill it, we accumulate host value of all brothers of this provide.
        if self.type == 'armonic_hosts' and self.from_require.from_provide.require:
            self._value = [r.provide.host for r in self.from_require.from_provide.require._from_requires]

        if self.type == 'armonic_host' and self._value is None:
            if self.from_require.type == 'external' and self.from_require.provide:
                self._value = self.from_require.provide.host
                # FIXME: We have a problem because host doesn't come from a variable!
                return
            # Auto-fill the value if ArmonicHost specified in a Require
            if self.from_require.type == 'simple' and self.from_require.from_provide:
                self._value = self.from_require.from_provide.host
                return

        # armonic_host variables must not be resolved.
        if self.type == 'armonic_host':
            return

        # If the variable has a from_xpath attribute,
        # try to find back its value
        if self.from_xpath is not None:
            for v in self.from_require.from_provide.Variables:
                if v.xpath == self.from_xpath:
                    self._set_by = v
                    logger.debug("Variable [%s] value comes from [%s] (@%s) with value %s" % (
                        self.xpath, v.xpath, id(v), v._value))
                    return
            logger.info("Variable [%s] from_xpath [%s] not found" % (
                self.xpath, self.from_xpath))

        if self.from_require.special and not self.name == 'host':
            return

        if self.from_xpath is None:
            for v in scope:
                if self.name == v.name and self is not v:
                    logger.trace("Variable [%s] is suggested by [%s] with value %s" % (
                        self.xpath, v.xpath, v._value))
                    logger.trace("Variable [%s] is resolved by [%s] with value %s" % (
                        v.xpath, self.xpath, v._value))
                    self._suggested_by = v
                    v._resolved_by = self

    def pprint(self):
        return {"name": self.name,
                "xpath": self.xpath,
                "default": self._default,
                "value": self.value,
                "error": self.error}

    def __repr__(self):
        return "Variable(skel=%s, %s, name=%s, xpath=%s, value=%s, error=%s)" % (
            self._is_skel,
            id(self),
            self.name,
            self.xpath,
            self._value,
            self.error)


class Requires(list):
    """This class is used to represent the require multiplicity."""
    def __init__(self, skel):
        self.skel = skel

    def get_new_require(self):
        new = self.skel.copy()
        new._from_requires = self
        try:
            new.child_num = self[-1].child_num + 1
        except IndexError:
            pass
        new.multiplicity_num = len(self)
        self.append(new)
        return new

    def variables_serialized(self):
        dct = {}
        for (i, r) in enumerate(self):
            for v in r.variables():
                if v.xpath not in dct:
                    dct[v.xpath] = {i: v.value}
                else:
                    dct[v.xpath].update({i: v.value})

        acc = []
        for (k, v) in dct.items():
            acc.append((k, v))
        return acc

    def variables(self):
        acc = []
        for r in self:
            acc += r.variables()
        return acc


class Require(object):
    """
    :param from_provide: The provide that holds this require.

    """

    def __init__(self, from_provide, special, child_num):
        self._is_skel = True

        self.child_num = child_num

        # Since the same require can be requried several times (via
        # multiplicity), this attribute describes the index of this
        # require amongst all same require.
        self.multiplicity_num = 0

        self.from_provide = from_provide
        self._from_requires = None
        # If this require comes from a special provide, ie. entry,
        # leave or cross. This is used to avoir variable value
        # propagation to require that comes from states.
        self.special = special
        self._scope_variables = []

        # We copy variables dict from parent the scope.
        # They will be upgraded when requires are built.
        if from_provide.require is not None:
            for v in from_provide.require._scope_variables:
                self._scope_variables.append(v)

    @classmethod
    def from_json(cls, dct_json, **kwargs):
        this = cls(**kwargs)
        this.xpath = dct_json['xpath']
        this.type = dct_json['type']
        this.name = dct_json['name']

        this._variables = []
        for v in dct_json['variables_skel']:
            this._variables.append(Variable.from_json(v, from_require=this))

        this.json = dct_json
        return this

    def copy(self):
        new = Require(from_provide=self.from_provide,
                      special=self.special,
                      child_num=self.child_num)
        # To know if this instance is a skeleton or not.
        new._is_skel = False

        new.xpath = self.xpath
        new.type = self.type
        new.name = self.name

        new._variables = []
        for v in self._variables:
            new._variables.append(v.copy(new))

        new.json = self.json
        return new

    def pprint(self):
        return {"xpath": self.xpath,
                "variables": [v.pprint() for v in self._variables]}

    def variables_serialized(self):
        """Get variables in the format for provide_call"""
        acc = []
        for v in self._variables:
            acc.append((v.xpath, {0: v.value}))
        return acc

    def variables(self):
        """:rtype: [:class:`Variable`]"""
        return self._variables


class Remote(Require):
    def __init__(self, from_provide, special, child_num):
        Require.__init__(self, from_provide, special, child_num)
        self.provide = None

    def copy(self):
        new = Remote(from_provide=self.from_provide,
                     special=self.special,
                     child_num=self.child_num)
        # To know if this instance is a skeleton or not.
        new._is_skel = False

        new.xpath = self.xpath
        new.type = self.type
        new.name = self.name
        new.nargs = self.nargs
        new.provide_xpath = self.provide_xpath

        new.provide_args = []
        for v in self.provide_args:
            new_variable = v.copy(new)
            new.provide_args.append(new_variable)
            new._scope_variables.append(new_variable)

        new.provide_ret = []
        for v in self.provide_ret:
            new_variable = v.copy(new)
            new.provide_ret.append(new_variable)
            new._scope_variables.append(new_variable)

        new.json = self.json
        return new

    @classmethod
    def from_json(cls, dct_json, **kwargs):
        this = cls(**kwargs)
        this.xpath = dct_json['xpath']
        this.type = dct_json['type']
        this.name = dct_json['name']

        this.nargs = dct_json['nargs']
        this.provide_xpath = dct_json['provide_xpath']
        this.provide_args = []

        for v in dct_json['provide_args']:
            var = Variable.from_json(v, from_require=this)
            this.provide_args.append(var)

            # This variable is added to the scope.
            # This should useless since it is the skeleton
            # this._scope_variables.append(var)

        # Here, we add provide ret variable.
        this.provide_ret = []
        for v in dct_json['provide_ret']:
            var = Variable.from_json(v, from_require=this,
                                     belongs_provide_ret=True)
            this.provide_ret.append(var)

            # This variable is added to the scope.
            # this._scope_variables.append(var)

        this.json = dct_json
        return this

    def pprint(self):
        return {"xpath": self.xpath,
                "variables": [v.pprint() for v in self.provide_args]}

    def variables_serialized(self):
        """Get variables in the format for provide_call"""
        acc = []
        for v in (self.provide_args + self.provide_ret):
            acc.append((v.xpath, {0: v.value}))
        return acc

    def variables(self):
        """:rtype: [:class:`Variable`]"""
        acc = [v for v in self.provide_args]
        for v in self.provide_ret:
            acc.append(v)
            if v.provided_by_xpath is None:
                pass
            else:
                logger.debug("Variable %s will be provided_by_xpath by %s" % (
                    v.xpath, v.provided_by_xpath))
        return acc

    def update_provide_ret(self, provide_ret):
        for (name, value) in provide_ret.items():
            for v in self.provide_ret:
                if v.name == name:
                    v._value = value
                    logger.debug("Variable %s has been updated with value "
                                 "'%s' from provide_ret" % (v.xpath, value))
                    break


class ArmonicProvide(object):

    def __init__(self):
        self.xpath = None
        self.name = ""
        self.extra = {}

    def _build_provide(self, provide_xpath_uri):
        provides = self.lfm.provide(provide_xpath_uri)
        # FIXME: This should not happen.  But this happens if a
        # provide from a replay file is used and the module is not
        # loaded for instance.
        if len(provides) < 1:
            msg = "The XPath used to build the provide doesn't match anything."
            logger.error(msg)
            raise Exception(msg)
        self.update_from_json(provides[0])

    def update_from_json(self, dct_json):
        self.name = dct_json['name']
        self.xpath = dct_json['xpath']
        self.extra = dct_json.get('extra', {})

    def ignore_error_on_variable(self, variable):
        """Can be overlapped to ignore validation error on some variables."""
        return False


class NodeId(object):
    def __init__(self, node_id):
        self._node_id = node_id

        # Old node id can come from a deployment info file
        self._old_node_id = None

    def __repr__(self):
        return "node_" + "_".join([str(n) for n in self._node_id])

    def to_str(self):
        return "node_" + "_".join([str(n) for n in self._node_id])

    def old_is_set(self):
        return self._old_node_id is not None

    def old_to_str(self):
        return str(self._old_node_id)


[docs]class Provide(ArmonicProvide): """This class describe a provide and its requires and remotes requires contains provide. Thus, this object can describe a tree. To build the tree, the function :func:`smart_call` must be used. To adapt the behavior of this class, redefine methods on_step and do_step, where step is manage, lfm, specialize, etc. If method do_step returns True, this step is 'yielded'. Method on_step takes as input the sent data. :param child_number: if this Provide is a dependencies, this is the number of this child. :param requirer: the provide that need this require :param require: the remote require of the requirer that leads to this provide. """ # Contains all variables. This is used to find back from_xpath value. Variables = [] require = None """Contains the :class:`Require` that requires this provide.""" requirer = None """Contains the :class:`Provide` that requires this current provide.""" def __init__(self, generic_xpath, requirer=None, child_num=None, require=None): ArmonicProvide.__init__(self) self.generic_xpath = generic_xpath # the provide that need this require self.requirer = requirer # the remote require of the requirer that leads # to this provide. self.require = require # This dict contains variables that belongs to this scope. self._scope_variables = {} # If this provide is the Root provide # We initialize depth and tree_id # # NOTE: depth could be deduce from tree_id: depth = len(tree_id) if not self.has_requirer(): self.depth = 0 self.tree_id = [0] else: self.depth = requirer.depth + 1 self.tree_id = requirer.tree_id + [child_num] # This will replace tree_id. self._node_id = NodeId(self.tree_id) # self.ignore = False self._step_current = 0 self._current_requires = None self._children_generator = None # Contain all requires. A require can be several time in this # list due to multiplicity. self._requires = None # Provide configuration variables. # # If this provide comes from a local require, the lfm is taken # from the requirer. self.lfm = None # the host contains the adress (IP or DNS) used to contact the # service. self._host = None # lfm_host contain the adress used to contact this agent self.lfm_host = None self.is_local = False if (require is not None and require.type == "local"): self.lfm = requirer.lfm self.host = requirer.host self.lfm_host = requirer.lfm_host self.is_local = True self.is_external = False if (require is not None and require.type == "external"): self.is_external = True # consider the root_provide like an external require if requirer is None: self.is_external = True self.manage = True self.call = None # True when all variables are validated self.is_validated = False def __repr__(self): return "<Provide(%s)>" % self.generic_xpath @property def host(self): # When filling ArmonicHosts we need the host # value of each Provide but each Provide might # not have any lfm setup yet. # # FIXME: We should introduce attribute lfm_data which is would # contain the data created at step lfm and used by on_lfm # method. Currently, we are supposing in smart that lfm_host # is created at step lfm. However, smart should not do this # kind of assumptions. # if self._host is None and self.lfm_host: self.on_lfm(self.lfm_host) return self._host @host.setter def host(self, value): self._host = value
[docs] def variables_serialized(self): """Get variables in the format for provide_call""" acc = [] for r in self.remotes + self.requires: acc += r.variables_serialized() return (acc, {'source': None, 'uuid': None})
[docs] def variables(self): """:rtype: [:class:`Variable`]""" acc = [] for v in self.remotes + self.requires: acc += v.variables() return acc
[docs] def variables_scope(self): """Return the variable scope of this provide. :rtype: [:class:`Variable`] """ if self.require is not None: return self.require._scope_variables return []
[docs] def validate(self, values, static=False): """Validate all variables using values from data. Moreover, variable value is set with values coming from data. The static validation is used to validate variables before deployment is running. In this case, we don't handle error on provide_ret's variables since we don't know value returned by porvide calls. :param static: If True, run a static validation. :rtype: bool """ # Update scope variables with values for variable in self.variables(): idx = variable.from_require.multiplicity_num for variable_xpath, variable_values in values: if variable.xpath == variable_xpath: # FIXME - can have multiple values try: variable.value = variable_values[idx] except KeyError: # FIXME web interface send string indexes try: variable.value = variable_values[str(idx)] except KeyError: variable.value = None logger.debug("Updating from user specified value %s=%s" % (variable_xpath, variable.value)) values = (values, {'source': None, 'uuid': None}) result = self.lfm.provide_call_validate(self.xpath, self.variables_serialized()) errors = False json_variables = [] for require in result['requires']: for r in require['requires']: # FIXME: handle nargs if len(r['variables']) > 0: for v in r['variables'][0]: json_variables.append(v) for variable in self.variables(): for json_variable in json_variables: if variable.xpath == json_variable['xpath']: # If a static validation is asked, we don't # consider the error if the variable belongs to # the provide_ret of the require. if json_variable['error'] is not None: if (variable.belongs_provide_ret and static): pass elif self.ignore_error_on_variable(variable): logger.info("Ignoring error of variable %s (due to provide.ignore_error_on_variable())" % variable.xpath) else: errors = True variable.update_from_json(json_variable) if variable.error: if armonic.common.SIMULATION: logger.debug("Variable %s has error: %s" % (variable.xpath, variable.error)) else: logger.error("Variable %s has error: %s" % (variable.xpath, variable.error)) self.is_validated = not errors return self.is_validated
[docs] def has_requirer(self): """To know if it is the root provide.""" return self.requirer is not None
@property def step(self): return STEPS[self._step_current] def _next_step(self): if self._step_current + 1 > len(STEPS) - 1: raise IndexError self._step_current += 1 def _previous_step(self): try: if STEPS[self._step_current - 1]: self._step_current -= 1 except IndexError: pass def _build_require_from_call_require(self, dct_json): """From a json dict, build Require and Remote require.""" self.remotes = [] self.requires = [] idx = 0 # Here, a not really clean hack to order requires. # We begin with 'remote' requires because to give to them # first child indexes. for p in dct_json: special = p['name'] in ['enter', 'leave', 'cross'] for require in p['requires']: if require['type'] in ['external', 'local']: self.remotes.append(Requires(Remote.from_json( require, special=special, child_num=idx, from_provide=self))) idx += 1 # Then, we give last indexes to simple requires. for p in dct_json: special = p['name'] in ['enter', 'leave', 'cross'] for require in p['requires']: if require['type'] in ['simple']: requires = Requires(Require.from_json( require, special=special, child_num=idx, from_provide=self)) requires.get_new_require() self.requires.append(requires) idx += 1 def _build_requires(self): """Get all requires""" provides = self.lfm.provide_call_requires(self.xpath) self._build_require_from_call_require(provides) def _requirator(self): """Be careful, this function always returns the same generator.""" def c(): for r in self.remotes: yield r if self._children_generator is None: self._children_generator = c() return self._children_generator
[docs] def build_child(self, generic_xpath, child_num, require): """Build and return a new provide by using the same class. """ ret = self.__class__(generic_xpath, requirer=self, child_num=child_num, require=require) return ret
[docs] def do_lfm(self): """The step lfm is applied if it returns True. Currently, do_lfm is already called, even if the provide is local. We may only call it when the provide is external """ # If lfm_host is set at the multiplicity step # we can create the lfm automatically if self.lfm_host: self.on_lfm(self.lfm_host) return self.lfm is None
def on_lfm(self, lfm): self.lfm = lfm def _test_lfm(self): """Verify that lfm and lfm_host attributes are set.""" if self.lfm is None: raise AttributeError("'lfm' attribute must not be None. Must be set at 'lfm' step") if self.lfm_host is None: raise AttributeError("'lfm_host' attribute must not be None. Must be set at 'lfm' step")
[docs] def reset_lfm(self): """ Reset all data set at the lfm step """ self.lfm = self.lfm_host = self.host = None
def do_call(self): return self.call is None
[docs] def on_call(self, call): """ :type call: boolean """ self.call = call
def _test_call(self): """ :type call: boolean """ if self.call is None: raise AttributeError("'call' attribute must not be None. Must be set at 'call' step") def do_multiplicity(self): return True
[docs] def on_multiplicity(self, requires, data): """Can be overload to adapt behavior of multiplicity step. This method must return either a number or a list. This is different than others steps because we can not bind the multiplicity value to the provide object since each require have its own multiplicity. Moreover, on_multiplicity is always called even if do_multiplicity returns False. :type requires: Requires """ return data
def do_manage(self): return True def on_manage(self, data): self.manage = data def _test_manage(self): if self.manage is None: raise AttributeError("'manage' attribute must not be None. Must be set at 'manage' step")
[docs] def matches(self): """Return the list of provides that matched the generic_xpath""" return self.lfm.provide(self.generic_xpath)
[docs] def on_specialize(self, xpath): """Actions after the provide has been specialized.""" pass
[docs] def do_specialize(self): """Specialization can not be avoided. If the provide matches only 1 xpath, yield doesn't occurs if this method returns False. Thus, by returning True, specialization always yields. """ return False
def do_validation(self): return True
[docs] def update_scope_provide_ret(self, provide_ret): """When the provide call returns value, we habve to update the scope of the require in order to be able to use these value to fill depending provides. """ # A provide should ALWAYS return a dict. if type(self.provide_ret) is dict: # FIXME if self.has_requirer(): self.require.update_provide_ret(self.provide_ret)
def lfm_call(self): if not armonic.common.DONT_VALIDATE_ON_CALL: # FIXME. This is a temporary hack! ret = self.lfm.provide_call_validate( provide_xpath_uri=self.xpath, requires=self.variables_serialized()) if ret['errors']: logger.error("Following variables have not been validated:") for v in require_validation_error(ret): logger.error("\t%s" % str(v)) ValidationError("Some variables have not been validated before provide_call!") self.provide_ret = self.lfm.provide_call( provide_xpath_uri=self.xpath, requires=self.variables_serialized()) if type(self.provide_ret) is not dict: logger.debug("Provide '%s' return type is %s (instead a dict)!" % (self.xpath, type(self.provide_ret))) else: logger.info("Provide '%s' returns:" % self.xpath) for k, v in self.provide_ret.items(): logger.info("- %s : %s" % (k, json.dumps(v))) self.update_scope_provide_ret(self.provide_ret) # self.provide_ret = self.lfm.call("provide_call_validate", # provide_xpath_uri=self.xpath, # requires=self.variables_serialized()) # from pprint import pprint # pprint(self.provide_ret) return self.provide_ret
class XpathNotFound(Exception): pass class Deployment(object): """ To create a replay file. The 'mapping' section store relationship between old and new node id. """ def __init__(self, scope, sections): # Variable are splitted into input and output because we don't try # to update input file to generate the output one. We regenerate # the output file each time smart is called. # This simplifies the process of node_id mapping if node_id have changed. self._manage_input = [] self._lfm_input = [] self._specialize_input = [] self._multiplicity_input = [] self._variables_input = [] self._manage_output = [] self._lfm_output = [] self._specialize_output = [] self._multiplicity_output = [] self._variables_output = [] # Contains variable that belongs to provide_ret require part self._variables_output_provide_ret = [] # Contains variable which type is armonic_host or armonic_hosts. # They can need special translation at deployement time. self._variables_output_host = [] self._mapping_output = [] for section_name, section in sections.items(): try: for key, value in section: getattr(self, "_" + section_name + "_input").append( (key, {"value": value}) ) except AttributeError: pass self.scope = scope def _get_value(self, section, node_id, xpath, consume=False): def _consume_value(infos): # This function return the value from infos and set used # flags to true if consume flag is set. # # If a vairalbe is asked, it's more complicated to set the # consume flag since a variable can occur several time. We # then also consume the dict of variables. if section == "_variables_input": values = infos['value'] idx = min(values) value = values[idx] if consume: value = values.pop(idx) infos['value'] = values if len(values) == 0: infos["used"] = True else: value = infos['value'] if consume: infos['used'] = True return value for (key, infos) in getattr(self, section): key_node_id, key_xpath = self._xpath_host(key) if xpath == key_xpath: if infos.get("used", False): continue # Section and xpath part have matched. # # Next, to get a value, several cases can occur. If # the node_id from input file matches the node_id of # the current scope, we simply consume the value. # # If node_id don't match, we assign the node_id to the # old_node_id attribute of the current scope node_id # and we consume the value. # # If it doesn't match the node_id, we use old_node_id # if it is set. if node_id.to_str() == key_node_id: return _consume_value(infos) elif node_id.old_is_set() and node_id.old_to_str() == key_node_id: return _consume_value(infos) elif node_id.old_is_set() is False: logger.debug("Use old node id: '%s' (instead of '%s')", key_node_id, node_id.to_str()) node_id._old_node_id = key_node_id # We create the mapping table between old and new node_id self._mapping_output.append((key_node_id, node_id.to_str())) return _consume_value(infos) if node_id.old_is_set(): msg = ("%s/%s or %s/%s not found in section %s" % (node_id.to_str(), xpath, node_id.old_to_str(), xpath, section)) else: msg = ("%s/%s not found in section %s" % (node_id.to_str(), xpath, section)) logger.debug(msg) raise XpathNotFound(msg) def _has_value(self, section, node_id, search_key): try: self._get_value(section, node_id, search_key) return True except XpathNotFound: return False def _get(self, section, node_id, key): try: return self._get_value(section, node_id, key, consume=True) except XpathNotFound: return None def _xpath_host(self, xpath): node_id = xpath.split('/')[0] path = "/".join(xpath.split('/')[1:]) return (node_id, path) @property def _generic_xpath(self): return self.scope._node_id.to_str() + '/' + self.scope.generic_xpath @property def _xpath(self): return self.scope.lfm_host + '/' + self.scope.xpath @property def manage(self): return self._get("_manage_input", self.scope._node_id, self.scope.generic_xpath) @manage.setter def manage(self, value): self._manage_output.append(( self._generic_xpath, {"value": value, "used": True}) ) @property def lfm(self): return self._get("_lfm_input", self.scope._node_id, self.scope.generic_xpath) @lfm.setter def lfm(self, value): self._lfm_output.append(( self._generic_xpath, {"value": value, "used": True}) ) @property def specialize(self): specialized = self._get("_specialize_input", self.scope._node_id, self.scope.generic_xpath) if specialized is not None: return self._xpath_host(specialized) return (None, None) @specialize.setter def specialize(self, value): self._specialize_output.append(( self._generic_xpath, {"value": self.scope._node_id.to_str() + '/' + value, "used": True}) ) def multiplicity(self, require_xpath): return self._get("_multiplicity_input", self.scope._node_id, require_xpath) def multiplicity_setter(self, require_xpath, hosts): self._multiplicity_output.append(( self.scope._node_id.to_str() + "/" + require_xpath, {"value": hosts}) ) def get_variable(self, xpath): variable_value = self._get("_variables_input", self.scope._node_id, xpath) if type(variable_value) == dict: if len(variable_value) > 1: return [value for index, value in variable_value.items()] else: return variable_value.itervalues().next() return variable_value def set_variables(self, variables): # Add a variable and its value to the variable_list. If the # variable already exists in the list, its value is added to # the value dict. def add_variable(variable_list, variable): variable_name = self.scope._node_id.to_str() + '/' + variable.xpath for v in variable_list: if variable_name == v[0]: v[1]['value'][len(v[1]['value'])] = variable.value return variable_list.append((variable_name, {"value": {0: variable.value}, "used": True})) if variables is None: return for v in variables: if v.belongs_provide_ret: if not self._has_value("_variables_output_provide_ret", self.scope._node_id, v.xpath): self._variables_output_provide_ret.append(( self.scope._node_id.to_str() + '/' + v.xpath, {"value": v.provided_by_xpath, "used": True})) elif v.type in ['armonic_host', 'host', 'armonic_this_host', 'armonic_hosts']: add_variable(self._variables_output_host, v) else: if not self._has_value("_variables_output", self.scope._node_id, v.xpath): add_variable(self._variables_output, v) def to_primitive(self): return { "manage": [(k, i["value"]) for k, i in self._manage_output], "lfm": [(k, i["value"]) for k, i in self._lfm_output], "specialize": [(k, i["value"]) for k, i in self._specialize_output], "multiplicity": [(k, i["value"]) for k, i in self._multiplicity_output], "variables": [(k, i["value"]) for k, i in self._variables_output], "provide_ret": [(k, i["value"]) for k, i in self._variables_output_provide_ret], "variables_host": [(k, i["value"]) for k, i in self._variables_output_host], "mapping": self._mapping_output }
[docs]def smart_call(root_provide, values={}): """Generator which 'yields' a 3-uple (provide, step, optionnal_args).""" # We clear all variables used for a deployment Provide.Variables = [] scope = root_provide deployment = Deployment(scope, values) # The provide call return value. ret = None logger.info("Smart is using prefilled values: %s" % deployment.to_primitive()) while True: logger.debug("Step: %s - %s" % (scope.step, scope)) # Stop and Pop conditions if scope.step == "done": yield (scope, scope.step, None) if scope.step == "done" or not scope.manage: # If all dependencies of root node have been threated we # break the loop if not scope.has_requirer(): break # If all dependencies have been threated we # go back to its requirer. else: scope = scope.requirer deployment.scope = scope continue if scope.manage: # post_/pre_ step handle if scope.step.startswith(('post_', 'pre_')): # check do_post_step or do_pre_step do_step = getattr(scope, 'do_' + scope.step, None) if do_step is not None and do_step() is True: data = yield(scope, scope.step, None) # run on_step on_step = getattr(scope, 'on_' + scope.step, None) if on_step is not None: on_step(data) scope._next_step() elif scope.step == "manage": if scope.do_manage(): data = deployment.manage if data is not None: if data: logger.debug("%s is managed from deployment data" % scope.generic_xpath) else: logger.debug("%s is NOT managed from deployment data" % scope.generic_xpath) else: data = yield(scope, scope.step, None) deployment.manage = data scope.on_manage(data) scope._test_manage() scope._next_step() elif scope.step == "lfm": host = deployment.lfm if scope.do_lfm(): if host is not None: data = host logger.debug("%s lfm on %s from deployment data" % (scope.generic_xpath, data)) else: data = yield(scope, scope.step, None) scope.on_lfm(data) scope._test_lfm() deployment.lfm = scope.lfm_host scope._next_step() elif scope.step == "specialize": m = scope.matches() logger.debug("Specialize matches: %s" % [p['xpath'] for p in m]) host, xpath = deployment.specialize def specialize(specialized): deployment.specialize = specialized scope.on_specialize(specialized) if scope.manage: scope._build_provide(specialized) scope._build_requires() scope._next_step() if xpath is not None: specialized = xpath logger.info("Replay specializes %s with %s" % (scope.generic_xpath, specialized)) specialize(specialized) elif len(m) > 1 or scope.do_specialize(): specialized = yield(scope, scope.step, m) specialize(specialized) elif len(m) == 1: specialized = m[0]['xpath'] specialize(specialized) else: os_type = scope.lfm.info()['os-type'] os_release = scope.lfm.info()['os-release'] # Go back to the lfm step if specialize doesn't match anything scope._previous_step() # Reset the lfm since we need to choose another one scope.reset_lfm() yield (scope, scope.step, PathNotFound('No path to %s found on %s (%s %s)' % ( scope.generic_xpath, scope.lfm_host, os_type, os_release))) elif scope.step == "multiplicity": # If no requires are currently managed, we will try to # find one (via scope._requirator()). If we are not # able to find one, then this step is done and we go # to the next step. if scope._current_requires is None: try: # We are trying to get a next Requires req = scope._requirator().next() if req.skel.nargs == "*": multiplicity = deployment.multiplicity(req.skel.xpath) if multiplicity is not None: logger.info("Replay sets multiplicity of '%s' to:" % scope.generic_xpath) for m in multiplicity: logger.info("\t%s" % m) if multiplicity is None: if scope.do_multiplicity(): multiplicity = yield (scope, scope.step, req) multiplicity = scope.on_multiplicity(req, multiplicity) if req.skel.type == 'external': if type(multiplicity) is not list: raise TypeError("Multiplicity step for external requires must send a list!") number = len(multiplicity) if type(number) is not int: raise TypeError("Multiplicity step must send a integer!") for i in range(0, number): # We build a new Require object from # the skeleton new = req.get_new_require() # We create a new provide child to the # current provide and attach the # require to this provide. p = scope.build_child( generic_xpath=new.provide_xpath, child_num=new.child_num, require=new) new.provide = p if req.skel.type == 'external': new.provide.lfm_host = multiplicity[i] deployment.multiplicity_setter(req.skel.xpath, multiplicity) else: new = req.get_new_require() p = scope.build_child( generic_xpath=new.provide_xpath, child_num=new.child_num, require=new) new.provide = p scope._current_requires = req except StopIteration: # If all requires have been treated, the # manage_dependencies step is done if scope._current_requires is None: scope._next_step() # If a requires is currently managed, we have to # process all provide attached to this Requires since # it can have a multiplicity greather than 1. # # We scan this requires to find the next non processed # one. If all provides have been processed, then we # set the current_requires to None in order to lookup # for the next Requires (at next main loop iteration) else: done = True for r in scope._current_requires: if r.provide.manage is True and not r.provide.step == "done": done = False scope = r.provide deployment.scope = scope break if done: scope._current_requires = None elif scope.step == "validation": if scope.do_validation() and not scope.is_validated: # Fill variables with replay file values for variable in scope.variables(): variable_value = deployment.get_variable(variable.xpath) if variable_value is not None: variable.value = variable_value logger.debug("Filling '%s' with value '%s' from deployment data" % (variable.xpath, variable_value)) data = yield(scope, scope.step, None) if scope.validate(data, static=armonic.common.SIMULATION): # Record variables values deployment.set_variables(scope.variables()) scope._next_step() else: scope._next_step() elif scope.step == "call": if scope.do_call(): data = yield(scope, scope.step, None) scope.on_call(data) if scope.call: scope.lfm_call() if scope.provide_ret is not None: yield (scope, scope.step, scope.provide_ret) scope._next_step() else: yield (scope, scope.step, None) scope._next_step() yield (None, STEP_DEPLOYMENT_VALUES, deployment.to_primitive()) return