diff options
Diffstat (limited to 'azure/aria/aria-extension-cloudify/src/aria/aria/modeling')
14 files changed, 6968 insertions, 0 deletions
diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/__init__.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/__init__.py new file mode 100644 index 0000000..57bc188 --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/__init__.py @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This package provides an API for modeling ARIA's state and serializing it to storage. +""" + +from collections import namedtuple + +from . import ( + mixins, + types, + models, + utils, + service_template as _service_template_bases, + service_instance as _service_instance_bases, + service_changes as _service_changes_bases, + service_common as _service_common_bases, + orchestration as _orchestration_bases +) + + +_ModelBasesCls = namedtuple('ModelBase', 'service_template,' + 'service_instance,' + 'service_changes,' + 'service_common,' + 'orchestration') + +model_bases = _ModelBasesCls(service_template=_service_template_bases, + service_instance=_service_instance_bases, + service_changes=_service_changes_bases, + service_common=_service_common_bases, + orchestration=_orchestration_bases) + + +__all__ = ( + 'mixins', + 'types', + 'models', + 'model_bases', + 'utils' +) diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/constraints.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/constraints.py new file mode 100644 index 0000000..8ed33d5 --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/constraints.py @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Constraints for the requirements-and-capabilities matching mechanism. +""" + +class NodeTemplateConstraint(object): + """ + Used to constrain requirements for node templates. + + Must be serializable. + """ + + def matches(self, source_node_template, target_node_template): + """ + Returns ``True`` if the target matches the constraint for the source. + """ + raise NotImplementedError diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/exceptions.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/exceptions.py new file mode 100644 index 0000000..cddc049 --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/exceptions.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Modeling exceptions. +""" + +from ..exceptions import AriaException + + +class ModelingException(AriaException): + """ + ARIA modeling exception. + """ + + +class ParameterException(ModelingException): + """ + ARIA parameter exception. + """ + pass + + +class ValueFormatException(ModelingException): + """ + ARIA modeling exception: the value is in the wrong format. + """ + + +class CannotEvaluateFunctionException(ModelingException): + """ + ARIA modeling exception: cannot evaluate the function at this time. + """ + + +class MissingRequiredInputsException(ParameterException): + """ + ARIA modeling exception: Required parameters have been omitted. + """ + + +class ParametersOfWrongTypeException(ParameterException): + """ + ARIA modeling exception: Parameters of the wrong types have been provided. + """ + + +class UndeclaredInputsException(ParameterException): + """ + ARIA modeling exception: Undeclared parameters have been provided. + """ diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/functions.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/functions.py new file mode 100644 index 0000000..554bbfb --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/functions.py @@ -0,0 +1,140 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Mechanism for evaluating intrinsic functions. +""" +from ..parser.exceptions import InvalidValueError +from ..parser.consumption import ConsumptionContext +from ..utils.collections import OrderedDict +from ..utils.type import full_type_name +from . import exceptions + + +class Function(object): + """ + Base class for intrinsic functions. Serves as a placeholder for a value that should eventually + be derived by "evaluating" (calling) the function. + + Note that this base class is provided as a convenience and you do not have to inherit it: any + object with an ``__evaluate__`` method would be treated similarly. + """ + + @property + def as_raw(self): + raise NotImplementedError + + def __evaluate__(self, container_holder): + """ + Evaluates the function if possible. + + :rtype: :class:`Evaluation` (or any object with ``value`` and ``final`` properties) + :raises CannotEvaluateFunctionException: if cannot be evaluated at this time (do *not* just + return ``None``) + """ + + raise NotImplementedError + + def __deepcopy__(self, memo): + # Circumvent cloning in order to maintain our state + return self + + +class Evaluation(object): + """ + An evaluated :class:`Function` return value. + + :ivar value: evaluated value + :ivar final: whether the value is final + :vartype final: boolean + """ + + def __init__(self, value, final=False): + self.value = value + self.final = final + + +def evaluate(value, container_holder, report_issues=False): # pylint: disable=too-many-branches + """ + Recursively attempts to call ``__evaluate__``. If an evaluation occurred will return an + :class:`Evaluation`, otherwise it will be ``None``. If any evaluation is non-final, then the + entire evaluation will also be non-final. + + The ``container_holder`` argument should have three properties: ``container`` should return + the model that contains the value, ``service`` should return the containing + :class:`~aria.modeling.models.Service` model or None, and ``service_template`` should return the + containing :class:`~aria.modeling.models.ServiceTemplate` model or ``None``. + """ + + evaluated = False + final = True + + if hasattr(value, '__evaluate__'): + try: + evaluation = value.__evaluate__(container_holder) + + # Verify evaluation structure + if (evaluation is None) \ + or (not hasattr(evaluation, 'value')) \ + or (not hasattr(evaluation, 'final')): + raise InvalidValueError('bad __evaluate__ implementation: {0}' + .format(full_type_name(value))) + + evaluated = True + value = evaluation.value + final = evaluation.final + + # The evaluated value might itself be evaluable + evaluation = evaluate(value, container_holder, report_issues) + if evaluation is not None: + value = evaluation.value + if not evaluation.final: + final = False + except exceptions.CannotEvaluateFunctionException: + pass + except InvalidValueError as e: + if report_issues: + context = ConsumptionContext.get_thread_local() + context.validation.report(e.issue) + + elif isinstance(value, list): + evaluated_list = [] + for v in value: + evaluation = evaluate(v, container_holder, report_issues) + if evaluation is not None: + evaluated_list.append(evaluation.value) + evaluated = True + if not evaluation.final: + final = False + else: + evaluated_list.append(v) + if evaluated: + value = evaluated_list + + elif isinstance(value, dict): + evaluated_dict = OrderedDict() + for k, v in value.iteritems(): + evaluation = evaluate(v, container_holder, report_issues) + if evaluation is not None: + evaluated_dict[k] = evaluation.value + evaluated = True + if not evaluation.final: + final = False + else: + evaluated_dict[k] = v + if evaluated: + value = evaluated_dict + + return Evaluation(value, final) if evaluated else None diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/mixins.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/mixins.py new file mode 100644 index 0000000..d58c25a --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/mixins.py @@ -0,0 +1,333 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ARIA modeling mix-ins module +""" + +from sqlalchemy.ext import associationproxy +from sqlalchemy import ( + Column, + Integer, + Text, + PickleType +) + +from ..utils import collections, caching +from ..utils.type import canonical_type_name, full_type_name +from . import utils, functions + + +class ModelMixin(object): + + @utils.classproperty + def __modelname__(cls): # pylint: disable=no-self-argument + return getattr(cls, '__mapiname__', cls.__tablename__) + + @classmethod + def id_column_name(cls): + raise NotImplementedError + + @classmethod + def name_column_name(cls): + raise NotImplementedError + + def to_dict(self, fields=None, suppress_error=False): + """ + Create a dict representation of the model. + + :param suppress_error: if set to ``True``, sets ``None`` to attributes that it's unable to + retrieve (e.g., if a relationship wasn't established yet, and so it's impossible to access + a property through it) + """ + + res = dict() + fields = fields or self.fields() + for field in fields: + try: + field_value = getattr(self, field) + except AttributeError: + if suppress_error: + field_value = None + else: + raise + if isinstance(field_value, list): + field_value = list(field_value) + elif isinstance(field_value, dict): + field_value = dict(field_value) + elif isinstance(field_value, ModelMixin): + field_value = field_value.to_dict() + res[field] = field_value + + return res + + @classmethod + def fields(cls): + """ + List of field names for this table. + + Mostly for backwards compatibility in the code (that uses ``fields``). + """ + + fields = set(cls._iter_association_proxies()) + fields.update(cls.__table__.columns.keys()) + return fields - set(getattr(cls, '__private_fields__', ())) + + @classmethod + def _iter_association_proxies(cls): + for col, value in vars(cls).items(): + if isinstance(value, associationproxy.AssociationProxy): + yield col + + def __repr__(self): + return '<{cls} id=`{id}`>'.format( + cls=self.__class__.__name__, + id=getattr(self, self.name_column_name())) + + +class ModelIDMixin(object): + id = Column(Integer, primary_key=True, autoincrement=True, doc=""" + Unique ID. + + :type: :obj:`int` + """) + + name = Column(Text, index=True, doc=""" + Model name. + + :type: :obj:`basestring` + """) + + @classmethod + def id_column_name(cls): + return 'id' + + @classmethod + def name_column_name(cls): + return 'name' + + +class InstanceModelMixin(ModelMixin): + """ + Mix-in for service instance models. + + All models support validation, diagnostic dumping, and representation as raw data (which can be + translated into JSON or YAML) via :meth:`as_raw`. + """ + + @property + def as_raw(self): + raise NotImplementedError + + def coerce_values(self, report_issues): + pass + + +class TemplateModelMixin(InstanceModelMixin): # pylint: disable=abstract-method + """ + Mix-in for service template models. + + All model models can be instantiated into service instance models. + """ + + +class ParameterMixin(TemplateModelMixin, caching.HasCachedMethods): #pylint: disable=abstract-method + """ + Mix-in for typed values. The value can contain nested intrinsic functions. + + This model can be used as the ``container_holder`` argument for + :func:`~aria.modeling.functions.evaluate`. + """ + + type_name = Column(Text, doc=""" + Type name. + + :type: :obj:`basestring` + """) + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + _value = Column(PickleType) + + @property + def value(self): + value = self._value + if value is not None: + evaluation = functions.evaluate(value, self) + if evaluation is not None: + value = evaluation.value + return value + + @value.setter + def value(self, value): + self._value = value + + @property + @caching.cachedmethod + def owner(self): + """ + The sole owner of this parameter, which is another model that relates to it. + + *All* parameters should have an owner model. + + :raises ~exceptions.ValueError: if failed to find an owner, which signifies an abnormal, + orphaned parameter + """ + + # Find first non-null relationship + for the_relationship in self.__mapper__.relationships: + v = getattr(self, the_relationship.key) + if v: + return v + + raise ValueError('orphaned {class_name}: does not have an owner: {name}'.format( + class_name=type(self).__name__, name=self.name)) + + @property + @caching.cachedmethod + def container(self): # pylint: disable=too-many-return-statements,too-many-branches + """ + The logical container for this parameter, which would be another model: service, node, + group, or policy (or their templates). + + The logical container is equivalent to the ``SELF`` keyword used by intrinsic functions in + TOSCA. + + *All* parameters should have a container model. + + :raises ~exceptions.ValueError: if failed to find a container model, which signifies an + abnormal, orphaned parameter + """ + + from . import models + + container = self.owner + + # Extract interface from operation + if isinstance(container, models.Operation): + container = container.interface + elif isinstance(container, models.OperationTemplate): + container = container.interface_template + + # Extract from other models + if isinstance(container, models.Interface): + container = container.node or container.group or container.relationship + elif isinstance(container, models.InterfaceTemplate): + container = container.node_template or container.group_template \ + or container.relationship_template + elif isinstance(container, models.Capability) or isinstance(container, models.Artifact): + container = container.node + elif isinstance(container, models.CapabilityTemplate) \ + or isinstance(container, models.ArtifactTemplate): + container = container.node_template + elif isinstance(container, models.Task): + container = container.actor + + # Extract node from relationship + if isinstance(container, models.Relationship): + container = container.source_node + elif isinstance(container, models.RelationshipTemplate): + container = container.requirement_template.node_template + + if container is not None: + return container + + raise ValueError('orphaned parameter: does not have a container: {0}'.format(self.name)) + + @property + @caching.cachedmethod + def service(self): + """ + The :class:`~aria.modeling.models.Service` model containing this parameter, or ``None`` if + not contained in a service. + + :raises ~exceptions.ValueError: if failed to find a container model, which signifies an + abnormal, orphaned parameter + """ + + from . import models + container = self.container + if isinstance(container, models.Service): + return container + elif hasattr(container, 'service'): + return container.service + return None + + @property + @caching.cachedmethod + def service_template(self): + """ + The :class:`~aria.modeling.models.ServiceTemplate` model containing this parameter, or + ``None`` if not contained in a service template. + + :raises ~exceptions.ValueError: if failed to find a container model, which signifies an + abnormal, orphaned parameter + """ + + from . import models + container = self.container + if isinstance(container, models.ServiceTemplate): + return container + elif hasattr(container, 'service_template'): + return container.service_template + return None + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type_name), + ('value', self.value), + ('description', self.description))) + + @property + def unwrapped(self): + return self.name, self.value + + @classmethod + def wrap(cls, name, value, description=None): + """ + Wraps an arbitrary value as a parameter. The type will be guessed via introspection. + + For primitive types, we will prefer their TOSCA aliases. See the `TOSCA Simple Profile v1.0 + cos01 specification <http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/cos01 + /TOSCA-Simple-Profile-YAML-v1.0-cos01.html#_Toc373867862>`__ + + :param name: parameter name + :type name: basestring + :param value: parameter value + :param description: human-readable description (optional) + :type description: basestring + """ + + type_name = canonical_type_name(value) + if type_name is None: + type_name = full_type_name(value) + return cls(name=name, # pylint: disable=unexpected-keyword-arg + type_name=type_name, + value=value, + description=description) + + def as_other_parameter_model(self, other_model_cls): + name, value = self.unwrapped + return other_model_cls.wrap(name, value) + + def as_argument(self): + from . import models + return self.as_other_parameter_model(models.Argument) diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/models.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/models.py new file mode 100644 index 0000000..cf84fdb --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/models.py @@ -0,0 +1,427 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Data models. + +Service template models +----------------------- + +.. autosummary:: + :nosignatures: + + aria.modeling.models.ServiceTemplate + aria.modeling.models.NodeTemplate + aria.modeling.models.GroupTemplate + aria.modeling.models.PolicyTemplate + aria.modeling.models.SubstitutionTemplate + aria.modeling.models.SubstitutionTemplateMapping + aria.modeling.models.RequirementTemplate + aria.modeling.models.RelationshipTemplate + aria.modeling.models.CapabilityTemplate + aria.modeling.models.InterfaceTemplate + aria.modeling.models.OperationTemplate + aria.modeling.models.ArtifactTemplate + aria.modeling.models.PluginSpecification + +Service instance models +----------------------- + +.. autosummary:: + :nosignatures: + + aria.modeling.models.Service + aria.modeling.models.Node + aria.modeling.models.Group + aria.modeling.models.Policy + aria.modeling.models.Substitution + aria.modeling.models.SubstitutionMapping + aria.modeling.models.Relationship + aria.modeling.models.Capability + aria.modeling.models.Interface + aria.modeling.models.Operation + aria.modeling.models.Artifact + +Common models +------------- + +.. autosummary:: + :nosignatures: + + aria.modeling.models.Output + aria.modeling.models.Input + aria.modeling.models.Configuration + aria.modeling.models.Property + aria.modeling.models.Attribute + aria.modeling.models.Type + aria.modeling.models.Metadata + +Orchestration models +-------------------- + +.. autosummary:: + :nosignatures: + + aria.modeling.models.Execution + aria.modeling.models.Task + aria.modeling.models.Log + aria.modeling.models.Plugin + aria.modeling.models.Argument +""" + +# pylint: disable=abstract-method + +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import ( + Column, + Text +) + +from . import ( + service_template, + service_instance, + service_changes, + service_common, + orchestration, + mixins, + utils +) + + +aria_declarative_base = declarative_base(cls=mixins.ModelIDMixin) + + +# See also models_to_register at the bottom of this file +__all__ = ( + 'models_to_register', + + # Service template models + 'ServiceTemplate', + 'NodeTemplate', + 'GroupTemplate', + 'PolicyTemplate', + 'SubstitutionTemplate', + 'SubstitutionTemplateMapping', + 'RequirementTemplate', + 'RelationshipTemplate', + 'CapabilityTemplate', + 'InterfaceTemplate', + 'OperationTemplate', + 'ArtifactTemplate', + 'PluginSpecification', + + # Service instance models + 'Service', + 'Node', + 'Group', + 'Policy', + 'Substitution', + 'SubstitutionMapping', + 'Relationship', + 'Capability', + 'Interface', + 'Operation', + 'Artifact', + + # Service changes models + 'ServiceUpdate', + 'ServiceUpdateStep', + 'ServiceModification', + + # Common service models + 'Input', + 'Configuration', + 'Output', + 'Property', + 'Attribute', + 'Type', + 'Metadata', + + # Orchestration models + 'Execution', + 'Plugin', + 'Task', + 'Log', + 'Argument' +) + + +# region service template models + +@utils.fix_doc +class ServiceTemplate(aria_declarative_base, service_template.ServiceTemplateBase): + name = Column(Text, index=True, unique=True) + + +@utils.fix_doc +class NodeTemplate(aria_declarative_base, service_template.NodeTemplateBase): + pass + + +@utils.fix_doc +class GroupTemplate(aria_declarative_base, service_template.GroupTemplateBase): + pass + + +@utils.fix_doc +class PolicyTemplate(aria_declarative_base, service_template.PolicyTemplateBase): + pass + + +@utils.fix_doc +class SubstitutionTemplate(aria_declarative_base, service_template.SubstitutionTemplateBase): + pass + + +@utils.fix_doc +class SubstitutionTemplateMapping(aria_declarative_base, + service_template.SubstitutionTemplateMappingBase): + pass + + +@utils.fix_doc +class RequirementTemplate(aria_declarative_base, service_template.RequirementTemplateBase): + pass + + +@utils.fix_doc +class RelationshipTemplate(aria_declarative_base, service_template.RelationshipTemplateBase): + pass + + +@utils.fix_doc +class CapabilityTemplate(aria_declarative_base, service_template.CapabilityTemplateBase): + pass + + +@utils.fix_doc +class InterfaceTemplate(aria_declarative_base, service_template.InterfaceTemplateBase): + pass + + +@utils.fix_doc +class OperationTemplate(aria_declarative_base, service_template.OperationTemplateBase): + pass + + +@utils.fix_doc +class ArtifactTemplate(aria_declarative_base, service_template.ArtifactTemplateBase): + pass + + +@utils.fix_doc +class PluginSpecification(aria_declarative_base, service_template.PluginSpecificationBase): + pass + +# endregion + + +# region service instance models + +@utils.fix_doc +class Service(aria_declarative_base, service_instance.ServiceBase): + name = Column(Text, index=True, unique=True) + + +@utils.fix_doc +class Node(aria_declarative_base, service_instance.NodeBase): + pass + + +@utils.fix_doc +class Group(aria_declarative_base, service_instance.GroupBase): + pass + + +@utils.fix_doc +class Policy(aria_declarative_base, service_instance.PolicyBase): + pass + + +@utils.fix_doc +class Substitution(aria_declarative_base, service_instance.SubstitutionBase): + pass + + +@utils.fix_doc +class SubstitutionMapping(aria_declarative_base, service_instance.SubstitutionMappingBase): + pass + + +@utils.fix_doc +class Relationship(aria_declarative_base, service_instance.RelationshipBase): + pass + + +@utils.fix_doc +class Capability(aria_declarative_base, service_instance.CapabilityBase): + pass + + +@utils.fix_doc +class Interface(aria_declarative_base, service_instance.InterfaceBase): + pass + + +@utils.fix_doc +class Operation(aria_declarative_base, service_instance.OperationBase): + pass + + +@utils.fix_doc +class Artifact(aria_declarative_base, service_instance.ArtifactBase): + pass + +# endregion + + +# region service changes models + +@utils.fix_doc +class ServiceUpdate(aria_declarative_base, service_changes.ServiceUpdateBase): + pass + + +@utils.fix_doc +class ServiceUpdateStep(aria_declarative_base, service_changes.ServiceUpdateStepBase): + pass + + +@utils.fix_doc +class ServiceModification(aria_declarative_base, service_changes.ServiceModificationBase): + pass + +# endregion + + +# region common service models + +@utils.fix_doc +class Input(aria_declarative_base, service_common.InputBase): + pass + + +@utils.fix_doc +class Configuration(aria_declarative_base, service_common.ConfigurationBase): + pass + + +@utils.fix_doc +class Output(aria_declarative_base, service_common.OutputBase): + pass + + +@utils.fix_doc +class Property(aria_declarative_base, service_common.PropertyBase): + pass + + +@utils.fix_doc +class Attribute(aria_declarative_base, service_common.AttributeBase): + pass + + +@utils.fix_doc +class Type(aria_declarative_base, service_common.TypeBase): + pass + + +@utils.fix_doc +class Metadata(aria_declarative_base, service_common.MetadataBase): + pass + +# endregion + + +# region orchestration models + +@utils.fix_doc +class Execution(aria_declarative_base, orchestration.ExecutionBase): + pass + + +@utils.fix_doc +class Plugin(aria_declarative_base, orchestration.PluginBase): + pass + + +@utils.fix_doc +class Task(aria_declarative_base, orchestration.TaskBase): + pass + + +@utils.fix_doc +class Log(aria_declarative_base, orchestration.LogBase): + pass + + +@utils.fix_doc +class Argument(aria_declarative_base, orchestration.ArgumentBase): + pass + +# endregion + + +# See also __all__ at the top of this file +models_to_register = ( + # Service template models + ServiceTemplate, + NodeTemplate, + GroupTemplate, + PolicyTemplate, + SubstitutionTemplate, + SubstitutionTemplateMapping, + RequirementTemplate, + RelationshipTemplate, + CapabilityTemplate, + InterfaceTemplate, + OperationTemplate, + ArtifactTemplate, + PluginSpecification, + + # Service instance models + Service, + Node, + Group, + Policy, + SubstitutionMapping, + Substitution, + Relationship, + Capability, + Interface, + Operation, + Artifact, + + # Service changes models + ServiceUpdate, + ServiceUpdateStep, + ServiceModification, + + # Common service models + Input, + Configuration, + Output, + Property, + Attribute, + Type, + Metadata, + + # Orchestration models + Execution, + Plugin, + Task, + Log, + Argument +) diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/orchestration.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/orchestration.py new file mode 100644 index 0000000..4d4f0fe --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/orchestration.py @@ -0,0 +1,715 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ARIA modeling orchestration module +""" + +# pylint: disable=no-self-argument, no-member, abstract-method +from datetime import datetime + +from sqlalchemy import ( + Column, + Integer, + Text, + DateTime, + Boolean, + Enum, + String, + Float, + orm, + PickleType) +from sqlalchemy.ext.declarative import declared_attr + +from ..orchestrator.exceptions import (TaskAbortException, TaskRetryException) +from . import mixins +from . import ( + relationship, + types as modeling_types +) + + +class ExecutionBase(mixins.ModelMixin): + """ + Workflow execution. + """ + + __tablename__ = 'execution' + + __private_fields__ = ('service_fk', + 'service_template') + + SUCCEEDED = 'succeeded' + FAILED = 'failed' + CANCELLED = 'cancelled' + PENDING = 'pending' + STARTED = 'started' + CANCELLING = 'cancelling' + + STATES = (SUCCEEDED, FAILED, CANCELLED, PENDING, STARTED, CANCELLING) + END_STATES = (SUCCEEDED, FAILED, CANCELLED) + + VALID_TRANSITIONS = { + PENDING: (STARTED, CANCELLED), + STARTED: END_STATES + (CANCELLING,), + CANCELLING: END_STATES, + # Retrying + CANCELLED: PENDING, + FAILED: PENDING + } + + # region one_to_many relationships + + @declared_attr + def inputs(cls): + """ + Execution parameters. + + :type: {:obj:`basestring`: :class:`Input`} + """ + return relationship.one_to_many(cls, 'input', dict_key='name') + + @declared_attr + def tasks(cls): + """ + Tasks. + + :type: [:class:`Task`] + """ + return relationship.one_to_many(cls, 'task') + + @declared_attr + def logs(cls): + """ + Log messages for the execution (including log messages for its tasks). + + :type: [:class:`Log`] + """ + return relationship.one_to_many(cls, 'log') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service(cls): + """ + Associated service. + + :type: :class:`Service` + """ + return relationship.many_to_one(cls, 'service') + + # endregion + + # region association proxies + + @declared_attr + def service_name(cls): + return relationship.association_proxy('service', cls.name_column_name()) + + @declared_attr + def service_template(cls): + return relationship.association_proxy('service', 'service_template') + + @declared_attr + def service_template_name(cls): + return relationship.association_proxy('service', 'service_template_name') + + # endregion + + # region foreign keys + + @declared_attr + def service_fk(cls): + return relationship.foreign_key('service') + + # endregion + + created_at = Column(DateTime, index=True, doc=""" + Creation timestamp. + + :type: :class:`~datetime.datetime` + """) + + started_at = Column(DateTime, nullable=True, index=True, doc=""" + Started timestamp. + + :type: :class:`~datetime.datetime` + """) + + ended_at = Column(DateTime, nullable=True, index=True, doc=""" + Ended timestamp. + + :type: :class:`~datetime.datetime` + """) + + error = Column(Text, nullable=True, doc=""" + Error message. + + :type: :obj:`basestring` + """) + + status = Column(Enum(*STATES, name='execution_status'), default=PENDING, doc=""" + Status. + + :type: :obj:`basestring` + """) + + workflow_name = Column(Text, doc=""" + Workflow name. + + :type: :obj:`basestring` + """) + + @orm.validates('status') + def validate_status(self, key, value): + """Validation function that verifies execution status transitions are OK""" + try: + current_status = getattr(self, key) + except AttributeError: + return + valid_transitions = self.VALID_TRANSITIONS.get(current_status, []) + if all([current_status is not None, + current_status != value, + value not in valid_transitions]): + raise ValueError('Cannot change execution status from {current} to {new}'.format( + current=current_status, + new=value)) + return value + + def has_ended(self): + return self.status in self.END_STATES + + def is_active(self): + return not self.has_ended() and self.status != self.PENDING + + def __str__(self): + return '<{0} id=`{1}` (status={2})>'.format( + self.__class__.__name__, + getattr(self, self.name_column_name()), + self.status + ) + + +class TaskBase(mixins.ModelMixin): + """ + Represents the smallest unit of stateful execution in ARIA. The task state includes inputs, + outputs, as well as an atomic status, ensuring that the task can only be running once at any + given time. + + The Python :attr:`function` is usually provided by an associated :class:`Plugin`. The + :attr:`arguments` of the function should be set according to the specific signature of the + function. + + Tasks may be "one shot" or may be configured to run repeatedly in the case of failure. + + Tasks are often based on :class:`Operation`, and thus act on either a :class:`Node` or a + :class:`Relationship`, however this is not required. + """ + + __tablename__ = 'task' + + __private_fields__ = ('node_fk', + 'relationship_fk', + 'plugin_fk', + 'execution_fk') + + START_WORKFLOW = 'start_workflow' + END_WORKFLOW = 'end_workflow' + START_SUBWROFKLOW = 'start_subworkflow' + END_SUBWORKFLOW = 'end_subworkflow' + STUB = 'stub' + CONDITIONAL = 'conditional' + + STUB_TYPES = ( + START_WORKFLOW, + START_SUBWROFKLOW, + END_WORKFLOW, + END_SUBWORKFLOW, + STUB, + CONDITIONAL, + ) + + PENDING = 'pending' + RETRYING = 'retrying' + SENT = 'sent' + STARTED = 'started' + SUCCESS = 'success' + FAILED = 'failed' + STATES = ( + PENDING, + RETRYING, + SENT, + STARTED, + SUCCESS, + FAILED, + ) + INFINITE_RETRIES = -1 + + # region one_to_many relationships + + @declared_attr + def logs(cls): + """ + Log messages. + + :type: [:class:`Log`] + """ + return relationship.one_to_many(cls, 'log') + + @declared_attr + def arguments(cls): + """ + Arguments sent to the Python :attr:`function``. + + :type: {:obj:`basestring`: :class:`Argument`} + """ + return relationship.one_to_many(cls, 'argument', dict_key='name') + + # endregion + + # region many_one relationships + + @declared_attr + def execution(cls): + """ + Containing execution. + + :type: :class:`Execution` + """ + return relationship.many_to_one(cls, 'execution') + + @declared_attr + def node(cls): + """ + Node actor (can be ``None``). + + :type: :class:`Node` + """ + return relationship.many_to_one(cls, 'node') + + @declared_attr + def relationship(cls): + """ + Relationship actor (can be ``None``). + + :type: :class:`Relationship` + """ + return relationship.many_to_one(cls, 'relationship') + + @declared_attr + def plugin(cls): + """ + Associated plugin. + + :type: :class:`Plugin` + """ + return relationship.many_to_one(cls, 'plugin') + + # endregion + + # region association proxies + + @declared_attr + def node_name(cls): + return relationship.association_proxy('node', cls.name_column_name()) + + @declared_attr + def relationship_name(cls): + return relationship.association_proxy('relationship', cls.name_column_name()) + + @declared_attr + def execution_name(cls): + return relationship.association_proxy('execution', cls.name_column_name()) + + # endregion + + # region foreign keys + + @declared_attr + def execution_fk(cls): + return relationship.foreign_key('execution', nullable=True) + + @declared_attr + def node_fk(cls): + return relationship.foreign_key('node', nullable=True) + + @declared_attr + def relationship_fk(cls): + return relationship.foreign_key('relationship', nullable=True) + + @declared_attr + def plugin_fk(cls): + return relationship.foreign_key('plugin', nullable=True) + + # endregion + + status = Column(Enum(*STATES, name='status'), default=PENDING, doc=""" + Current atomic status ('pending', 'retrying', 'sent', 'started', 'success', 'failed'). + + :type: :obj:`basestring` + """) + + due_at = Column(DateTime, nullable=False, index=True, default=datetime.utcnow(), doc=""" + Timestamp to start the task. + + :type: :class:`~datetime.datetime` + """) + + started_at = Column(DateTime, default=None, doc=""" + Started timestamp. + + :type: :class:`~datetime.datetime` + """) + + ended_at = Column(DateTime, default=None, doc=""" + Ended timestamp. + + :type: :class:`~datetime.datetime` + """) + + attempts_count = Column(Integer, default=1, doc=""" + How many attempts occurred. + + :type: :class:`~datetime.datetime` + """) + + function = Column(String, doc=""" + Full path to Python function. + + :type: :obj:`basestring` + """) + + max_attempts = Column(Integer, default=1, doc=""" + Maximum number of attempts allowed in case of task failure. + + :type: :obj:`int` + """) + + retry_interval = Column(Float, default=0, doc=""" + Interval between task retry attemps (in seconds). + + :type: :obj:`float` + """) + + ignore_failure = Column(Boolean, default=False, doc=""" + Set to ``True`` to ignore failures. + + :type: :obj:`bool` + """) + + interface_name = Column(String, doc=""" + Name of interface on node or relationship. + + :type: :obj:`basestring` + """) + + operation_name = Column(String, doc=""" + Name of operation in interface on node or relationship. + + :type: :obj:`basestring` + """) + + _api_id = Column(String) + _executor = Column(PickleType) + _context_cls = Column(PickleType) + _stub_type = Column(Enum(*STUB_TYPES)) + + @property + def actor(self): + """ + Actor of the task (node or relationship). + """ + return self.node or self.relationship + + @orm.validates('max_attempts') + def validate_max_attempts(self, _, value): # pylint: disable=no-self-use + """ + Validates that max attempts is either -1 or a positive number. + """ + if value < 1 and value != TaskBase.INFINITE_RETRIES: + raise ValueError('Max attempts can be either -1 (infinite) or any positive number. ' + 'Got {value}'.format(value=value)) + return value + + @staticmethod + def abort(message=None): + raise TaskAbortException(message) + + @staticmethod + def retry(message=None, retry_interval=None): + raise TaskRetryException(message, retry_interval=retry_interval) + + @declared_attr + def dependencies(cls): + return relationship.many_to_many(cls, self=True) + + def has_ended(self): + return self.status in (self.SUCCESS, self.FAILED) + + def is_waiting(self): + if self._stub_type: + return not self.has_ended() + else: + return self.status in (self.PENDING, self.RETRYING) + + @classmethod + def from_api_task(cls, api_task, executor, **kwargs): + instantiation_kwargs = {} + + if hasattr(api_task.actor, 'outbound_relationships'): + instantiation_kwargs['node'] = api_task.actor + elif hasattr(api_task.actor, 'source_node'): + instantiation_kwargs['relationship'] = api_task.actor + else: + raise RuntimeError('No operation context could be created for {actor.model_cls}' + .format(actor=api_task.actor)) + + instantiation_kwargs.update( + { + 'name': api_task.name, + 'status': cls.PENDING, + 'max_attempts': api_task.max_attempts, + 'retry_interval': api_task.retry_interval, + 'ignore_failure': api_task.ignore_failure, + 'execution': api_task._workflow_context.execution, + 'interface_name': api_task.interface_name, + 'operation_name': api_task.operation_name, + + # Only non-stub tasks have these fields + 'plugin': api_task.plugin, + 'function': api_task.function, + 'arguments': api_task.arguments, + '_context_cls': api_task._context_cls, + '_executor': executor, + } + ) + + instantiation_kwargs.update(**kwargs) + + return cls(**instantiation_kwargs) + + +class LogBase(mixins.ModelMixin): + """ + Single log message. + """ + + __tablename__ = 'log' + + __private_fields__ = ('execution_fk', + 'task_fk') + + # region many_to_one relationships + + @declared_attr + def execution(cls): + """ + Containing execution. + + :type: :class:`Execution` + """ + return relationship.many_to_one(cls, 'execution') + + @declared_attr + def task(cls): + """ + Containing task (can be ``None``). + + :type: :class:`Task` + """ + return relationship.many_to_one(cls, 'task') + + # endregion + + # region foreign keys + + @declared_attr + def execution_fk(cls): + return relationship.foreign_key('execution') + + @declared_attr + def task_fk(cls): + return relationship.foreign_key('task', nullable=True) + + # endregion + + level = Column(String, doc=""" + Log level. + + :type: :obj:`basestring` + """) + + msg = Column(String, doc=""" + Log message. + + :type: :obj:`basestring` + """) + + created_at = Column(DateTime, index=True, doc=""" + Creation timestamp. + + :type: :class:`~datetime.datetime` + """) + + traceback = Column(Text, doc=""" + Error traceback in case of failure. + + :type: :class:`~datetime.datetime` + """) + + def __str__(self): + return self.msg + + def __repr__(self): + name = (self.task.actor if self.task else self.execution).name + return '{name}: {self.msg}'.format(name=name, self=self) + + +class PluginBase(mixins.ModelMixin): + """ + Installed plugin. + + Plugins are usually packaged as `wagons <https://github.com/cloudify-cosmo/wagon>`__, which + are archives of one or more `wheels <https://packaging.python.org/distributing/#wheels>`__. + Most of these fields are indeed extracted from the installed wagon's metadata. + """ + + __tablename__ = 'plugin' + + # region one_to_many relationships + + @declared_attr + def tasks(cls): + """ + Associated Tasks. + + :type: [:class:`Task`] + """ + return relationship.one_to_many(cls, 'task') + + # endregion + + archive_name = Column(Text, nullable=False, index=True, doc=""" + Filename (not the full path) of the wagon's archive, often with a ``.wgn`` extension. + + :type: :obj:`basestring` + """) + + distribution = Column(Text, doc=""" + Name of the operating system on which the wagon was installed (e.g. ``ubuntu``). + + :type: :obj:`basestring` + """) + + distribution_release = Column(Text, doc=""" + Release of the operating system on which the wagon was installed (e.g. ``trusty``). + + :type: :obj:`basestring` + """) + + distribution_version = Column(Text, doc=""" + Version of the operating system on which the wagon was installed (e.g. ``14.04``). + + :type: :obj:`basestring` + """) + + package_name = Column(Text, nullable=False, index=True, doc=""" + Primary Python package name used when the wagon was installed, which is one of the wheels in the + wagon (e.g. ``cloudify-script-plugin``). + + :type: :obj:`basestring` + """) + + package_source = Column(Text, doc=""" + Full install string for the primary Python package name used when the wagon was installed (e.g. + ``cloudify-script-plugin==1.2``). + + :type: :obj:`basestring` + """) + + package_version = Column(Text, doc=""" + Version for the primary Python package name used when the wagon was installed (e.g. ``1.2``). + + :type: :obj:`basestring` + """) + + supported_platform = Column(Text, doc=""" + If the wheels are *all* pure Python then this would be "any", otherwise it would be the + installed platform name (e.g. ``linux_x86_64``). + + :type: :obj:`basestring` + """) + + supported_py_versions = Column(modeling_types.StrictList(basestring), doc=""" + Python versions supported by all the wheels (e.g. ``["py26", "py27"]``) + + :type: [:obj:`basestring`] + """) + + wheels = Column(modeling_types.StrictList(basestring), nullable=False, doc=""" + Filenames of the wheels archived in the wagon, often with a ``.whl`` extension. + + :type: [:obj:`basestring`] + """) + + uploaded_at = Column(DateTime, nullable=False, index=True, doc=""" + Timestamp for when the wagon was installed. + + :type: :class:`~datetime.datetime` + """) + + +class ArgumentBase(mixins.ParameterMixin): + """ + Python function argument parameter. + """ + + __tablename__ = 'argument' + + # region many_to_one relationships + + @declared_attr + def task(cls): + """ + Containing task (can be ``None``); + + :type: :class:`Task` + """ + return relationship.many_to_one(cls, 'task') + + @declared_attr + def operation(cls): + """ + Containing operation (can be ``None``); + + :type: :class:`Operation` + """ + return relationship.many_to_one(cls, 'operation') + + # endregion + + # region foreign keys + + @declared_attr + def task_fk(cls): + return relationship.foreign_key('task', nullable=True) + + @declared_attr + def operation_fk(cls): + return relationship.foreign_key('operation', nullable=True) + + # endregion diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/relationship.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/relationship.py new file mode 100644 index 0000000..0d906de --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/relationship.py @@ -0,0 +1,395 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ARIA modeling relationship module +""" + +# pylint: disable=invalid-name, redefined-outer-name + +from sqlalchemy.orm import relationship, backref +from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.ext.associationproxy import association_proxy as original_association_proxy +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Table +) + +from ..utils import formatting + +NO_BACK_POP = 'NO_BACK_POP' + + +def foreign_key(other_table, nullable=False): + """ + Declare a foreign key property, which will also create a foreign key column in the table with + the name of the property. By convention the property name should end in "_fk". + + You are required to explicitly create foreign keys in order to allow for one-to-one, + one-to-many, and many-to-one relationships (but not for many-to-many relationships). If you do + not do so, SQLAlchemy will fail to create the relationship property and raise an exception with + a clear error message. + + You should normally not have to access this property directly, but instead use the associated + relationship properties. + + *This utility method should only be used during class creation.* + + :param other_table: other table name + :type other_table: basestring + :param nullable: ``True`` to allow null values (meaning that there is no relationship) + :type nullable: bool + """ + + return Column(Integer, + ForeignKey('{table}.id'.format(table=other_table), ondelete='CASCADE'), + nullable=nullable) + + +def one_to_one_self(model_class, fk): + """ + Declare a one-to-one relationship property. The property value would be an instance of the same + model. + + You will need an associated foreign key to our own table. + + *This utility method should only be used during class creation.* + + :param model_class: class in which this relationship will be declared + :type model_class: type + :param fk: foreign key name + :type fk: basestring + """ + + remote_side = '{model_class}.{remote_column}'.format( + model_class=model_class.__name__, + remote_column=model_class.id_column_name() + ) + + primaryjoin = '{remote_side} == {model_class}.{column}'.format( + remote_side=remote_side, + model_class=model_class.__name__, + column=fk + ) + return _relationship( + model_class, + model_class.__tablename__, + relationship_kwargs={ + 'primaryjoin': primaryjoin, + 'remote_side': remote_side, + 'post_update': True + } + ) + + +def one_to_one(model_class, + other_table, + fk=None, + other_fk=None, + back_populates=None): + """ + Declare a one-to-one relationship property. The property value would be an instance of the other + table's model. + + You have two options for the foreign key. Either this table can have an associated key to the + other table (use the ``fk`` argument) or the other table can have an associated foreign key to + this our table (use the ``other_fk`` argument). + + *This utility method should only be used during class creation.* + + :param model_class: class in which this relationship will be declared + :type model_class: type + :param other_table: other table name + :type other_table: basestring + :param fk: foreign key name at our table (no need specify if there's no ambiguity) + :type fk: basestring + :param other_fk: foreign key name at the other table (no need specify if there's no ambiguity) + :type other_fk: basestring + :param back_populates: override name of matching many-to-many property at other table; set to + ``False`` to disable + :type back_populates: basestring or bool + """ + backref_kwargs = None + if back_populates is not NO_BACK_POP: + if back_populates is None: + back_populates = model_class.__tablename__ + backref_kwargs = {'name': back_populates, 'uselist': False} + back_populates = None + + return _relationship(model_class, + other_table, + fk=fk, + back_populates=back_populates, + backref_kwargs=backref_kwargs, + other_fk=other_fk) + + +def one_to_many(model_class, + other_table=None, + other_fk=None, + dict_key=None, + back_populates=None, + rel_kwargs=None, + self=False): + """ + Declare a one-to-many relationship property. The property value would be a list or dict of + instances of the child table's model. + + The child table will need an associated foreign key to our table. + + The declaration will automatically create a matching many-to-one property at the child model, + named after our table name. Use the ``child_property`` argument to override this name. + + *This utility method should only be used during class creation.* + + :param model_class: class in which this relationship will be declared + :type model_class: type + :param other_table: other table name + :type other_table: basestring + :param other_fk: foreign key name at the other table (no need specify if there's no ambiguity) + :type other_fk: basestring + :param dict_key: if set the value will be a dict with this key as the dict key; otherwise will + be a list + :type dict_key: basestring + :param back_populates: override name of matching many-to-one property at other table; set to + ``False`` to disable + :type back_populates: basestring or bool + :param rel_kwargs: additional relationship kwargs to be used by SQLAlchemy + :type rel_kwargs: dict + :param self: used for relationships between a table and itself. if set, other_table will + become the same as the source table. + :type self: bool + """ + relationship_kwargs = rel_kwargs or {} + if self: + assert other_fk + other_table_name = model_class.__tablename__ + back_populates = False + relationship_kwargs['remote_side'] = '{model}.{column}'.format(model=model_class.__name__, + column=other_fk) + + else: + assert other_table + other_table_name = other_table + if back_populates is None: + back_populates = model_class.__tablename__ + relationship_kwargs.setdefault('cascade', 'all') + + return _relationship( + model_class, + other_table_name, + back_populates=back_populates, + other_fk=other_fk, + dict_key=dict_key, + relationship_kwargs=relationship_kwargs) + + +def many_to_one(model_class, + parent_table, + fk=None, + parent_fk=None, + back_populates=None): + """ + Declare a many-to-one relationship property. The property value would be an instance of the + parent table's model. + + You will need an associated foreign key to the parent table. + + The declaration will automatically create a matching one-to-many property at the child model, + named after the plural form of our table name. Use the ``parent_property`` argument to override + this name. Note: the automatic property will always be a SQLAlchemy query object; if you need a + Python collection then use :func:`one_to_many` at that model. + + *This utility method should only be used during class creation.* + + :param model_class: class in which this relationship will be declared + :type model_class: type + :param parent_table: parent table name + :type parent_table: basestring + :param fk: foreign key name at our table (no need specify if there's no ambiguity) + :type fk: basestring + :param back_populates: override name of matching one-to-many property at parent table; set to + ``False`` to disable + :type back_populates: basestring or bool + """ + if back_populates is None: + back_populates = formatting.pluralize(model_class.__tablename__) + + return _relationship(model_class, + parent_table, + back_populates=back_populates, + fk=fk, + other_fk=parent_fk) + + +def many_to_many(model_class, + other_table=None, + prefix=None, + dict_key=None, + other_property=None, + self=False): + """ + Declare a many-to-many relationship property. The property value would be a list or dict of + instances of the other table's model. + + You do not need associated foreign keys for this relationship. Instead, an extra table will be + created for you. + + The declaration will automatically create a matching many-to-many property at the other model, + named after the plural form of our table name. Use the ``other_property`` argument to override + this name. Note: the automatic property will always be a SQLAlchemy query object; if you need a + Python collection then use :func:`many_to_many` again at that model. + + *This utility method should only be used during class creation.* + + :param model_class: class in which this relationship will be declared + :type model_class: type + :param other_table: parent table name + :type other_table: basestring + :param prefix: optional prefix for extra table name as well as for ``other_property`` + :type prefix: basestring + :param dict_key: if set the value will be a dict with this key as the dict key; otherwise will + be a list + :type dict_key: basestring + :param other_property: override name of matching many-to-many property at other table; set to + ``False`` to disable + :type other_property: basestring or bool + :param self: used for relationships between a table and itself. if set, other_table will + become the same as the source table. + :type self: bool + """ + + this_table = model_class.__tablename__ + this_column_name = '{0}_id'.format(this_table) + this_foreign_key = '{0}.id'.format(this_table) + + if self: + other_table = this_table + + other_column_name = '{0}_{1}'.format(other_table, 'self_ref_id' if self else 'id') + other_foreign_key = '{0}.{1}'.format(other_table, 'id') + + secondary_table_name = '{0}_{1}'.format(this_table, other_table) + + if prefix is not None: + secondary_table_name = '{0}_{1}'.format(prefix, secondary_table_name) + if other_property is None: + other_property = '{0}_{1}'.format(prefix, formatting.pluralize(this_table)) + + secondary_table = _get_secondary_table( + model_class.metadata, + secondary_table_name, + this_column_name, + other_column_name, + this_foreign_key, + other_foreign_key + ) + + kwargs = {'relationship_kwargs': {'secondary': secondary_table}} + + if self: + kwargs['back_populates'] = NO_BACK_POP + kwargs['relationship_kwargs']['primaryjoin'] = \ + getattr(model_class, 'id') == getattr(secondary_table.c, this_column_name) + kwargs['relationship_kwargs']['secondaryjoin'] = \ + getattr(model_class, 'id') == getattr(secondary_table.c, other_column_name) + else: + kwargs['backref_kwargs'] = \ + {'name': other_property, 'uselist': True} if other_property else None + kwargs['dict_key'] = dict_key + + return _relationship(model_class, other_table, **kwargs) + + +def association_proxy(*args, **kwargs): + if 'type' in kwargs: + type_ = kwargs.get('type') + del kwargs['type'] + else: + type_ = ':obj:`basestring`' + proxy = original_association_proxy(*args, **kwargs) + proxy.__doc__ = """ + Internal. For use in SQLAlchemy queries. + + :type: {0} + """.format(type_) + return proxy + + +def _relationship(model_class, + other_table_name, + back_populates=None, + backref_kwargs=None, + relationship_kwargs=None, + fk=None, + other_fk=None, + dict_key=None): + relationship_kwargs = relationship_kwargs or {} + + if fk: + relationship_kwargs.setdefault( + 'foreign_keys', + lambda: getattr(_get_class_for_table(model_class, model_class.__tablename__), fk) + ) + + elif other_fk: + relationship_kwargs.setdefault( + 'foreign_keys', + lambda: getattr(_get_class_for_table(model_class, other_table_name), other_fk) + ) + + if dict_key: + relationship_kwargs.setdefault('collection_class', + attribute_mapped_collection(dict_key)) + + if backref_kwargs: + assert back_populates is None + return relationship( + lambda: _get_class_for_table(model_class, other_table_name), + backref=backref(**backref_kwargs), + **relationship_kwargs + ) + else: + if back_populates is not NO_BACK_POP: + relationship_kwargs['back_populates'] = back_populates + return relationship(lambda: _get_class_for_table(model_class, other_table_name), + **relationship_kwargs) + + +def _get_class_for_table(model_class, tablename): + if tablename in (model_class.__name__, model_class.__tablename__): + return model_class + + for table_cls in model_class._decl_class_registry.itervalues(): + if tablename == getattr(table_cls, '__tablename__', None): + return table_cls + + raise ValueError('unknown table: {0}'.format(tablename)) + + +def _get_secondary_table(metadata, + name, + first_column, + second_column, + first_foreign_key, + second_foreign_key): + return Table( + name, + metadata, + Column(first_column, Integer, ForeignKey(first_foreign_key)), + Column(second_column, Integer, ForeignKey(second_foreign_key)) + ) diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_changes.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_changes.py new file mode 100644 index 0000000..061262a --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_changes.py @@ -0,0 +1,253 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ARIA modeling service changes module +""" + +# pylint: disable=no-self-argument, no-member, abstract-method + +from collections import namedtuple + +from sqlalchemy import ( + Column, + Text, + DateTime, + Enum, +) +from sqlalchemy.ext.declarative import declared_attr + +from .types import (List, Dict) +from .mixins import ModelMixin +from . import relationship + + +class ServiceUpdateBase(ModelMixin): + """ + Deployment update model representation. + """ + __tablename__ = 'service_update' + + __private_fields__ = ('service_fk', + 'execution_fk') + + created_at = Column(DateTime, nullable=False, index=True) + service_plan = Column(Dict, nullable=False) + service_update_nodes = Column(Dict) + service_update_service = Column(Dict) + service_update_node_templates = Column(List) + modified_entity_ids = Column(Dict) + state = Column(Text) + + # region association proxies + + @declared_attr + def execution_name(cls): + return relationship.association_proxy('execution', cls.name_column_name()) + + @declared_attr + def service_name(cls): + return relationship.association_proxy('service', cls.name_column_name()) + + # endregion + + # region one_to_one relationships + + # endregion + + # region one_to_many relationships + + @declared_attr + def steps(cls): + return relationship.one_to_many(cls, 'service_update_step') + + # endregion + + # region many_to_one relationships + + @declared_attr + def execution(cls): + return relationship.one_to_one(cls, 'execution', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def service(cls): + return relationship.many_to_one(cls, 'service', back_populates='updates') + + # endregion + + # region foreign keys + + @declared_attr + def execution_fk(cls): + return relationship.foreign_key('execution', nullable=True) + + @declared_attr + def service_fk(cls): + return relationship.foreign_key('service') + + # endregion + + def to_dict(self, suppress_error=False, **kwargs): + dep_update_dict = super(ServiceUpdateBase, self).to_dict(suppress_error) #pylint: disable=no-member + # Taking care of the fact the DeploymentSteps are _BaseModels + dep_update_dict['steps'] = [step.to_dict() for step in self.steps] + return dep_update_dict + + +class ServiceUpdateStepBase(ModelMixin): + """ + Deployment update step model representation. + """ + + __tablename__ = 'service_update_step' + + __private_fields__ = ('service_update_fk',) + + _action_types = namedtuple('ACTION_TYPES', 'ADD, REMOVE, MODIFY') + ACTION_TYPES = _action_types(ADD='add', REMOVE='remove', MODIFY='modify') + + _entity_types = namedtuple( + 'ENTITY_TYPES', + 'NODE, RELATIONSHIP, PROPERTY, OPERATION, WORKFLOW, OUTPUT, DESCRIPTION, GROUP, PLUGIN') + ENTITY_TYPES = _entity_types( + NODE='node', + RELATIONSHIP='relationship', + PROPERTY='property', + OPERATION='operation', + WORKFLOW='workflow', + OUTPUT='output', + DESCRIPTION='description', + GROUP='group', + PLUGIN='plugin' + ) + + action = Column(Enum(*ACTION_TYPES, name='action_type'), nullable=False) + entity_id = Column(Text, nullable=False) + entity_type = Column(Enum(*ENTITY_TYPES, name='entity_type'), nullable=False) + + # region association proxies + + @declared_attr + def service_update_name(cls): + return relationship.association_proxy('service_update', cls.name_column_name()) + + # endregion + + # region one_to_one relationships + + # endregion + + # region one_to_many relationships + + # endregion + + # region many_to_one relationships + + @declared_attr + def service_update(cls): + return relationship.many_to_one(cls, 'service_update', back_populates='steps') + + # endregion + + # region foreign keys + + @declared_attr + def service_update_fk(cls): + return relationship.foreign_key('service_update') + + # endregion + + def __hash__(self): + return hash((getattr(self, self.id_column_name()), self.entity_id)) + + def __lt__(self, other): + """ + the order is 'remove' < 'modify' < 'add' + :param other: + :return: + """ + if not isinstance(other, self.__class__): + return not self >= other + + if self.action != other.action: + if self.action == 'remove': + return_value = True + elif self.action == 'add': + return_value = False + else: + return_value = other.action == 'add' + return return_value + + if self.action == 'add': + return self.entity_type == 'node' and other.entity_type == 'relationship' + if self.action == 'remove': + return self.entity_type == 'relationship' and other.entity_type == 'node' + return False + + +class ServiceModificationBase(ModelMixin): + """ + Deployment modification model representation. + """ + + __tablename__ = 'service_modification' + + __private_fields__ = ('service_fk',) + + STARTED = 'started' + FINISHED = 'finished' + ROLLEDBACK = 'rolledback' + + STATES = [STARTED, FINISHED, ROLLEDBACK] + END_STATES = [FINISHED, ROLLEDBACK] + + context = Column(Dict) + created_at = Column(DateTime, nullable=False, index=True) + ended_at = Column(DateTime, index=True) + modified_node_templates = Column(Dict) + nodes = Column(Dict) + status = Column(Enum(*STATES, name='service_modification_status')) + + # region association proxies + + @declared_attr + def service_name(cls): + return relationship.association_proxy('service', cls.name_column_name()) + + # endregion + + # region one_to_one relationships + + # endregion + + # region one_to_many relationships + + # endregion + + # region many_to_one relationships + + @declared_attr + def service(cls): + return relationship.many_to_one(cls, 'service', back_populates='modifications') + + # endregion + + # region foreign keys + + @declared_attr + def service_fk(cls): + return relationship.foreign_key('service') + + # endregion diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_common.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_common.py new file mode 100644 index 0000000..d1f6b00 --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_common.py @@ -0,0 +1,601 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ARIA modeling service common module +""" + +# pylint: disable=no-self-argument, no-member, abstract-method + +from sqlalchemy import ( + Column, + Text, + Boolean +) +from sqlalchemy.ext.declarative import declared_attr + +from ..utils import ( + collections, + formatting +) +from .mixins import InstanceModelMixin, TemplateModelMixin, ParameterMixin +from . import relationship + + +class OutputBase(ParameterMixin): + """ + Output parameter or declaration for an output parameter. + """ + + __tablename__ = 'output' + + # region many_to_one relationships + + @declared_attr + def service_template(cls): + """ + Containing service template (can be ``None``). + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template') + + @declared_attr + def service(cls): + """ + Containing service (can be ``None``). + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service') + + # endregion + + # region foreign keys + + @declared_attr + def service_template_fk(cls): + return relationship.foreign_key('service_template', nullable=True) + + @declared_attr + def service_fk(cls): + return relationship.foreign_key('service', nullable=True) + + # endregion + + +class InputBase(ParameterMixin): + """ + Input parameter or declaration for an input parameter. + """ + + __tablename__ = 'input' + + required = Column(Boolean, doc=""" + Is the input mandatory. + + :type: :obj:`bool` + """) + + @classmethod + def wrap(cls, name, value, description=None, required=True): # pylint: disable=arguments-differ + input = super(InputBase, cls).wrap(name, value, description) + input.required = required + return input + + # region many_to_one relationships + + @declared_attr + def service_template(cls): + """ + Containing service template (can be ``None``). + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template') + + @declared_attr + def service(cls): + """ + Containing service (can be ``None``). + + :type: :class:`Service` + """ + return relationship.many_to_one(cls, 'service') + + @declared_attr + def interface(cls): + """ + Containing interface (can be ``None``). + + :type: :class:`Interface` + """ + return relationship.many_to_one(cls, 'interface') + + @declared_attr + def operation(cls): + """ + Containing operation (can be ``None``). + + :type: :class:`Operation` + """ + return relationship.many_to_one(cls, 'operation') + + @declared_attr + def interface_template(cls): + """ + Containing interface template (can be ``None``). + + :type: :class:`InterfaceTemplate` + """ + return relationship.many_to_one(cls, 'interface_template') + + @declared_attr + def operation_template(cls): + """ + Containing operation template (can be ``None``). + + :type: :class:`OperationTemplate` + """ + return relationship.many_to_one(cls, 'operation_template') + + @declared_attr + def execution(cls): + """ + Containing execution (can be ``None``). + + :type: :class:`Execution` + """ + return relationship.many_to_one(cls, 'execution') + + # endregion + + # region foreign keys + + @declared_attr + def service_template_fk(cls): + return relationship.foreign_key('service_template', nullable=True) + + @declared_attr + def service_fk(cls): + return relationship.foreign_key('service', nullable=True) + + @declared_attr + def interface_fk(cls): + return relationship.foreign_key('interface', nullable=True) + + @declared_attr + def operation_fk(cls): + return relationship.foreign_key('operation', nullable=True) + + @declared_attr + def interface_template_fk(cls): + return relationship.foreign_key('interface_template', nullable=True) + + @declared_attr + def operation_template_fk(cls): + return relationship.foreign_key('operation_template', nullable=True) + + @declared_attr + def execution_fk(cls): + return relationship.foreign_key('execution', nullable=True) + + @declared_attr + def task_fk(cls): + return relationship.foreign_key('task', nullable=True) + + # endregion + + +class ConfigurationBase(ParameterMixin): + """ + Configuration parameter. + """ + + __tablename__ = 'configuration' + + # region many_to_one relationships + + @declared_attr + def operation_template(cls): + """ + Containing operation template (can be ``None``). + + :type: :class:`OperationTemplate` + """ + return relationship.many_to_one(cls, 'operation_template') + + @declared_attr + def operation(cls): + """ + Containing operation (can be ``None``). + + :type: :class:`Operation` + """ + return relationship.many_to_one(cls, 'operation') + + # endregion + + # region foreign keys + + @declared_attr + def operation_template_fk(cls): + return relationship.foreign_key('operation_template', nullable=True) + + @declared_attr + def operation_fk(cls): + return relationship.foreign_key('operation', nullable=True) + + # endregion + + +class PropertyBase(ParameterMixin): + """ + Property parameter or declaration for a property parameter. + """ + + __tablename__ = 'property' + + # region many_to_one relationships + + @declared_attr + def node_template(cls): + """ + Containing node template (can be ``None``). + + :type: :class:`NodeTemplate` + """ + return relationship.many_to_one(cls, 'node_template') + + @declared_attr + def group_template(cls): + """ + Containing group template (can be ``None``). + + :type: :class:`GroupTemplate` + """ + return relationship.many_to_one(cls, 'group_template') + + @declared_attr + def policy_template(cls): + """ + Containing policy template (can be ``None``). + + :type: :class:`PolicyTemplate` + """ + return relationship.many_to_one(cls, 'policy_template') + + @declared_attr + def relationship_template(cls): + """ + Containing relationship template (can be ``None``). + + :type: :class:`RelationshipTemplate` + """ + return relationship.many_to_one(cls, 'relationship_template') + + @declared_attr + def capability_template(cls): + """ + Containing capability template (can be ``None``). + + :type: :class:`CapabilityTemplate` + """ + return relationship.many_to_one(cls, 'capability_template') + + @declared_attr + def artifact_template(cls): + """ + Containing artifact template (can be ``None``). + + :type: :class:`ArtifactTemplate` + """ + return relationship.many_to_one(cls, 'artifact_template') + + @declared_attr + def node(cls): + """ + Containing node (can be ``None``). + + :type: :class:`Node` + """ + return relationship.many_to_one(cls, 'node') + + @declared_attr + def group(cls): + """ + Containing group (can be ``None``). + + :type: :class:`Group` + """ + return relationship.many_to_one(cls, 'group') + + @declared_attr + def policy(cls): + """ + Containing policy (can be ``None``). + + :type: :class:`Policy` + """ + return relationship.many_to_one(cls, 'policy') + + @declared_attr + def relationship(cls): + """ + Containing relationship (can be ``None``). + + :type: :class:`Relationship` + """ + return relationship.many_to_one(cls, 'relationship') + + @declared_attr + def capability(cls): + """ + Containing capability (can be ``None``). + + :type: :class:`Capability` + """ + return relationship.many_to_one(cls, 'capability') + + @declared_attr + def artifact(cls): + """ + Containing artifact (can be ``None``). + + :type: :class:`Artifact` + """ + return relationship.many_to_one(cls, 'artifact') + + # endregion + + # region foreign keys + + @declared_attr + def node_template_fk(cls): + return relationship.foreign_key('node_template', nullable=True) + + @declared_attr + def group_template_fk(cls): + return relationship.foreign_key('group_template', nullable=True) + + @declared_attr + def policy_template_fk(cls): + return relationship.foreign_key('policy_template', nullable=True) + + @declared_attr + def relationship_template_fk(cls): + return relationship.foreign_key('relationship_template', nullable=True) + + @declared_attr + def capability_template_fk(cls): + return relationship.foreign_key('capability_template', nullable=True) + + @declared_attr + def artifact_template_fk(cls): + return relationship.foreign_key('artifact_template', nullable=True) + + @declared_attr + def node_fk(cls): + return relationship.foreign_key('node', nullable=True) + + @declared_attr + def group_fk(cls): + return relationship.foreign_key('group', nullable=True) + + @declared_attr + def policy_fk(cls): + return relationship.foreign_key('policy', nullable=True) + + @declared_attr + def relationship_fk(cls): + return relationship.foreign_key('relationship', nullable=True) + + @declared_attr + def capability_fk(cls): + return relationship.foreign_key('capability', nullable=True) + + @declared_attr + def artifact_fk(cls): + return relationship.foreign_key('artifact', nullable=True) + + # endregion + + +class AttributeBase(ParameterMixin): + """ + Attribute parameter or declaration for an attribute parameter. + """ + + __tablename__ = 'attribute' + + # region many_to_one relationships + + @declared_attr + def node_template(cls): + """ + Containing node template (can be ``None``). + + :type: :class:`NodeTemplate` + """ + return relationship.many_to_one(cls, 'node_template') + + @declared_attr + def node(cls): + """ + Containing node (can be ``None``). + + :type: :class:`Node` + """ + return relationship.many_to_one(cls, 'node') + + # endregion + + # region foreign keys + + @declared_attr + def node_template_fk(cls): + """For Attribute many-to-one to NodeTemplate""" + return relationship.foreign_key('node_template', nullable=True) + + @declared_attr + def node_fk(cls): + """For Attribute many-to-one to Node""" + return relationship.foreign_key('node', nullable=True) + + # endregion + + +class TypeBase(InstanceModelMixin): + """ + Type and its children. Can serve as the root for a type hierarchy. + """ + + __tablename__ = 'type' + + __private_fields__ = ('parent_type_fk',) + + variant = Column(Text, nullable=False) + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + _role = Column(Text, name='role') + + # region one_to_one relationships + + @declared_attr + def parent(cls): + """ + Parent type (will be ``None`` for the root of a type hierarchy). + + :type: :class:`Type` + """ + return relationship.one_to_one_self(cls, 'parent_type_fk') + + # endregion + + # region one_to_many relationships + + @declared_attr + def children(cls): + """ + Children. + + :type: [:class:`Type`] + """ + return relationship.one_to_many(cls, other_fk='parent_type_fk', self=True) + + # endregion + + # region foreign keys + + @declared_attr + def parent_type_fk(cls): + """For Type one-to-many to Type""" + return relationship.foreign_key('type', nullable=True) + + # endregion + + @property + def role(self): + def get_role(the_type): + if the_type is None: + return None + elif the_type._role is None: + return get_role(the_type.parent) + return the_type._role + + return get_role(self) + + @role.setter + def role(self, value): + self._role = value + + def is_descendant(self, base_name, name): + base = self.get_descendant(base_name) + if base is not None: + if base.get_descendant(name) is not None: + return True + return False + + def get_descendant(self, name): + if self.name == name: + return self + for child in self.children: + found = child.get_descendant(name) + if found is not None: + return found + return None + + def iter_descendants(self): + for child in self.children: + yield child + for descendant in child.iter_descendants(): + yield descendant + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('role', self.role))) + + @property + def as_raw_all(self): + types = [] + self._append_raw_children(types) + return types + + def _append_raw_children(self, types): + for child in self.children: + raw_child = formatting.as_raw(child) + raw_child['parent'] = self.name + types.append(raw_child) + child._append_raw_children(types) + + @property + def hierarchy(self): + """ + Type hierarchy as a list beginning with this type and ending in the root. + + :type: [:class:`Type`] + """ + return [self] + (self.parent.hierarchy if self.parent else []) + + +class MetadataBase(TemplateModelMixin): + """ + Custom values associated with the service. + + This model is used by both service template and service instance elements. + + :ivar name: name + :vartype name: basestring + :ivar value: value + :vartype value: basestring + """ + + __tablename__ = 'metadata' + + value = Column(Text) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('value', self.value))) diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_instance.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_instance.py new file mode 100644 index 0000000..01c4da9 --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_instance.py @@ -0,0 +1,1695 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ARIA modeling service instance module +""" + +# pylint: disable=too-many-lines, no-self-argument, no-member, abstract-method + +from sqlalchemy import ( + Column, + Text, + Integer, + Enum, + Boolean +) +from sqlalchemy import DateTime +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.orderinglist import ordering_list + +from . import ( + relationship, + types as modeling_types +) +from .mixins import InstanceModelMixin + +from ..utils import ( + collections, + formatting +) + + +class ServiceBase(InstanceModelMixin): + """ + Usually an instance of a :class:`ServiceTemplate` and its many associated templates (node + templates, group templates, policy templates, etc.). However, it can also be created + programmatically. + """ + + __tablename__ = 'service' + + __private_fields__ = ('substitution_fk', + 'service_template_fk') + + # region one_to_one relationships + + @declared_attr + def substitution(cls): + """ + Exposes the entire service as a single node. + + :type: :class:`Substitution` + """ + return relationship.one_to_one(cls, 'substitution', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region one_to_many relationships + + @declared_attr + def outputs(cls): + """ + Output parameters. + + :type: {:obj:`basestring`: :class:`Output`} + """ + return relationship.one_to_many(cls, 'output', dict_key='name') + + @declared_attr + def inputs(cls): + """ + Externally provided parameters. + + :type: {:obj:`basestring`: :class:`Input`} + """ + return relationship.one_to_many(cls, 'input', dict_key='name') + + @declared_attr + def updates(cls): + """ + Service updates. + + :type: [:class:`ServiceUpdate`] + """ + return relationship.one_to_many(cls, 'service_update') + + @declared_attr + def modifications(cls): + """ + Service modifications. + + :type: [:class:`ServiceModification`] + """ + return relationship.one_to_many(cls, 'service_modification') + + @declared_attr + def executions(cls): + """ + Executions. + + :type: [:class:`Execution`] + """ + return relationship.one_to_many(cls, 'execution') + + @declared_attr + def nodes(cls): + """ + Nodes. + + :type: {:obj:`basestring`, :class:`Node`} + """ + return relationship.one_to_many(cls, 'node', dict_key='name') + + @declared_attr + def groups(cls): + """ + Groups. + + :type: {:obj:`basestring`, :class:`Group`} + """ + return relationship.one_to_many(cls, 'group', dict_key='name') + + @declared_attr + def policies(cls): + """ + Policies. + + :type: {:obj:`basestring`, :class:`Policy`} + """ + return relationship.one_to_many(cls, 'policy', dict_key='name') + + @declared_attr + def workflows(cls): + """ + Workflows. + + :type: {:obj:`basestring`, :class:`Operation`} + """ + return relationship.one_to_many(cls, 'operation', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service_template(cls): + """ + Source service template (can be ``None``). + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template') + + # endregion + + # region many_to_many relationships + + @declared_attr + def meta_data(cls): + """ + Associated metadata. + + :type: {:obj:`basestring`, :class:`Metadata`} + """ + # Warning! We cannot use the attr name "metadata" because it's used by SQLAlchemy! + return relationship.many_to_many(cls, 'metadata', dict_key='name') + + @declared_attr + def plugins(cls): + """ + Associated plugins. + + :type: {:obj:`basestring`, :class:`Plugin`} + """ + return relationship.many_to_many(cls, 'plugin', dict_key='name') + + # endregion + + # region association proxies + + @declared_attr + def service_template_name(cls): + return relationship.association_proxy('service_template', 'name', type=':obj:`basestring`') + + # endregion + + # region foreign keys + + @declared_attr + def substitution_fk(cls): + """Service one-to-one to Substitution""" + return relationship.foreign_key('substitution', nullable=True) + + @declared_attr + def service_template_fk(cls): + """For Service many-to-one to ServiceTemplate""" + return relationship.foreign_key('service_template', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + created_at = Column(DateTime, nullable=False, index=True, doc=""" + Creation timestamp. + + :type: :class:`~datetime.datetime` + """) + + updated_at = Column(DateTime, doc=""" + Update timestamp. + + :type: :class:`~datetime.datetime` + """) + + def get_node_by_type(self, type_name): + """ + Finds the first node of a type (or descendent type). + """ + service_template = self.service_template + + if service_template is not None: + node_types = service_template.node_types + if node_types is not None: + for node in self.nodes.itervalues(): + if node_types.is_descendant(type_name, node.type.name): + return node + + return None + + def get_policy_by_type(self, type_name): + """ + Finds the first policy of a type (or descendent type). + """ + service_template = self.service_template + + if service_template is not None: + policy_types = service_template.policy_types + if policy_types is not None: + for policy in self.policies.itervalues(): + if policy_types.is_descendant(type_name, policy.type.name): + return policy + + return None + + @property + def as_raw(self): + return collections.OrderedDict(( + ('description', self.description), + ('metadata', formatting.as_raw_dict(self.meta_data)), + ('nodes', formatting.as_raw_list(self.nodes)), + ('groups', formatting.as_raw_list(self.groups)), + ('policies', formatting.as_raw_list(self.policies)), + ('substitution', formatting.as_raw(self.substitution)), + ('inputs', formatting.as_raw_dict(self.inputs)), + ('outputs', formatting.as_raw_dict(self.outputs)), + ('workflows', formatting.as_raw_list(self.workflows)))) + + +class NodeBase(InstanceModelMixin): + """ + Typed vertex in the service topology. + + Nodes may have zero or more :class:`Relationship` instances to other nodes, together forming + a many-to-many node graph. + + Usually an instance of a :class:`NodeTemplate`. + """ + + __tablename__ = 'node' + + __private_fields__ = ('type_fk', + 'host_fk', + 'service_fk', + 'node_template_fk') + + INITIAL = 'initial' + CREATING = 'creating' + CREATED = 'created' + CONFIGURING = 'configuring' + CONFIGURED = 'configured' + STARTING = 'starting' + STARTED = 'started' + STOPPING = 'stopping' + DELETING = 'deleting' + DELETED = 'deleted' + ERROR = 'error' + + # Note: 'deleted' isn't actually part of the TOSCA spec, since according the description of the + # 'deleting' state: "Node is transitioning from its current state to one where it is deleted and + # its state is no longer tracked by the instance model." However, we prefer to be able to + # retrieve information about deleted nodes, so we chose to add this 'deleted' state to enable us + # to do so. + + STATES = (INITIAL, CREATING, CREATED, CONFIGURING, CONFIGURED, STARTING, STARTED, STOPPING, + DELETING, DELETED, ERROR) + + _OP_TO_STATE = {'create': {'transitional': CREATING, 'finished': CREATED}, + 'configure': {'transitional': CONFIGURING, 'finished': CONFIGURED}, + 'start': {'transitional': STARTING, 'finished': STARTED}, + 'stop': {'transitional': STOPPING, 'finished': CONFIGURED}, + 'delete': {'transitional': DELETING, 'finished': DELETED}} + + # region one_to_one relationships + + @declared_attr + def host(cls): # pylint: disable=method-hidden + """ + Node in which we are hosted (can be ``None``). + + Normally the host node is found by following the relationship graph (relationships with + ``host`` roles) to final nodes (with ``host`` roles). + + :type: :class:`Node` + """ + return relationship.one_to_one_self(cls, 'host_fk') + + # endregion + + # region one_to_many relationships + + @declared_attr + def tasks(cls): + """ + Associated tasks. + + :type: [:class:`Task`] + """ + return relationship.one_to_many(cls, 'task') + + @declared_attr + def interfaces(cls): + """ + Associated interfaces. + + :type: {:obj:`basestring`: :class:`Interface`} + """ + return relationship.one_to_many(cls, 'interface', dict_key='name') + + @declared_attr + def properties(cls): + """ + Associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + @declared_attr + def attributes(cls): + """ + Associated mutable parameters. + + :type: {:obj:`basestring`: :class:`Attribute`} + """ + return relationship.one_to_many(cls, 'attribute', dict_key='name') + + @declared_attr + def artifacts(cls): + """ + Associated artifacts. + + :type: {:obj:`basestring`: :class:`Artifact`} + """ + return relationship.one_to_many(cls, 'artifact', dict_key='name') + + @declared_attr + def capabilities(cls): + """ + Associated exposed capabilities. + + :type: {:obj:`basestring`: :class:`Capability`} + """ + return relationship.one_to_many(cls, 'capability', dict_key='name') + + @declared_attr + def outbound_relationships(cls): + """ + Relationships to other nodes. + + :type: [:class:`Relationship`] + """ + return relationship.one_to_many( + cls, 'relationship', other_fk='source_node_fk', back_populates='source_node', + rel_kwargs=dict( + order_by='Relationship.source_position', + collection_class=ordering_list('source_position', count_from=0) + ) + ) + + @declared_attr + def inbound_relationships(cls): + """ + Relationships from other nodes. + + :type: [:class:`Relationship`] + """ + return relationship.one_to_many( + cls, 'relationship', other_fk='target_node_fk', back_populates='target_node', + rel_kwargs=dict( + order_by='Relationship.target_position', + collection_class=ordering_list('target_position', count_from=0) + ) + ) + + # endregion + + # region many_to_one relationships + + @declared_attr + def service(cls): + """ + Containing service. + + :type: :class:`Service` + """ + return relationship.many_to_one(cls, 'service') + + @declared_attr + def node_template(cls): + """ + Source node template (can be ``None``). + + :type: :class:`NodeTemplate` + """ + return relationship.many_to_one(cls, 'node_template') + + @declared_attr + def type(cls): + """ + Node type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region association proxies + + @declared_attr + def service_name(cls): + return relationship.association_proxy('service', 'name', type=':obj:`basestring`') + + @declared_attr + def node_template_name(cls): + return relationship.association_proxy('node_template', 'name', type=':obj:`basestring`') + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Node many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def host_fk(cls): + """For Node one-to-one to Node""" + return relationship.foreign_key('node', nullable=True) + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Node""" + return relationship.foreign_key('service') + + @declared_attr + def node_template_fk(cls): + """For Node many-to-one to NodeTemplate""" + return relationship.foreign_key('node_template') + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + state = Column(Enum(*STATES, name='node_state'), nullable=False, default=INITIAL, doc=""" + TOSCA state. + + :type: :obj:`basestring` + """) + + version = Column(Integer, default=1, doc=""" + Used by :mod:`aria.storage.instrumentation`. + + :type: :obj:`int` + """) + + __mapper_args__ = {'version_id_col': version} # Enable SQLAlchemy automatic version counting + + @classmethod + def determine_state(cls, op_name, is_transitional): + """ + :returns the state the node should be in as a result of running the operation on this node. + + E.g. if we are running tosca.interfaces.node.lifecycle.Standard.create, then + the resulting state should either 'creating' (if the task just started) or 'created' + (if the task ended). + + If the operation is not a standard TOSCA lifecycle operation, then we return None. + """ + + state_type = 'transitional' if is_transitional else 'finished' + try: + return cls._OP_TO_STATE[op_name][state_type] + except KeyError: + return None + + def is_available(self): + return self.state not in (self.INITIAL, self.DELETED, self.ERROR) + + def get_outbound_relationship_by_name(self, name): + for the_relationship in self.outbound_relationships: + if the_relationship.name == name: + return the_relationship + return None + + def get_inbound_relationship_by_name(self, name): + for the_relationship in self.inbound_relationships: + if the_relationship.name == name: + return the_relationship + return None + + @property + def host_address(self): + if self.host and self.host.attributes: + attribute = self.host.attributes.get('ip') + if attribute is not None: + return attribute.value + return None + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)), + ('attributes', formatting.as_raw_dict(self.properties)), + ('interfaces', formatting.as_raw_list(self.interfaces)), + ('artifacts', formatting.as_raw_list(self.artifacts)), + ('capabilities', formatting.as_raw_list(self.capabilities)), + ('relationships', formatting.as_raw_list(self.outbound_relationships)))) + + +class GroupBase(InstanceModelMixin): + """ + Typed logical container for zero or more :class:`Node` instances. + + Usually an instance of a :class:`GroupTemplate`. + """ + + __tablename__ = 'group' + + __private_fields__ = ('type_fk', + 'service_fk', + 'group_template_fk') + + # region one_to_many relationships + + @declared_attr + def properties(cls): + """ + Associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + @declared_attr + def interfaces(cls): + """ + Associated interfaces. + + :type: {:obj:`basestring`: :class:`Interface`} + """ + return relationship.one_to_many(cls, 'interface', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service(cls): + """ + Containing service. + + :type: :class:`Service` + """ + return relationship.many_to_one(cls, 'service') + + @declared_attr + def group_template(cls): + """ + Source group template (can be ``None``). + + :type: :class:`GroupTemplate` + """ + return relationship.many_to_one(cls, 'group_template') + + @declared_attr + def type(cls): + """ + Group type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region many_to_many relationships + + @declared_attr + def nodes(cls): + """ + Member nodes. + + :type: [:class:`Node`] + """ + return relationship.many_to_many(cls, 'node') + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Group many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Group""" + return relationship.foreign_key('service') + + @declared_attr + def group_template_fk(cls): + """For Group many-to-one to GroupTemplate""" + return relationship.foreign_key('group_template', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('properties', formatting.as_raw_dict(self.properties)), + ('interfaces', formatting.as_raw_list(self.interfaces)))) + + +class PolicyBase(InstanceModelMixin): + """ + Typed set of orchestration hints applied to zero or more :class:`Node` or :class:`Group` + instances. + + Usually an instance of a :class:`PolicyTemplate`. + """ + + __tablename__ = 'policy' + + __private_fields__ = ('type_fk', + 'service_fk', + 'policy_template_fk') + + # region one_to_many relationships + + @declared_attr + def properties(cls): + """ + Associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service(cls): + """ + Containing service. + + :type: :class:`Service` + """ + return relationship.many_to_one(cls, 'service') + + @declared_attr + def policy_template(cls): + """ + Source policy template (can be ``None``). + + :type: :class:`PolicyTemplate` + """ + return relationship.many_to_one(cls, 'policy_template') + + @declared_attr + def type(cls): + """ + Group type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region many_to_many relationships + + @declared_attr + def nodes(cls): + """ + Policy is enacted on these nodes. + + :type: {:obj:`basestring`: :class:`Node`} + """ + return relationship.many_to_many(cls, 'node') + + @declared_attr + def groups(cls): + """ + Policy is enacted on nodes in these groups. + + :type: {:obj:`basestring`: :class:`Group`} + """ + return relationship.many_to_many(cls, 'group') + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Policy many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Policy""" + return relationship.foreign_key('service') + + @declared_attr + def policy_template_fk(cls): + """For Policy many-to-one to PolicyTemplate""" + return relationship.foreign_key('policy_template', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)))) + + +class SubstitutionBase(InstanceModelMixin): + """ + Exposes the entire service as a single node. + + Usually an instance of a :class:`SubstitutionTemplate`. + """ + + __tablename__ = 'substitution' + + __private_fields__ = ('node_type_fk', + 'substitution_template_fk') + + # region one_to_many relationships + + @declared_attr + def mappings(cls): + """ + Map requirement and capabilities to exposed node. + + :type: {:obj:`basestring`: :class:`SubstitutionMapping`} + """ + return relationship.one_to_many(cls, 'substitution_mapping', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service(cls): + """ + Containing service. + + :type: :class:`Service` + """ + return relationship.one_to_one(cls, 'service', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def substitution_template(cls): + """ + Source substitution template (can be ``None``). + + :type: :class:`SubstitutionTemplate` + """ + return relationship.many_to_one(cls, 'substitution_template') + + @declared_attr + def node_type(cls): + """ + Exposed node type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign_keys + + @declared_attr + def node_type_fk(cls): + """For Substitution many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def substitution_template_fk(cls): + """For Substitution many-to-one to SubstitutionTemplate""" + return relationship.foreign_key('substitution_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('node_type_name', self.node_type.name), + ('mappings', formatting.as_raw_dict(self.mappings)))) + + +class SubstitutionMappingBase(InstanceModelMixin): + """ + Used by :class:`Substitution` to map a capability or a requirement to the exposed node. + + The :attr:`name` field should match the capability or requirement template name on the exposed + node's type. + + Only one of :attr:`capability` and :attr:`requirement_template` can be set. If the latter is + set, then :attr:`node` must also be set. + + Usually an instance of a :class:`SubstitutionMappingTemplate`. + """ + + __tablename__ = 'substitution_mapping' + + __private_fields__ = ('substitution_fk', + 'node_fk', + 'capability_fk', + 'requirement_template_fk') + + # region one_to_one relationships + + @declared_attr + def capability(cls): + """ + Capability to expose (can be ``None``). + + :type: :class:`Capability` + """ + return relationship.one_to_one(cls, 'capability', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def requirement_template(cls): + """ + Requirement template to expose (can be ``None``). + + :type: :class:`RequirementTemplate` + """ + return relationship.one_to_one(cls, 'requirement_template', + back_populates=relationship.NO_BACK_POP) + + @declared_attr + def node(cls): + """ + Node for which to expose :attr:`requirement_template` (can be ``None``). + + :type: :class:`Node` + """ + return relationship.one_to_one(cls, 'node', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region many_to_one relationships + + @declared_attr + def substitution(cls): + """ + Containing substitution. + + :type: :class:`Substitution` + """ + return relationship.many_to_one(cls, 'substitution', back_populates='mappings') + + # endregion + + # region foreign keys + + @declared_attr + def substitution_fk(cls): + """For Substitution one-to-many to SubstitutionMapping""" + return relationship.foreign_key('substitution') + + @declared_attr + def capability_fk(cls): + """For Substitution one-to-one to Capability""" + return relationship.foreign_key('capability', nullable=True) + + @declared_attr + def node_fk(cls): + """For Substitution one-to-one to Node""" + return relationship.foreign_key('node', nullable=True) + + @declared_attr + def requirement_template_fk(cls): + """For Substitution one-to-one to RequirementTemplate""" + return relationship.foreign_key('requirement_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name),)) + + +class RelationshipBase(InstanceModelMixin): + """ + Optionally-typed edge in the service topology, connecting a :class:`Node` to a + :class:`Capability` of another node. + + Might be an instance of :class:`RelationshipTemplate` and/or :class:`RequirementTemplate`. + """ + + __tablename__ = 'relationship' + + __private_fields__ = ('type_fk', + 'source_node_fk', + 'target_node_fk', + 'target_capability_fk', + 'requirement_template_fk', + 'relationship_template_fk', + 'target_position', + 'source_position') + + # region one_to_one relationships + + @declared_attr + def target_capability(cls): + """ + Target capability. + + :type: :class:`Capability` + """ + return relationship.one_to_one(cls, 'capability', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region one_to_many relationships + + @declared_attr + def tasks(cls): + """ + Associated tasks. + + :type: [:class:`Task`] + """ + return relationship.one_to_many(cls, 'task') + + @declared_attr + def interfaces(cls): + """ + Associated interfaces. + + :type: {:obj:`basestring`: :class:`Interface`} + """ + return relationship.one_to_many(cls, 'interface', dict_key='name') + + @declared_attr + def properties(cls): + """ + Associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def source_node(cls): + """ + Source node. + + :type: :class:`Node` + """ + return relationship.many_to_one( + cls, 'node', fk='source_node_fk', back_populates='outbound_relationships') + + @declared_attr + def target_node(cls): + """ + Target node. + + :type: :class:`Node` + """ + return relationship.many_to_one( + cls, 'node', fk='target_node_fk', back_populates='inbound_relationships') + + @declared_attr + def relationship_template(cls): + """ + Source relationship template (can be ``None``). + + :type: :class:`RelationshipTemplate` + """ + return relationship.many_to_one(cls, 'relationship_template') + + @declared_attr + def requirement_template(cls): + """ + Source requirement template (can be ``None``). + + :type: :class:`RequirementTemplate` + """ + return relationship.many_to_one(cls, 'requirement_template') + + @declared_attr + def type(cls): + """ + Relationship type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region association proxies + + @declared_attr + def source_node_name(cls): + return relationship.association_proxy('source_node', 'name') + + @declared_attr + def target_node_name(cls): + return relationship.association_proxy('target_node', 'name') + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For Relationship many-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def source_node_fk(cls): + """For Node one-to-many to Relationship""" + return relationship.foreign_key('node') + + @declared_attr + def target_node_fk(cls): + """For Node one-to-many to Relationship""" + return relationship.foreign_key('node') + + @declared_attr + def target_capability_fk(cls): + """For Relationship one-to-one to Capability""" + return relationship.foreign_key('capability', nullable=True) + + @declared_attr + def requirement_template_fk(cls): + """For Relationship many-to-one to RequirementTemplate""" + return relationship.foreign_key('requirement_template', nullable=True) + + @declared_attr + def relationship_template_fk(cls): + """For Relationship many-to-one to RelationshipTemplate""" + return relationship.foreign_key('relationship_template', nullable=True) + + # endregion + + source_position = Column(Integer, doc=""" + Position at source. + + :type: :obj:`int` + """) + + target_position = Column(Integer, doc=""" + Position at target. + + :type: :obj:`int` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('target_node_id', self.target_node.name), + ('type_name', self.type.name + if self.type is not None else None), + ('template_name', self.relationship_template.name + if self.relationship_template is not None else None), + ('properties', formatting.as_raw_dict(self.properties)), + ('interfaces', formatting.as_raw_list(self.interfaces)))) + + +class CapabilityBase(InstanceModelMixin): + """ + Typed attachment serving two purposes: to provide extra properties and attributes to a + :class:`Node`, and to expose targets for :class:`Relationship` instances from other nodes. + + Usually an instance of a :class:`CapabilityTemplate`. + """ + + __tablename__ = 'capability' + + __private_fields__ = ('capability_fk', + 'node_fk', + 'capability_template_fk') + + # region one_to_many relationships + + @declared_attr + def properties(cls): + """ + Associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node(cls): + """ + Containing node. + + :type: :class:`Node` + """ + return relationship.many_to_one(cls, 'node') + + @declared_attr + def capability_template(cls): + """ + Source capability template (can be ``None``). + + :type: :class:`CapabilityTemplate` + """ + return relationship.many_to_one(cls, 'capability_template') + + @declared_attr + def type(cls): + """ + Capability type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Capability many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_fk(cls): + """For Node one-to-many to Capability""" + return relationship.foreign_key('node') + + @declared_attr + def capability_template_fk(cls): + """For Capability many-to-one to CapabilityTemplate""" + return relationship.foreign_key('capability_template', nullable=True) + + # endregion + + min_occurrences = Column(Integer, default=None, doc=""" + Minimum number of requirement matches required. + + :type: :obj:`int` + """) + + max_occurrences = Column(Integer, default=None, doc=""" + Maximum number of requirement matches allowed. + + :type: :obj:`int` + """) + + occurrences = Column(Integer, default=0, doc=""" + Number of requirement matches. + + :type: :obj:`int` + """) + + @property + def has_enough_relationships(self): + if self.min_occurrences is not None: + return self.occurrences >= self.min_occurrences + return True + + def relate(self): + if self.max_occurrences is not None: + if self.occurrences == self.max_occurrences: + return False + self.occurrences += 1 + return True + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)))) + + +class InterfaceBase(InstanceModelMixin): + """ + Typed bundle of :class:`Operation` instances. + + Can be associated with a :class:`Node`, a :class:`Group`, or a :class:`Relationship`. + + Usually an instance of a :class:`InterfaceTemplate`. + """ + + __tablename__ = 'interface' + + __private_fields__ = ('type_fk', + 'node_fk', + 'group_fk', + 'relationship_fk', + 'interface_template_fk') + + # region one_to_many relationships + + @declared_attr + def inputs(cls): + """ + Parameters for all operations of the interface. + + :type: {:obj:`basestring`: :class:`Input`} + """ + return relationship.one_to_many(cls, 'input', dict_key='name') + + @declared_attr + def operations(cls): + """ + Associated operations. + + :type: {:obj:`basestring`: :class:`Operation`} + """ + return relationship.one_to_many(cls, 'operation', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node(cls): + """ + Containing node (can be ``None``). + + :type: :class:`Node` + """ + return relationship.many_to_one(cls, 'node') + + @declared_attr + def group(cls): + """ + Containing group (can be ``None``). + + :type: :class:`Group` + """ + return relationship.many_to_one(cls, 'group') + + @declared_attr + def relationship(cls): + """ + Containing relationship (can be ``None``). + + :type: :class:`Relationship` + """ + return relationship.many_to_one(cls, 'relationship') + + @declared_attr + def interface_template(cls): + """ + Source interface template (can be ``None``). + + :type: :class:`InterfaceTemplate` + """ + return relationship.many_to_one(cls, 'interface_template') + + @declared_attr + def type(cls): + """ + Interface type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Interface many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_fk(cls): + """For Node one-to-many to Interface""" + return relationship.foreign_key('node', nullable=True) + + @declared_attr + def group_fk(cls): + """For Group one-to-many to Interface""" + return relationship.foreign_key('group', nullable=True) + + @declared_attr + def relationship_fk(cls): + """For Relationship one-to-many to Interface""" + return relationship.foreign_key('relationship', nullable=True) + + @declared_attr + def interface_template_fk(cls): + """For Interface many-to-one to InterfaceTemplate""" + return relationship.foreign_key('interface_template', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('inputs', formatting.as_raw_dict(self.inputs)), + ('operations', formatting.as_raw_list(self.operations)))) + + +class OperationBase(InstanceModelMixin): + """ + Entry points to Python functions called as part of a workflow execution. + + The operation signature (its :attr:`name` and its :attr:`inputs`'s names and types) is declared + by the type of the :class:`Interface`, however each operation can provide its own + :attr:`implementation` as well as additional inputs. + + The Python :attr:`function` is usually provided by an associated :class:`Plugin`. Its purpose is + to execute the implementation, providing it with both the operation's and interface's inputs. + The :attr:`arguments` of the function should be set according to the specific signature of the + function. + + Additionally, :attr:`configuration` parameters can be provided as hints to configure the + function's behavior. For example, they can be used to configure remote execution credentials. + + Might be an instance of :class:`OperationTemplate`. + """ + + __tablename__ = 'operation' + + __private_fields__ = ('service_fk', + 'interface_fk', + 'plugin_fk', + 'operation_template_fk') + + # region one_to_one relationships + + @declared_attr + def plugin(cls): + """ + Associated plugin. + + :type: :class:`Plugin` + """ + return relationship.one_to_one(cls, 'plugin', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region one_to_many relationships + + @declared_attr + def inputs(cls): + """ + Parameters provided to the :attr:`implementation`. + + :type: {:obj:`basestring`: :class:`Input`} + """ + return relationship.one_to_many(cls, 'input', dict_key='name') + + @declared_attr + def arguments(cls): + """ + Arguments sent to the Python :attr:`function`. + + :type: {:obj:`basestring`: :class:`Argument`} + """ + return relationship.one_to_many(cls, 'argument', dict_key='name') + + @declared_attr + def configurations(cls): + """ + Configuration parameters for the Python :attr:`function`. + + :type: {:obj:`basestring`: :class:`Configuration`} + """ + return relationship.one_to_many(cls, 'configuration', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service(cls): + """ + Containing service (can be ``None``). For workflow operations. + + :type: :class:`Service` + """ + return relationship.many_to_one(cls, 'service', back_populates='workflows') + + @declared_attr + def interface(cls): + """ + Containing interface (can be ``None``). + + :type: :class:`Interface` + """ + return relationship.many_to_one(cls, 'interface') + + @declared_attr + def operation_template(cls): + """ + Source operation template (can be ``None``). + + :type: :class:`OperationTemplate` + """ + return relationship.many_to_one(cls, 'operation_template') + + # endregion + + # region foreign_keys + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Operation""" + return relationship.foreign_key('service', nullable=True) + + @declared_attr + def interface_fk(cls): + """For Interface one-to-many to Operation""" + return relationship.foreign_key('interface', nullable=True) + + @declared_attr + def plugin_fk(cls): + """For Operation one-to-one to Plugin""" + return relationship.foreign_key('plugin', nullable=True) + + @declared_attr + def operation_template_fk(cls): + """For Operation many-to-one to OperationTemplate""" + return relationship.foreign_key('operation_template', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + relationship_edge = Column(Boolean, doc=""" + When ``True`` specifies that the operation is on the relationship's target edge; ``False`` is + the source edge (only used by operations on relationships) + + :type: :obj:`bool` + """) + + implementation = Column(Text, doc=""" + Implementation (usually the name of an artifact). + + :type: :obj:`basestring` + """) + + dependencies = Column(modeling_types.StrictList(item_cls=basestring), doc=""" + Dependencies (usually names of artifacts). + + :type: [:obj:`basestring`] + """) + + function = Column(Text, doc=""" + Full path to Python function. + + :type: :obj:`basestring` + """) + + executor = Column(Text, doc=""" + Name of executor. + + :type: :obj:`basestring` + """) + + max_attempts = Column(Integer, doc=""" + Maximum number of attempts allowed in case of task failure. + + :type: :obj:`int` + """) + + retry_interval = Column(Integer, doc=""" + Interval between task retry attempts (in seconds). + + :type: :obj:`float` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('implementation', self.implementation), + ('dependencies', self.dependencies), + ('inputs', formatting.as_raw_dict(self.inputs)))) + + +class ArtifactBase(InstanceModelMixin): + """ + Typed file, either provided in a CSAR or downloaded from a repository. + + Usually an instance of :class:`ArtifactTemplate`. + """ + + __tablename__ = 'artifact' + + __private_fields__ = ('type_fk', + 'node_fk', + 'artifact_template_fk') + + # region one_to_many relationships + + @declared_attr + def properties(cls): + """ + Associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node(cls): + """ + Containing node. + + :type: :class:`Node` + """ + return relationship.many_to_one(cls, 'node') + + @declared_attr + def artifact_template(cls): + """ + Source artifact template (can be ``None``). + + :type: :class:`ArtifactTemplate` + """ + return relationship.many_to_one(cls, 'artifact_template') + + @declared_attr + def type(cls): + """ + Artifact type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Artifact many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_fk(cls): + """For Node one-to-many to Artifact""" + return relationship.foreign_key('node') + + @declared_attr + def artifact_template_fk(cls): + """For Artifact many-to-one to ArtifactTemplate""" + return relationship.foreign_key('artifact_template', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + source_path = Column(Text, doc=""" + Source path (in CSAR or repository). + + :type: :obj:`basestring` + """) + + target_path = Column(Text, doc=""" + Path at which to install at destination. + + :type: :obj:`basestring` + """) + + repository_url = Column(Text, doc=""" + Repository URL. + + :type: :obj:`basestring` + """) + + repository_credential = Column(modeling_types.StrictDict(basestring, basestring), doc=""" + Credentials for accessing the repository. + + :type: {:obj:`basestring`, :obj:`basestring`} + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('source_path', self.source_path), + ('target_path', self.target_path), + ('repository_url', self.repository_url), + ('repository_credential', formatting.as_agnostic(self.repository_credential)), + ('properties', formatting.as_raw_dict(self.properties)))) diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_template.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_template.py new file mode 100644 index 0000000..cd0adb4 --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_template.py @@ -0,0 +1,1758 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +ARIA modeling service template module +""" + +# pylint: disable=too-many-lines, no-self-argument, no-member, abstract-method + +from __future__ import absolute_import # so we can import standard 'types' + +from sqlalchemy import ( + Column, + Text, + Integer, + Boolean, + DateTime, + PickleType +) +from sqlalchemy.ext.declarative import declared_attr + +from ..utils import (collections, formatting) +from .mixins import TemplateModelMixin +from . import ( + relationship, + types as modeling_types +) + + +class ServiceTemplateBase(TemplateModelMixin): + """ + Template for creating :class:`Service` instances. + + Usually created by various DSL parsers, such as ARIA's TOSCA extension. However, it can also be + created programmatically. + """ + + __tablename__ = 'service_template' + + __private_fields__ = ('substitution_template_fk', + 'node_type_fk', + 'group_type_fk', + 'policy_type_fk', + 'relationship_type_fk', + 'capability_type_fk', + 'interface_type_fk', + 'artifact_type_fk') + + # region one_to_one relationships + + @declared_attr + def substitution_template(cls): + """ + Exposes an entire service as a single node. + + :type: :class:`SubstitutionTemplate` + """ + return relationship.one_to_one( + cls, 'substitution_template', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def node_types(cls): + """ + Base for the node type hierarchy, + + :type: :class:`Type` + """ + return relationship.one_to_one( + cls, 'type', fk='node_type_fk', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def group_types(cls): + """ + Base for the group type hierarchy, + + :type: :class:`Type` + """ + return relationship.one_to_one( + cls, 'type', fk='group_type_fk', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def policy_types(cls): + """ + Base for the policy type hierarchy, + + :type: :class:`Type` + """ + return relationship.one_to_one( + cls, 'type', fk='policy_type_fk', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def relationship_types(cls): + """ + Base for the relationship type hierarchy, + + :type: :class:`Type` + """ + return relationship.one_to_one( + cls, 'type', fk='relationship_type_fk', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def capability_types(cls): + """ + Base for the capability type hierarchy, + + :type: :class:`Type` + """ + return relationship.one_to_one( + cls, 'type', fk='capability_type_fk', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def interface_types(cls): + """ + Base for the interface type hierarchy, + + :type: :class:`Type` + """ + return relationship.one_to_one( + cls, 'type', fk='interface_type_fk', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def artifact_types(cls): + """ + Base for the artifact type hierarchy, + + :type: :class:`Type` + """ + return relationship.one_to_one( + cls, 'type', fk='artifact_type_fk', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region one_to_many relationships + + @declared_attr + def services(cls): + """ + Instantiated services. + + :type: [:class:`Service`] + """ + return relationship.one_to_many(cls, 'service', dict_key='name') + + @declared_attr + def node_templates(cls): + """ + Templates for creating nodes. + + :type: {:obj:`basestring`, :class:`NodeTemplate`} + """ + return relationship.one_to_many(cls, 'node_template', dict_key='name') + + @declared_attr + def group_templates(cls): + """ + Templates for creating groups. + + :type: {:obj:`basestring`, :class:`GroupTemplate`} + """ + return relationship.one_to_many(cls, 'group_template', dict_key='name') + + @declared_attr + def policy_templates(cls): + """ + Templates for creating policies. + + :type: {:obj:`basestring`, :class:`PolicyTemplate`} + """ + return relationship.one_to_many(cls, 'policy_template', dict_key='name') + + @declared_attr + def workflow_templates(cls): + """ + Templates for creating workflows. + + :type: {:obj:`basestring`, :class:`OperationTemplate`} + """ + return relationship.one_to_many(cls, 'operation_template', dict_key='name') + + @declared_attr + def outputs(cls): + """ + Declarations for output parameters are filled in after service installation. + + :type: {:obj:`basestring`: :class:`Output`} + """ + return relationship.one_to_many(cls, 'output', dict_key='name') + + @declared_attr + def inputs(cls): + """ + Declarations for externally provided parameters. + + :type: {:obj:`basestring`: :class:`Input`} + """ + return relationship.one_to_many(cls, 'input', dict_key='name') + + @declared_attr + def plugin_specifications(cls): + """ + Required plugins for instantiated services. + + :type: {:obj:`basestring`: :class:`PluginSpecification`} + """ + return relationship.one_to_many(cls, 'plugin_specification', dict_key='name') + + # endregion + + # region many_to_many relationships + + @declared_attr + def meta_data(cls): + """ + Associated metadata. + + :type: {:obj:`basestring`: :class:`Metadata`} + """ + # Warning! We cannot use the attr name "metadata" because it's used by SQLAlchemy! + return relationship.many_to_many(cls, 'metadata', dict_key='name') + + # endregion + + # region foreign keys + + @declared_attr + def substitution_template_fk(cls): + """For ServiceTemplate one-to-one to SubstitutionTemplate""" + return relationship.foreign_key('substitution_template', nullable=True) + + @declared_attr + def node_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def group_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def policy_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def relationship_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def capability_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def interface_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def artifact_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + main_file_name = Column(Text, doc=""" + Filename of CSAR or YAML file from which this service template was parsed. + + :type: :obj:`basestring` + """) + + created_at = Column(DateTime, nullable=False, index=True, doc=""" + Creation timestamp. + + :type: :class:`~datetime.datetime` + """) + + updated_at = Column(DateTime, doc=""" + Update timestamp. + + :type: :class:`~datetime.datetime` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('description', self.description), + ('metadata', formatting.as_raw_dict(self.meta_data)), + ('node_templates', formatting.as_raw_list(self.node_templates)), + ('group_templates', formatting.as_raw_list(self.group_templates)), + ('policy_templates', formatting.as_raw_list(self.policy_templates)), + ('substitution_template', formatting.as_raw(self.substitution_template)), + ('inputs', formatting.as_raw_dict(self.inputs)), + ('outputs', formatting.as_raw_dict(self.outputs)), + ('workflow_templates', formatting.as_raw_list(self.workflow_templates)))) + + @property + def types_as_raw(self): + return collections.OrderedDict(( + ('node_types', formatting.as_raw(self.node_types)), + ('group_types', formatting.as_raw(self.group_types)), + ('policy_types', formatting.as_raw(self.policy_types)), + ('relationship_types', formatting.as_raw(self.relationship_types)), + ('capability_types', formatting.as_raw(self.capability_types)), + ('interface_types', formatting.as_raw(self.interface_types)), + ('artifact_types', formatting.as_raw(self.artifact_types)))) + + +class NodeTemplateBase(TemplateModelMixin): + """ + Template for creating zero or more :class:`Node` instances, which are typed vertices in the + service topology. + """ + + __tablename__ = 'node_template' + + __private_fields__ = ('type_fk', + 'service_template_fk') + + # region one_to_many relationships + + @declared_attr + def nodes(cls): + """ + Instantiated nodes. + + :type: [:class:`Node`] + """ + return relationship.one_to_many(cls, 'node') + + @declared_attr + def interface_templates(cls): + """ + Associated interface templates. + + :type: {:obj:`basestring`: :class:`InterfaceTemplate`} + """ + return relationship.one_to_many(cls, 'interface_template', dict_key='name') + + @declared_attr + def artifact_templates(cls): + """ + Associated artifacts. + + :type: {:obj:`basestring`: :class:`ArtifactTemplate`} + """ + return relationship.one_to_many(cls, 'artifact_template', dict_key='name') + + @declared_attr + def capability_templates(cls): + """ + Associated exposed capability templates. + + :type: {:obj:`basestring`: :class:`CapabilityTemplate`} + """ + return relationship.one_to_many(cls, 'capability_template', dict_key='name') + + @declared_attr + def requirement_templates(cls): + """ + Associated potential relationships with other nodes. + + :type: [:class:`RequirementTemplate`] + """ + return relationship.one_to_many(cls, 'requirement_template', other_fk='node_template_fk') + + @declared_attr + def properties(cls): + """ + Declarations for associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + @declared_attr + def attributes(cls): + """ + Declarations for associated mutable parameters. + + :type: {:obj:`basestring`: :class:`Attribute`} + """ + return relationship.one_to_many(cls, 'attribute', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def type(cls): + """ + Node type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def service_template(cls): + """ + Containing service template. + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template') + + # endregion + + # region association proxies + + @declared_attr + def service_template_name(cls): + return relationship.association_proxy('service_template', 'name') + + @declared_attr + def type_name(cls): + return relationship.association_proxy('type', 'name') + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For NodeTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to NodeTemplate""" + return relationship.foreign_key('service_template') + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + directives = Column(PickleType, doc=""" + Directives that apply to this node template. + + :type: [:obj:`basestring`] + """) + + default_instances = Column(Integer, default=1, doc=""" + Default number nodes that will appear in the service. + + :type: :obj:`int` + """) + + min_instances = Column(Integer, default=0, doc=""" + Minimum number nodes that will appear in the service. + + :type: :obj:`int` + """) + + max_instances = Column(Integer, default=None, doc=""" + Maximum number nodes that will appear in the service. + + :type: :obj:`int` + """) + + target_node_template_constraints = Column(PickleType, doc=""" + Constraints for filtering relationship targets. + + :type: [:class:`NodeTemplateConstraint`] + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)), + ('attributes', formatting.as_raw_dict(self.properties)), + ('interface_templates', formatting.as_raw_list(self.interface_templates)), + ('artifact_templates', formatting.as_raw_list(self.artifact_templates)), + ('capability_templates', formatting.as_raw_list(self.capability_templates)), + ('requirement_templates', formatting.as_raw_list(self.requirement_templates)))) + + def is_target_node_template_valid(self, target_node_template): + """ + Checks if ``target_node_template`` matches all our ``target_node_template_constraints``. + """ + + if self.target_node_template_constraints: + for node_template_constraint in self.target_node_template_constraints: + if not node_template_constraint.matches(self, target_node_template): + return False + return True + + @property + def _next_index(self): + """ + Next available node index. + + :returns: node index + :rtype: int + """ + + max_index = 0 + if self.nodes: + max_index = max(int(n.name.rsplit('_', 1)[-1]) for n in self.nodes) + return max_index + 1 + + @property + def _next_name(self): + """ + Next available node name. + + :returns: node name + :rtype: basestring + """ + + return '{name}_{index}'.format(name=self.name, index=self._next_index) + + @property + def scaling(self): + scaling = {} + + def extract_property(properties, name): + if name in scaling: + return + prop = properties.get(name) + if (prop is not None) and (prop.type_name == 'integer') and (prop.value is not None): + scaling[name] = prop.value + + def extract_properties(properties): + extract_property(properties, 'min_instances') + extract_property(properties, 'max_instances') + extract_property(properties, 'default_instances') + + # From our scaling capabilities + for capability_template in self.capability_templates.itervalues(): + if capability_template.type.role == 'scaling': + extract_properties(capability_template.properties) + + # From service scaling policies + for policy_template in self.service_template.policy_templates.itervalues(): + if policy_template.type.role == 'scaling': + if policy_template.is_for_node_template(self.name): + extract_properties(policy_template.properties) + + # Defaults + scaling.setdefault('min_instances', 0) + scaling.setdefault('max_instances', 1) + scaling.setdefault('default_instances', 1) + + return scaling + + +class GroupTemplateBase(TemplateModelMixin): + """ + Template for creating a :class:`Group` instance, which is a typed logical container for zero or + more :class:`Node` instances. + """ + + __tablename__ = 'group_template' + + __private_fields__ = ('type_fk', + 'service_template_fk') + + # region one_to_many relationships + + @declared_attr + def groups(cls): + """ + Instantiated groups. + + :type: [:class:`Group`] + """ + return relationship.one_to_many(cls, 'group') + + @declared_attr + def interface_templates(cls): + """ + Associated interface templates. + + :type: {:obj:`basestring`: :class:`InterfaceTemplate`} + """ + return relationship.one_to_many(cls, 'interface_template', dict_key='name') + + @declared_attr + def properties(cls): + """ + Declarations for associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service_template(cls): + """ + Containing service template. + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template') + + @declared_attr + def type(cls): + """ + Group type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region many_to_many relationships + + @declared_attr + def node_templates(cls): + """ + Nodes instantiated by these templates will be members of the group. + + :type: [:class:`NodeTemplate`] + """ + return relationship.many_to_many(cls, 'node_template') + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For GroupTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to GroupTemplate""" + return relationship.foreign_key('service_template') + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)), + ('interface_templates', formatting.as_raw_list(self.interface_templates)))) + + def contains_node_template(self, name): + for node_template in self.node_templates: + if node_template.name == name: + return True + return False + + +class PolicyTemplateBase(TemplateModelMixin): + """ + Template for creating a :class:`Policy` instance, which is a typed set of orchestration hints + applied to zero or more :class:`Node` or :class:`Group` instances. + """ + + __tablename__ = 'policy_template' + + __private_fields__ = ('type_fk', + 'service_template_fk') + + # region one_to_many relationships + + @declared_attr + def policies(cls): + """ + Instantiated policies. + + :type: [:class:`Policy`] + """ + return relationship.one_to_many(cls, 'policy') + + @declared_attr + def properties(cls): + """ + Declarations for associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service_template(cls): + """ + Containing service template. + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template') + + @declared_attr + def type(cls): + """ + Policy type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region many_to_many relationships + + @declared_attr + def node_templates(cls): + """ + Policy will be enacted on all nodes instantiated by these templates. + + :type: {:obj:`basestring`: :class:`NodeTemplate`} + """ + return relationship.many_to_many(cls, 'node_template') + + @declared_attr + def group_templates(cls): + """ + Policy will be enacted on all nodes in all groups instantiated by these templates. + + :type: {:obj:`basestring`: :class:`GroupTemplate`} + """ + return relationship.many_to_many(cls, 'group_template') + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For PolicyTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to PolicyTemplate""" + return relationship.foreign_key('service_template') + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)))) + + def is_for_node_template(self, name): + for node_template in self.node_templates: + if node_template.name == name: + return True + for group_template in self.group_templates: + if group_template.contains_node_template(name): + return True + return False + + def is_for_group_template(self, name): + for group_template in self.group_templates: + if group_template.name == name: + return True + return False + + +class SubstitutionTemplateBase(TemplateModelMixin): + """ + Template for creating a :class:`Substitution` instance, which exposes an entire instantiated + service as a single node. + """ + + __tablename__ = 'substitution_template' + + __private_fields__ = ('node_type_fk',) + + # region one_to_many relationships + + @declared_attr + def substitutions(cls): + """ + Instantiated substitutions. + + :type: [:class:`Substitution`] + """ + return relationship.one_to_many(cls, 'substitution') + + @declared_attr + def mappings(cls): + """ + Map requirement and capabilities to exposed node. + + :type: {:obj:`basestring`: :class:`SubstitutionTemplateMapping`} + """ + return relationship.one_to_many(cls, 'substitution_template_mapping', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node_type(cls): + """ + Exposed node type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign keys + + @declared_attr + def node_type_fk(cls): + """For SubstitutionTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('node_type_name', self.node_type.name), + ('mappings', formatting.as_raw_dict(self.mappings)))) + + +class SubstitutionTemplateMappingBase(TemplateModelMixin): + """ + Used by :class:`SubstitutionTemplate` to map a capability template or a requirement template to + the exposed node. + + The :attr:`name` field should match the capability or requirement name on the exposed node's + type. + + Only one of :attr:`capability_template` and :attr:`requirement_template` can be set. + """ + + __tablename__ = 'substitution_template_mapping' + + __private_fields__ = ('substitution_template_fk', + 'capability_template_fk', + 'requirement_template_fk') + + # region one_to_one relationships + + @declared_attr + def capability_template(cls): + """ + Capability template to expose (can be ``None``). + + :type: :class:`CapabilityTemplate` + """ + return relationship.one_to_one( + cls, 'capability_template', back_populates=relationship.NO_BACK_POP) + + @declared_attr + def requirement_template(cls): + """ + Requirement template to expose (can be ``None``). + + :type: :class:`RequirementTemplate` + """ + return relationship.one_to_one( + cls, 'requirement_template', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region many_to_one relationships + + @declared_attr + def substitution_template(cls): + """ + Containing substitution template. + + :type: :class:`SubstitutionTemplate` + """ + return relationship.many_to_one(cls, 'substitution_template', back_populates='mappings') + + # endregion + + # region foreign keys + + @declared_attr + def substitution_template_fk(cls): + """For SubstitutionTemplate one-to-many to SubstitutionTemplateMapping""" + return relationship.foreign_key('substitution_template') + + @declared_attr + def capability_template_fk(cls): + """For SubstitutionTemplate one-to-one to CapabilityTemplate""" + return relationship.foreign_key('capability_template', nullable=True) + + @declared_attr + def requirement_template_fk(cls): + """For SubstitutionTemplate one-to-one to RequirementTemplate""" + return relationship.foreign_key('requirement_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name),)) + + +class RequirementTemplateBase(TemplateModelMixin): + """ + Template for creating :class:`Relationship` instances, which are optionally-typed edges in the + service topology, connecting a :class:`Node` to a :class:`Capability` of another node. + + Note that there is no equivalent "Requirement" instance model. Instead, during instantiation a + requirement template is matched with a capability and a :class:`Relationship` is instantiated. + + A requirement template *must* target a :class:`CapabilityType` or a capability name. It can + optionally target a specific :class:`NodeType` or :class:`NodeTemplate`. + + Requirement templates may optionally contain a :class:`RelationshipTemplate`. If they do not, + a :class:`Relationship` will be instantiated with default values. + """ + + __tablename__ = 'requirement_template' + + __private_fields__ = ('target_capability_type_fk', + 'target_node_template_fk', + 'target_node_type_fk', + 'relationship_template_fk', + 'node_template_fk') + + # region one_to_one relationships + + @declared_attr + def target_capability_type(cls): + """ + Target capability type. + + :type: :class:`CapabilityType` + """ + return relationship.one_to_one(cls, + 'type', + fk='target_capability_type_fk', + back_populates=relationship.NO_BACK_POP) + + @declared_attr + def target_node_template(cls): + """ + Target node template (can be ``None``). + + :type: :class:`NodeTemplate` + """ + return relationship.one_to_one(cls, + 'node_template', + fk='target_node_template_fk', + back_populates=relationship.NO_BACK_POP) + + @declared_attr + def relationship_template(cls): + """ + Associated relationship template (can be ``None``). + + :type: :class:`RelationshipTemplate` + """ + return relationship.one_to_one(cls, 'relationship_template') + + # endregion + + # region one_to_many relationships + + @declared_attr + def relationships(cls): + """ + Instantiated relationships. + + :type: [:class:`Relationship`] + """ + return relationship.one_to_many(cls, 'relationship') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node_template(cls): + """ + Containing node template. + + :type: :class:`NodeTemplate` + """ + return relationship.many_to_one(cls, 'node_template', fk='node_template_fk') + + @declared_attr + def target_node_type(cls): + """ + Target node type (can be ``None``). + + :type: :class:`Type` + """ + return relationship.many_to_one( + cls, 'type', fk='target_node_type_fk', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign keys + + @declared_attr + def target_node_type_fk(cls): + """For RequirementTemplate many-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def target_node_template_fk(cls): + """For RequirementTemplate one-to-one to NodeTemplate""" + return relationship.foreign_key('node_template', nullable=True) + + @declared_attr + def target_capability_type_fk(cls): + """For RequirementTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to RequirementTemplate""" + return relationship.foreign_key('node_template') + + @declared_attr + def relationship_template_fk(cls): + """For RequirementTemplate one-to-one to RelationshipTemplate""" + return relationship.foreign_key('relationship_template', nullable=True) + + # endregion + + target_capability_name = Column(Text, doc=""" + Target capability name in node template or node type (can be ``None``). + + :type: :obj:`basestring` + """) + + target_node_template_constraints = Column(PickleType, doc=""" + Constraints for filtering relationship targets. + + :type: [:class:`NodeTemplateConstraint`] + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('target_node_type_name', self.target_node_type.name + if self.target_node_type is not None else None), + ('target_node_template_name', self.target_node_template.name + if self.target_node_template is not None else None), + ('target_capability_type_name', self.target_capability_type.name + if self.target_capability_type is not None else None), + ('target_capability_name', self.target_capability_name), + ('relationship_template', formatting.as_raw(self.relationship_template)))) + + +class RelationshipTemplateBase(TemplateModelMixin): + """ + Optional addition to a :class:`RequirementTemplate`. + + Note that a relationship template here is not exactly equivalent to a relationship template + entity in TOSCA. For example, a TOSCA requirement specifying a relationship type rather than a + relationship template would still be represented here as a relationship template. + """ + + __tablename__ = 'relationship_template' + + __private_fields__ = ('type_fk',) + + # region one_to_many relationships + + @declared_attr + def relationships(cls): + """ + Instantiated relationships. + + :type: [:class:`Relationship`] + """ + return relationship.one_to_many(cls, 'relationship') + + @declared_attr + def interface_templates(cls): + """ + Associated interface templates. + + :type: {:obj:`basestring`: :class:`InterfaceTemplate`} + """ + return relationship.one_to_many(cls, 'interface_template', dict_key='name') + + @declared_attr + def properties(cls): + """ + Declarations for associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def type(cls): + """ + Relationship type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For RelationshipTemplate many-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('type_name', self.type.name if self.type is not None else None), + ('name', self.name), + ('description', self.description), + ('properties', formatting.as_raw_dict(self.properties)), + ('interface_templates', formatting.as_raw_list(self.interface_templates)))) + + +class CapabilityTemplateBase(TemplateModelMixin): + """ + Template for creating :class:`Capability` instances, typed attachments which serve two purposes: + to provide extra properties and attributes to :class:`Node` instances, and to expose targets for + :class:`Relationship` instances from other nodes. + """ + + __tablename__ = 'capability_template' + + __private_fields__ = ('type_fk', + 'node_template_fk') + + # region one_to_many relationships + + @declared_attr + def capabilities(cls): + """ + Instantiated capabilities. + + :type: [:class:`Capability`] + """ + return relationship.one_to_many(cls, 'capability') + + @declared_attr + def properties(cls): + """ + Declarations for associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node_template(cls): + """ + Containing node template. + + :type: :class:`NodeTemplate` + """ + return relationship.many_to_one(cls, 'node_template') + + @declared_attr + def type(cls): + """ + Capability type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region many_to_many relationships + + @declared_attr + def valid_source_node_types(cls): + """ + Reject requirements that are not from these node types. + + :type: [:class:`Type`] + """ + return relationship.many_to_many(cls, 'type', prefix='valid_sources') + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For CapabilityTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to CapabilityTemplate""" + return relationship.foreign_key('node_template') + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + min_occurrences = Column(Integer, default=None, doc=""" + Minimum number of requirement matches required. + + :type: :obj:`int` + """) + + max_occurrences = Column(Integer, default=None, doc=""" + Maximum number of requirement matches allowed. + + :type: :obj:`int` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('min_occurrences', self.min_occurrences), + ('max_occurrences', self.max_occurrences), + ('valid_source_node_types', [v.name for v in self.valid_source_node_types]), + ('properties', formatting.as_raw_dict(self.properties)))) + + +class InterfaceTemplateBase(TemplateModelMixin): + """ + Template for creating :class:`Interface` instances, which are typed bundles of + :class:`Operation` instances. + + Can be associated with a :class:`NodeTemplate`, a :class:`GroupTemplate`, or a + :class:`RelationshipTemplate`. + """ + + __tablename__ = 'interface_template' + + __private_fields__ = ('type_fk', + 'node_template_fk', + 'group_template_fk', + 'relationship_template_fk') + + # region one_to_many relationships + + @declared_attr + def inputs(cls): + """ + Declarations for externally provided parameters that can be used by all operations of the + interface. + + :type: {:obj:`basestring`: :class:`Input`} + """ + return relationship.one_to_many(cls, 'input', dict_key='name') + + @declared_attr + def interfaces(cls): + """ + Instantiated interfaces. + + :type: [:class:`Interface`] + """ + return relationship.one_to_many(cls, 'interface') + + @declared_attr + def operation_templates(cls): + """ + Associated operation templates. + + :type: {:obj:`basestring`: :class:`OperationTemplate`} + """ + return relationship.one_to_many(cls, 'operation_template', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node_template(cls): + """ + Containing node template (can be ``None``). + + :type: :class:`NodeTemplate` + """ + return relationship.many_to_one(cls, 'node_template') + + @declared_attr + def group_template(cls): + """ + Containing group template (can be ``None``). + + :type: :class:`GroupTemplate` + """ + return relationship.many_to_one(cls, 'group_template') + + @declared_attr + def relationship_template(cls): + """ + Containing relationship template (can be ``None``). + + :type: :class:`RelationshipTemplate` + """ + return relationship.many_to_one(cls, 'relationship_template') + + @declared_attr + def type(cls): + """ + Interface type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For InterfaceTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to InterfaceTemplate""" + return relationship.foreign_key('node_template', nullable=True) + + @declared_attr + def group_template_fk(cls): + """For GroupTemplate one-to-many to InterfaceTemplate""" + return relationship.foreign_key('group_template', nullable=True) + + @declared_attr + def relationship_template_fk(cls): + """For RelationshipTemplate one-to-many to InterfaceTemplate""" + return relationship.foreign_key('relationship_template', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('inputs', formatting.as_raw_dict(self.inputs)), # pylint: disable=no-member + # TODO fix self.properties reference + ('operation_templates', formatting.as_raw_list(self.operation_templates)))) + + +class OperationTemplateBase(TemplateModelMixin): + """ + Template for creating :class:`Operation` instances, which are entry points to Python functions + called as part of a workflow execution. + """ + + __tablename__ = 'operation_template' + + __private_fields__ = ('service_template_fk', + 'interface_template_fk', + 'plugin_fk') + + # region one_to_one relationships + + @declared_attr + def plugin_specification(cls): + """ + Associated plugin specification. + + :type: :class:`PluginSpecification` + """ + return relationship.one_to_one( + cls, 'plugin_specification', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region one_to_many relationships + + @declared_attr + def operations(cls): + """ + Instantiated operations. + + :type: [:class:`Operation`] + """ + return relationship.one_to_many(cls, 'operation') + + @declared_attr + def inputs(cls): + """ + Declarations for parameters provided to the :attr:`implementation`. + + :type: {:obj:`basestring`: :class:`Input`} + """ + return relationship.one_to_many(cls, 'input', dict_key='name') + + @declared_attr + def configurations(cls): + """ + Configuration parameters for the operation instance Python :attr:`function`. + + :type: {:obj:`basestring`: :class:`Configuration`} + """ + return relationship.one_to_many(cls, 'configuration', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def service_template(cls): + """ + Containing service template (can be ``None``). For workflow operation templates. + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template', + back_populates='workflow_templates') + + @declared_attr + def interface_template(cls): + """ + Containing interface template (can be ``None``). + + :type: :class:`InterfaceTemplate` + """ + return relationship.many_to_one(cls, 'interface_template') + + # endregion + + # region foreign keys + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to OperationTemplate""" + return relationship.foreign_key('service_template', nullable=True) + + @declared_attr + def interface_template_fk(cls): + """For InterfaceTemplate one-to-many to OperationTemplate""" + return relationship.foreign_key('interface_template', nullable=True) + + @declared_attr + def plugin_specification_fk(cls): + """For OperationTemplate one-to-one to PluginSpecification""" + return relationship.foreign_key('plugin_specification', nullable=True) + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + relationship_edge = Column(Boolean, doc=""" + When ``True`` specifies that the operation is on the relationship's target edge; ``False`` is + the source edge (only used by operations on relationships) + + :type: :obj:`bool` + """) + + implementation = Column(Text, doc=""" + Implementation (usually the name of an artifact). + + :type: :obj:`basestring` + """) + + dependencies = Column(modeling_types.StrictList(item_cls=basestring), doc=""" + Dependencies (usually names of artifacts). + + :type: [:obj:`basestring`] + """) + + function = Column(Text, doc=""" + Full path to Python function. + + :type: :obj:`basestring` + """) + + executor = Column(Text, doc=""" + Name of executor. + + :type: :obj:`basestring` + """) + + max_attempts = Column(Integer, doc=""" + Maximum number of attempts allowed in case of task failure. + + :type: :obj:`int` + """) + + retry_interval = Column(Integer, doc=""" + Interval between task retry attemps (in seconds). + + :type: :obj:`float` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('implementation', self.implementation), + ('dependencies', self.dependencies), + ('inputs', formatting.as_raw_dict(self.inputs)))) + + +class ArtifactTemplateBase(TemplateModelMixin): + """ + Template for creating an :class:`Artifact` instance, which is a typed file, either provided in a + CSAR or downloaded from a repository. + """ + + __tablename__ = 'artifact_template' + + __private_fields__ = ('type_fk', + 'node_template_fk') + + # region one_to_many relationships + + @declared_attr + def artifacts(cls): + """ + Instantiated artifacts. + + :type: [:class:`Artifact`] + """ + return relationship.one_to_many(cls, 'artifact') + + @declared_attr + def properties(cls): + """ + Declarations for associated immutable parameters. + + :type: {:obj:`basestring`: :class:`Property`} + """ + return relationship.one_to_many(cls, 'property', dict_key='name') + + # endregion + + # region many_to_one relationships + + @declared_attr + def node_template(cls): + """ + Containing node template. + + :type: :class:`NodeTemplate` + """ + return relationship.many_to_one(cls, 'node_template') + + @declared_attr + def type(cls): + """ + Artifact type. + + :type: :class:`Type` + """ + return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For ArtifactTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to ArtifactTemplate""" + return relationship.foreign_key('node_template') + + # endregion + + description = Column(Text, doc=""" + Human-readable description. + + :type: :obj:`basestring` + """) + + source_path = Column(Text, doc=""" + Source path (in CSAR or repository). + + :type: :obj:`basestring` + """) + + target_path = Column(Text, doc=""" + Path at which to install at destination. + + :type: :obj:`basestring` + """) + + repository_url = Column(Text, doc=""" + Repository URL. + + :type: :obj:`basestring` + """) + + repository_credential = Column(modeling_types.StrictDict(basestring, basestring), doc=""" + Credentials for accessing the repository. + + :type: {:obj:`basestring`, :obj:`basestring`} + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('source_path', self.source_path), + ('target_path', self.target_path), + ('repository_url', self.repository_url), + ('repository_credential', formatting.as_agnostic(self.repository_credential)), + ('properties', formatting.as_raw_dict(self.properties)))) + + +class PluginSpecificationBase(TemplateModelMixin): + """ + Requirement for a :class:`Plugin`. + + The actual plugin to be selected depends on those currently installed in ARIA. + """ + + __tablename__ = 'plugin_specification' + + __private_fields__ = ('service_template_fk', + 'plugin_fk') + + # region many_to_one relationships + + @declared_attr + def service_template(cls): + """ + Containing service template. + + :type: :class:`ServiceTemplate` + """ + return relationship.many_to_one(cls, 'service_template') + + @declared_attr + def plugin(cls): # pylint: disable=method-hidden + """ + Matched plugin. + + :type: :class:`Plugin` + """ + return relationship.many_to_one(cls, 'plugin', back_populates=relationship.NO_BACK_POP) + + # endregion + + # region foreign keys + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to PluginSpecification""" + return relationship.foreign_key('service_template', nullable=True) + + @declared_attr + def plugin_fk(cls): + """For PluginSpecification many-to-one to Plugin""" + return relationship.foreign_key('plugin', nullable=True) + + # endregion + + version = Column(Text, doc=""" + Minimum plugin version. + + :type: :obj:`basestring` + """) + + enabled = Column(Boolean, nullable=False, default=True, doc=""" + Whether the plugin is enabled. + + :type: :obj:`bool` + """) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('version', self.version), + ('enabled', self.enabled))) diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/types.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/types.py new file mode 100644 index 0000000..38240fa --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/types.py @@ -0,0 +1,318 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Allows JSON-serializable collections to be used as SQLAlchemy column types. +""" + +import json +from collections import namedtuple + +from sqlalchemy import ( + TypeDecorator, + VARCHAR, + event +) +from sqlalchemy.ext import mutable +from ruamel import yaml + +from . import exceptions + + +class _MutableType(TypeDecorator): + """ + Dict representation of type. + """ + @property + def python_type(self): + raise NotImplementedError + + def process_literal_param(self, value, dialect): + pass + + impl = VARCHAR + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value) + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value + + +class Dict(_MutableType): + """ + JSON-serializable dict type for SQLAlchemy columns. + """ + @property + def python_type(self): + return dict + + +class List(_MutableType): + """ + JSON-serializable list type for SQLAlchemy columns. + """ + @property + def python_type(self): + return list + + +class _StrictDictMixin(object): + + @classmethod + def coerce(cls, key, value): + """ + Convert plain dictionaries to MutableDict. + """ + try: + if not isinstance(value, cls): + if isinstance(value, dict): + for k, v in value.items(): + cls._assert_strict_key(k) + cls._assert_strict_value(v) + return cls(value) + return mutable.MutableDict.coerce(key, value) + else: + return value + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce to MutableDict', cause=e) + + def __setitem__(self, key, value): + self._assert_strict_key(key) + self._assert_strict_value(value) + super(_StrictDictMixin, self).__setitem__(key, value) + + def setdefault(self, key, value): + self._assert_strict_key(key) + self._assert_strict_value(value) + super(_StrictDictMixin, self).setdefault(key, value) + + def update(self, *args, **kwargs): + for k, v in kwargs.items(): + self._assert_strict_key(k) + self._assert_strict_value(v) + super(_StrictDictMixin, self).update(*args, **kwargs) + + @classmethod + def _assert_strict_key(cls, key): + if cls._key_cls is not None and not isinstance(key, cls._key_cls): + raise exceptions.ValueFormatException('key type was set strictly to {0}, but was {1}' + .format(cls._key_cls, type(key))) + + @classmethod + def _assert_strict_value(cls, value): + if cls._value_cls is not None and not isinstance(value, cls._value_cls): + raise exceptions.ValueFormatException('value type was set strictly to {0}, but was {1}' + .format(cls._value_cls, type(value))) + + +class _MutableDict(mutable.MutableDict): + """ + Enables tracking for dict values. + """ + + @classmethod + def coerce(cls, key, value): + """ + Convert plain dictionaries to MutableDict. + """ + try: + return mutable.MutableDict.coerce(key, value) + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce value', cause=e) + + +class _StrictListMixin(object): + + @classmethod + def coerce(cls, key, value): + "Convert plain dictionaries to MutableDict." + try: + if not isinstance(value, cls): + if isinstance(value, list): + for item in value: + cls._assert_item(item) + return cls(value) + return mutable.MutableList.coerce(key, value) + else: + return value + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce to MutableDict', cause=e) + + def __setitem__(self, index, value): + """ + Detect list set events and emit change events. + """ + self._assert_item(value) + super(_StrictListMixin, self).__setitem__(index, value) + + def append(self, item): + self._assert_item(item) + super(_StrictListMixin, self).append(item) + + def extend(self, item): + self._assert_item(item) + super(_StrictListMixin, self).extend(item) + + def insert(self, index, item): + self._assert_item(item) + super(_StrictListMixin, self).insert(index, item) + + @classmethod + def _assert_item(cls, item): + if cls._item_cls is not None and not isinstance(item, cls._item_cls): + raise exceptions.ValueFormatException('key type was set strictly to {0}, but was {1}' + .format(cls._item_cls, type(item))) + + +class _MutableList(mutable.MutableList): + + @classmethod + def coerce(cls, key, value): + """ + Convert plain dictionaries to MutableDict. + """ + try: + return mutable.MutableList.coerce(key, value) + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce to MutableDict', cause=e) + + +_StrictDictID = namedtuple('_StrictDictID', 'key_cls, value_cls') +_StrictValue = namedtuple('_StrictValue', 'type_cls, listener_cls') + +class _StrictDict(object): + """ + This entire class functions as a factory for strict dicts and their listeners. No type class, + and no listener type class is created more than once. If a relevant type class exists it is + returned. + """ + _strict_map = {} + + def __call__(self, key_cls=None, value_cls=None): + strict_dict_map_key = _StrictDictID(key_cls=key_cls, value_cls=value_cls) + if strict_dict_map_key not in self._strict_map: + key_cls_name = getattr(key_cls, '__name__', str(key_cls)) + value_cls_name = getattr(value_cls, '__name__', str(value_cls)) + # Creating the type class itself. this class would be returned (used by the SQLAlchemy + # Column). + strict_dict_cls = type( + 'StrictDict_{0}_{1}'.format(key_cls_name, value_cls_name), + (Dict, ), + {} + ) + # Creating the type listening class. + # The new class inherits from both the _MutableDict class and the _StrictDictMixin, + # while setting the necessary _key_cls and _value_cls as class attributes. + listener_cls = type( + 'StrictMutableDict_{0}_{1}'.format(key_cls_name, value_cls_name), + (_StrictDictMixin, _MutableDict), + {'_key_cls': key_cls, '_value_cls': value_cls} + ) + yaml.representer.RoundTripRepresenter.add_representer( + listener_cls, yaml.representer.RoundTripRepresenter.represent_list) + self._strict_map[strict_dict_map_key] = _StrictValue(type_cls=strict_dict_cls, + listener_cls=listener_cls) + + return self._strict_map[strict_dict_map_key].type_cls + + +StrictDict = _StrictDict() +""" +JSON-serializable strict dict type for SQLAlchemy columns. + +:param key_cls: +:param value_cls: +""" + + +class _StrictList(object): + """ + This entire class functions as a factory for strict lists and their listeners. No type class, + and no listener type class is created more than once. If a relevant type class exists it is + returned. + """ + _strict_map = {} + + def __call__(self, item_cls=None): + + if item_cls not in self._strict_map: + item_cls_name = getattr(item_cls, '__name__', str(item_cls)) + # Creating the type class itself. this class would be returned (used by the SQLAlchemy + # Column). + strict_list_cls = type( + 'StrictList_{0}'.format(item_cls_name), + (List, ), + {} + ) + # Creating the type listening class. + # The new class inherits from both the _MutableList class and the _StrictListMixin, + # while setting the necessary _item_cls as class attribute. + listener_cls = type( + 'StrictMutableList_{0}'.format(item_cls_name), + (_StrictListMixin, _MutableList), + {'_item_cls': item_cls} + ) + yaml.representer.RoundTripRepresenter.add_representer( + listener_cls, yaml.representer.RoundTripRepresenter.represent_list) + self._strict_map[item_cls] = _StrictValue(type_cls=strict_list_cls, + listener_cls=listener_cls) + + return self._strict_map[item_cls].type_cls + + +StrictList = _StrictList() +""" +JSON-serializable strict list type for SQLAlchemy columns. + +:param item_cls: +""" + + +def _mutable_association_listener(mapper, cls): + strict_dict_type_to_listener = \ + dict((v.type_cls, v.listener_cls) for v in _StrictDict._strict_map.itervalues()) + + strict_list_type_to_listener = \ + dict((v.type_cls, v.listener_cls) for v in _StrictList._strict_map.itervalues()) + + for prop in mapper.column_attrs: + column_type = prop.columns[0].type + # Dict Listeners + if type(column_type) in strict_dict_type_to_listener: # pylint: disable=unidiomatic-typecheck + strict_dict_type_to_listener[type(column_type)].associate_with_attribute( + getattr(cls, prop.key)) + elif isinstance(column_type, Dict): + _MutableDict.associate_with_attribute(getattr(cls, prop.key)) + + # List Listeners + if type(column_type) in strict_list_type_to_listener: # pylint: disable=unidiomatic-typecheck + strict_list_type_to_listener[type(column_type)].associate_with_attribute( + getattr(cls, prop.key)) + elif isinstance(column_type, List): + _MutableList.associate_with_attribute(getattr(cls, prop.key)) + + +_LISTENER_ARGS = (mutable.mapper, 'mapper_configured', _mutable_association_listener) + + +def _register_mutable_association_listener(): + event.listen(*_LISTENER_ARGS) + +_register_mutable_association_listener() diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/utils.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/utils.py new file mode 100644 index 0000000..491b71a --- /dev/null +++ b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/utils.py @@ -0,0 +1,185 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Miscellaneous modeling utilities. +""" + +import os +from json import JSONEncoder +from StringIO import StringIO + +from . import exceptions +from ..utils.type import validate_value_type +from ..utils.collections import OrderedDict +from ..utils.formatting import string_list_as_string + + +class ModelJSONEncoder(JSONEncoder): + """ + JSON encoder that automatically unwraps ``value`` attributes. + """ + def __init__(self, *args, **kwargs): + # Just here to make sure Sphinx doesn't grab the base constructor's docstring + super(ModelJSONEncoder, self).__init__(*args, **kwargs) + + def default(self, o): # pylint: disable=method-hidden + from .mixins import ModelMixin + if isinstance(o, ModelMixin): + if hasattr(o, 'value'): + dict_to_return = o.to_dict(fields=('value',)) + return dict_to_return['value'] + else: + return o.to_dict() + else: + return JSONEncoder.default(self, o) + + +class NodeTemplateContainerHolder(object): + """ + Wrapper that allows using a :class:`~aria.modeling.models.NodeTemplate` model directly as the + ``container_holder`` input for :func:`~aria.modeling.functions.evaluate`. + """ + + def __init__(self, node_template): + self.container = node_template + self.service = None + + @property + def service_template(self): + return self.container.service_template + + +# def validate_no_undeclared_inputs(declared_inputs, supplied_inputs): +# +# undeclared_inputs = [input for input in supplied_inputs if input not in declared_inputs] +# if undeclared_inputs: +# raise exceptions.UndeclaredInputsException( +# 'Undeclared inputs have been provided: {0}; Declared inputs: {1}' +# .format(string_list_as_string(undeclared_inputs), +# string_list_as_string(declared_inputs.keys()))) + + +def validate_required_inputs_are_supplied(declared_inputs, supplied_inputs): + required_inputs = [input for input in declared_inputs.values() if input.required] + missing_required_inputs = [input for input in required_inputs + if input.name not in supplied_inputs and not str(input.value)] + if missing_required_inputs: + raise exceptions.MissingRequiredInputsException( + 'Required inputs {0} have not been provided values' + .format(string_list_as_string(missing_required_inputs))) + + +def merge_parameter_values(provided_values, declared_parameters, model_cls=None): + """ + Merges parameter values according to those declared by a type. + + Exceptions will be raised for validation errors. + + :param provided_values: provided parameter values or None + :type provided_values: {:obj:`basestring`: object} + :param declared_parameters: declared parameters + :type declared_parameters: {:obj:`basestring`: :class:`~aria.modeling.models.Parameter`} + :param model_cls: the model class that should be created from a provided value + :type model_cls: :class:`~aria.modeling.models.Input` or :class:`~aria.modeling.models.Argument` + :return: the merged parameters + :rtype: {:obj:`basestring`: :class:`~aria.modeling.models.Parameter`} + :raises ~aria.modeling.exceptions.UndeclaredInputsException: if a key in + ``parameter_values`` does not exist in ``declared_parameters`` + :raises ~aria.modeling.exceptions.MissingRequiredInputsException: if a key in + ``declared_parameters`` does not exist in ``parameter_values`` and also has no default value + :raises ~aria.modeling.exceptions.ParametersOfWrongTypeException: if a value in + ``parameter_values`` does not match its type in ``declared_parameters`` + """ + + provided_values = provided_values or {} + provided_values_of_wrong_type = OrderedDict() + model_parameters = OrderedDict() + model_cls = model_cls or _get_class_from_sql_relationship(declared_parameters) + + for declared_parameter_name, declared_parameter in declared_parameters.iteritems(): + if declared_parameter_name in provided_values: + # a value has been provided + value = provided_values[declared_parameter_name] + + # Validate type + type_name = declared_parameter.type_name + try: + validate_value_type(value, type_name) + except ValueError: + provided_values_of_wrong_type[declared_parameter_name] = type_name + except RuntimeError: + # TODO This error shouldn't be raised (or caught), but right now we lack support + # for custom data_types, which will raise this error. Skipping their validation. + pass + model_parameters[declared_parameter_name] = model_cls( # pylint: disable=unexpected-keyword-arg + name=declared_parameter_name, + type_name=type_name, + description=declared_parameter.description, + value=value) + else: + # Copy default value from declaration + model_parameters[declared_parameter_name] = model_cls( + value=declared_parameter._value, + name=declared_parameter.name, + type_name=declared_parameter.type_name, + description=declared_parameter.description) + + if provided_values_of_wrong_type: + error_message = StringIO() + for param_name, param_type in provided_values_of_wrong_type.iteritems(): + error_message.write('Parameter "{0}" is not of declared type "{1}"{2}' + .format(param_name, param_type, os.linesep)) + raise exceptions.ParametersOfWrongTypeException(error_message.getvalue()) + + return model_parameters + + +def parameters_as_values(the_dict): + return dict((k, v.value) for k, v in the_dict.iteritems()) + + +def dict_as_arguments(the_dict): + return OrderedDict((name, value.as_argument()) for name, value in the_dict.iteritems()) + + +class classproperty(object): # pylint: disable=invalid-name + def __init__(self, f): + self._func = f + self.__doct__ = f.__doc__ + + def __get__(self, instance, owner): + return self._func(owner) + + +def fix_doc(cls): + """ + Class decorator to use the last base class's docstring and make sure Sphinx doesn't grab the + base constructor's docstring. + """ + original_init = cls.__init__ + def init(*args, **kwargs): + original_init(*args, **kwargs) + + cls.__init__ = init + cls.__doc__ = cls.__bases__[-1].__doc__ + + return cls + + +def _get_class_from_sql_relationship(field): + class_ = field._sa_adapter.owner_state.class_ + prop_name = field._sa_adapter.attr.key + return getattr(class_, prop_name).property.mapper.class_ |