| # 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 .mixins import InstanceModelMixin |
| from ..orchestrator import execution_plugin |
| from ..parser import validation |
| from ..parser.consumption import ConsumptionContext |
| from ..utils import ( |
| collections, |
| formatting, |
| console |
| ) |
| from . import ( |
| relationship, |
| utils, |
| types as modeling_types |
| ) |
| |
| |
| 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 association proxies |
| |
| @declared_attr |
| def service_template_name(cls): |
| return relationship.association_proxy('service_template', 'name', type=':obj:`basestring`') |
| |
| # endregion |
| |
| # 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 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 satisfy_requirements(self): |
| satisfied = True |
| for node in self.nodes.itervalues(): |
| if not node.satisfy_requirements(): |
| satisfied = False |
| return satisfied |
| |
| def validate_capabilities(self): |
| satisfied = True |
| for node in self.nodes.itervalues(): |
| if not node.validate_capabilities(): |
| satisfied = False |
| return satisfied |
| |
| def find_hosts(self): |
| for node in self.nodes.itervalues(): |
| node.find_host() |
| |
| def configure_operations(self): |
| for node in self.nodes.itervalues(): |
| node.configure_operations() |
| for group in self.groups.itervalues(): |
| group.configure_operations() |
| for operation in self.workflows.itervalues(): |
| operation.configure() |
| |
| def is_node_a_target(self, target_node): |
| for node in self.nodes.itervalues(): |
| if self._is_node_a_target(node, target_node): |
| return True |
| return False |
| |
| def _is_node_a_target(self, source_node, target_node): |
| if source_node.outbound_relationships: |
| for relationship_model in source_node.outbound_relationships: |
| if relationship_model.target_node.name == target_node.name: |
| return True |
| else: |
| node = relationship_model.target_node |
| if node is not None: |
| if self._is_node_a_target(node, target_node): |
| return True |
| return False |
| |
| @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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.meta_data) |
| utils.validate_dict_values(self.nodes) |
| utils.validate_dict_values(self.groups) |
| utils.validate_dict_values(self.policies) |
| if self.substitution is not None: |
| self.substitution.validate() |
| utils.validate_dict_values(self.inputs) |
| utils.validate_dict_values(self.outputs) |
| utils.validate_dict_values(self.workflows) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.meta_data, report_issues) |
| utils.coerce_dict_values(self.nodes, report_issues) |
| utils.coerce_dict_values(self.groups, report_issues) |
| utils.coerce_dict_values(self.policies, report_issues) |
| if self.substitution is not None: |
| self.substitution.coerce_values(report_issues) |
| utils.coerce_dict_values(self.inputs, report_issues) |
| utils.coerce_dict_values(self.outputs, report_issues) |
| utils.coerce_dict_values(self.workflows, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| if self.description is not None: |
| console.puts(context.style.meta(self.description)) |
| utils.dump_dict_values(self.meta_data, 'Metadata') |
| for node in self.nodes.itervalues(): |
| node.dump() |
| for group in self.groups.itervalues(): |
| group.dump() |
| for policy in self.policies.itervalues(): |
| policy.dump() |
| if self.substitution is not None: |
| self.substitution.dump() |
| utils.dump_dict_values(self.inputs, 'Inputs') |
| utils.dump_dict_values(self.outputs, 'Outputs') |
| utils.dump_dict_values(self.workflows, 'Workflows') |
| |
| def dump_graph(self): |
| for node in self.nodes.itervalues(): |
| if not self.is_node_a_target(node): |
| self._dump_graph_node(node) |
| |
| def _dump_graph_node(self, node, capability=None): |
| context = ConsumptionContext.get_thread_local() |
| console.puts(context.style.node(node.name)) |
| if capability is not None: |
| console.puts('{0} ({1})'.format(context.style.property(capability.name), |
| context.style.type(capability.type.name))) |
| if node.outbound_relationships: |
| with context.style.indent: |
| for relationship_model in node.outbound_relationships: |
| relationship_name = context.style.property(relationship_model.name) |
| if relationship_model.type is not None: |
| console.puts('-> {0} ({1})'.format(relationship_name, |
| context.style.type( |
| relationship_model.type.name))) |
| else: |
| console.puts('-> {0}'.format(relationship_name)) |
| with console.indent(3): |
| self._dump_graph_node(relationship_model.target_node, |
| relationship_model.target_capability) |
| |
| |
| 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' |
| |
| # '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 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 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 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) |
| |
| @property |
| def host_address(self): |
| if self.host and self.host.attributes: |
| attribute = self.host.attributes.get('ip') |
| return attribute.value if attribute else None |
| return None |
| |
| def satisfy_requirements(self): |
| node_template = self.node_template |
| satisfied = True |
| for requirement_template in node_template.requirement_templates: |
| # Find target template |
| target_node_template, target_node_capability = \ |
| requirement_template.find_target(node_template) |
| if target_node_template is not None: |
| satisfied = self._satisfy_capability(target_node_capability, |
| target_node_template, |
| requirement_template) |
| else: |
| context = ConsumptionContext.get_thread_local() |
| context.validation.report('requirement "{0}" of node "{1}" has no target node ' |
| 'template'.format(requirement_template.name, self.name), |
| level=validation.Issue.BETWEEN_INSTANCES) |
| satisfied = False |
| return satisfied |
| |
| def _satisfy_capability(self, target_node_capability, target_node_template, |
| requirement_template): |
| from . import models |
| context = ConsumptionContext.get_thread_local() |
| # Find target nodes |
| target_nodes = target_node_template.nodes |
| if target_nodes: |
| target_node = None |
| target_capability = None |
| |
| if target_node_capability is not None: |
| # Relate to the first target node that has capacity |
| for node in target_nodes: |
| a_target_capability = node.capabilities.get(target_node_capability.name) |
| if a_target_capability.relate(): |
| target_node = node |
| target_capability = a_target_capability |
| break |
| else: |
| # Use first target node |
| target_node = target_nodes[0] |
| |
| if target_node is not None: |
| if requirement_template.relationship_template is not None: |
| relationship_model = \ |
| requirement_template.relationship_template.instantiate(self) |
| else: |
| relationship_model = models.Relationship() |
| relationship_model.name = requirement_template.name |
| relationship_model.requirement_template = requirement_template |
| relationship_model.target_node = target_node |
| relationship_model.target_capability = target_capability |
| self.outbound_relationships.append(relationship_model) |
| return True |
| else: |
| context.validation.report('requirement "{0}" of node "{1}" targets node ' |
| 'template "{2}" but its instantiated nodes do not ' |
| 'have enough capacity'.format( |
| requirement_template.name, |
| self.name, |
| target_node_template.name), |
| level=validation.Issue.BETWEEN_INSTANCES) |
| return False |
| else: |
| context.validation.report('requirement "{0}" of node "{1}" targets node template ' |
| '"{2}" but it has no instantiated nodes'.format( |
| requirement_template.name, |
| self.name, |
| target_node_template.name), |
| level=validation.Issue.BETWEEN_INSTANCES) |
| return False |
| |
| def validate_capabilities(self): |
| context = ConsumptionContext.get_thread_local() |
| satisfied = False |
| for capability in self.capabilities.itervalues(): |
| if not capability.has_enough_relationships: |
| context.validation.report('capability "{0}" of node "{1}" requires at least {2:d} ' |
| 'relationships but has {3:d}'.format( |
| capability.name, |
| self.name, |
| capability.min_occurrences, |
| capability.occurrences), |
| level=validation.Issue.BETWEEN_INSTANCES) |
| satisfied = False |
| return satisfied |
| |
| def find_host(self): |
| def _find_host(node): |
| if node.type.role == 'host': |
| return node |
| for the_relationship in node.outbound_relationships: |
| if (the_relationship.target_capability is not None) and \ |
| the_relationship.target_capability.type.role == 'host': |
| host = _find_host(the_relationship.target_node) |
| if host is not None: |
| return host |
| for the_relationship in node.inbound_relationships: |
| if (the_relationship.target_capability is not None) and \ |
| the_relationship.target_capability.type.role == 'feature': |
| host = _find_host(the_relationship.source_node) |
| if host is not None: |
| return host |
| return None |
| |
| self.host = _find_host(self) |
| |
| def configure_operations(self): |
| for interface in self.interfaces.itervalues(): |
| interface.configure_operations() |
| for the_relationship in self.outbound_relationships: |
| the_relationship.configure_operations() |
| |
| @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)))) |
| |
| def validate(self): |
| context = ConsumptionContext.get_thread_local() |
| if len(self.name) > context.modeling.id_max_length: |
| context.validation.report('"{0}" has an ID longer than the limit of {1:d} characters: ' |
| '{2:d}'.format( |
| self.name, |
| context.modeling.id_max_length, |
| len(self.name)), |
| level=validation.Issue.BETWEEN_INSTANCES) |
| |
| # TODO: validate that node template is of type? |
| |
| utils.validate_dict_values(self.properties) |
| utils.validate_dict_values(self.attributes) |
| utils.validate_dict_values(self.interfaces) |
| utils.validate_dict_values(self.artifacts) |
| utils.validate_dict_values(self.capabilities) |
| utils.validate_list_values(self.outbound_relationships) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.properties, report_issues) |
| utils.coerce_dict_values(self.attributes, report_issues) |
| utils.coerce_dict_values(self.interfaces, report_issues) |
| utils.coerce_dict_values(self.artifacts, report_issues) |
| utils.coerce_dict_values(self.capabilities, report_issues) |
| utils.coerce_list_values(self.outbound_relationships, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts('Node: {0}'.format(context.style.node(self.name))) |
| with context.style.indent: |
| console.puts('Type: {0}'.format(context.style.type(self.type.name))) |
| console.puts('Template: {0}'.format(context.style.node(self.node_template.name))) |
| utils.dump_dict_values(self.properties, 'Properties') |
| utils.dump_dict_values(self.attributes, 'Attributes') |
| utils.dump_interfaces(self.interfaces) |
| utils.dump_dict_values(self.artifacts, 'Artifacts') |
| utils.dump_dict_values(self.capabilities, 'Capabilities') |
| utils.dump_list_values(self.outbound_relationships, '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 association proxies |
| |
| # endregion |
| |
| # region one_to_one relationships |
| |
| # endregion |
| |
| # 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` |
| """) |
| |
| def configure_operations(self): |
| for interface in self.interfaces.itervalues(): |
| interface.configure_operations() |
| |
| @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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.properties) |
| utils.validate_dict_values(self.interfaces) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.properties, report_issues) |
| utils.coerce_dict_values(self.interfaces, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts('Group: {0}'.format(context.style.node(self.name))) |
| with context.style.indent: |
| console.puts('Type: {0}'.format(context.style.type(self.type.name))) |
| utils.dump_dict_values(self.properties, 'Properties') |
| utils.dump_interfaces(self.interfaces) |
| if self.nodes: |
| console.puts('Member nodes:') |
| with context.style.indent: |
| for node in self.nodes: |
| console.puts(context.style.node(node.name)) |
| |
| |
| 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 association proxies |
| |
| # endregion |
| |
| # region one_to_one relationships |
| |
| # endregion |
| |
| # 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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.properties) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.properties, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts('Policy: {0}'.format(context.style.node(self.name))) |
| with context.style.indent: |
| console.puts('Type: {0}'.format(context.style.type(self.type.name))) |
| utils.dump_dict_values(self.properties, 'Properties') |
| if self.nodes: |
| console.puts('Target nodes:') |
| with context.style.indent: |
| for node in self.nodes: |
| console.puts(context.style.node(node.name)) |
| if self.groups: |
| console.puts('Target groups:') |
| with context.style.indent: |
| for group in self.groups: |
| console.puts(context.style.node(group.name)) |
| |
| |
| 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 association proxies |
| |
| # endregion |
| |
| # region one_to_one relationships |
| |
| # endregion |
| |
| # 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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.mappings) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.mappings, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts('Substitution:') |
| with context.style.indent: |
| console.puts('Node type: {0}'.format(context.style.type(self.node_type.name))) |
| utils.dump_dict_values(self.mappings, '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', |
| 'capability_fk', |
| 'requirement_template_fk', |
| 'node_fk') |
| |
| # region association proxies |
| |
| # endregion |
| |
| # 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 one_to_many relationships |
| |
| # 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),)) |
| |
| def coerce_values(self, report_issues): |
| pass |
| |
| def validate(self): |
| context = ConsumptionContext.get_thread_local() |
| if (self.capability is None) and (self.requirement_template is None): |
| context.validation.report('mapping "{0}" refers to neither capability nor a requirement' |
| ' in node: {1}'.format( |
| self.name, |
| formatting.safe_repr(self.node.name)), |
| level=validation.Issue.BETWEEN_TYPES) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| if self.capability is not None: |
| console.puts('{0} -> {1}.{2}'.format( |
| context.style.node(self.name), |
| context.style.node(self.capability.node.name), |
| context.style.node(self.capability.name))) |
| else: |
| console.puts('{0} -> {1}.{2}'.format( |
| context.style.node(self.name), |
| context.style.node(self.node.name), |
| context.style.node(self.requirement_template.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 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 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 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` |
| """) |
| |
| def configure_operations(self): |
| for interface in self.interfaces.itervalues(): |
| interface.configure_operations() |
| |
| @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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.properties) |
| utils.validate_dict_values(self.interfaces) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.properties, report_issues) |
| utils.coerce_dict_values(self.interfaces, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| if self.name: |
| console.puts('{0} ->'.format(context.style.node(self.name))) |
| else: |
| console.puts('->') |
| with context.style.indent: |
| console.puts('Node: {0}'.format(context.style.node(self.target_node.name))) |
| if self.target_capability: |
| console.puts('Capability: {0}'.format(context.style.node( |
| self.target_capability.name))) |
| if self.type is not None: |
| console.puts('Relationship type: {0}'.format(context.style.type(self.type.name))) |
| if (self.relationship_template is not None) and self.relationship_template.name: |
| console.puts('Relationship template: {0}'.format( |
| context.style.node(self.relationship_template.name))) |
| utils.dump_dict_values(self.properties, 'Properties') |
| utils.dump_interfaces(self.interfaces, '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 association proxies |
| |
| # endregion |
| |
| # region one_to_one relationships |
| |
| # endregion |
| |
| # 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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.properties) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.properties, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts(context.style.node(self.name)) |
| with context.style.indent: |
| console.puts('Type: {0}'.format(context.style.type(self.type.name))) |
| console.puts('Occurrences: {0:d} ({1:d}{2})'.format( |
| self.occurrences, |
| self.min_occurrences or 0, |
| ' to {0:d}'.format(self.max_occurrences) |
| if self.max_occurrences is not None |
| else ' or more')) |
| utils.dump_dict_values(self.properties, '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 association proxies |
| |
| # endregion |
| |
| # region one_to_one relationships |
| |
| # endregion |
| |
| # 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` |
| """) |
| |
| def configure_operations(self): |
| for operation in self.operations.itervalues(): |
| operation.configure() |
| |
| @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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.inputs) |
| utils.validate_dict_values(self.operations) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.inputs, report_issues) |
| utils.coerce_dict_values(self.operations, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts(context.style.node(self.name)) |
| if self.description: |
| console.puts(context.style.meta(self.description)) |
| with context.style.indent: |
| console.puts('Interface type: {0}'.format(context.style.type(self.type.name))) |
| utils.dump_dict_values(self.inputs, 'Inputs') |
| utils.dump_dict_values(self.operations, '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 association proxies |
| |
| # endregion |
| |
| # 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 many_to_many relationships |
| |
| # 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 attemps (in seconds). |
| |
| :type: :obj:`float` |
| """) |
| |
| def configure(self): |
| if (self.implementation is None) and (self.function is None): |
| return |
| |
| if (self.interface is not None) and (self.plugin is None) and (self.function is None): |
| # ("interface" is None for workflow operations, which do not currently use "plugin") |
| # The default (None) plugin is the execution plugin |
| execution_plugin.instantiation.configure_operation(self) |
| else: |
| # In the future plugins may be able to add their own "configure_operation" hook that |
| # can validate the configuration and otherwise create specially derived arguments. For |
| # now, we just send all configuration parameters as arguments without validation. |
| configurations_as_arguments = {} |
| for configuration in self.configurations.itervalues(): |
| configurations_as_arguments[configuration.name] = configuration.as_argument() |
| |
| utils.instantiate_dict(self, self.arguments, configurations_as_arguments) |
| |
| # Send all inputs as extra arguments |
| # Note that they will override existing arguments of the same names |
| inputs_as_arguments = {} |
| for input in self.inputs.itervalues(): |
| inputs_as_arguments[input.name] = input.as_argument() |
| |
| utils.instantiate_dict(self, self.arguments, inputs_as_arguments) |
| |
| # Check for reserved arguments |
| from ..orchestrator.decorators import OPERATION_DECORATOR_RESERVED_ARGUMENTS |
| used_reserved_names = \ |
| OPERATION_DECORATOR_RESERVED_ARGUMENTS.intersection(self.arguments.keys()) |
| if used_reserved_names: |
| context = ConsumptionContext.get_thread_local() |
| context.validation.report('using reserved arguments in node "{0}": {1}' |
| .format( |
| self.name, |
| formatting.string_list_as_string(used_reserved_names)), |
| level=validation.Issue.EXTERNAL) |
| |
| |
| @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)))) |
| |
| def validate(self): |
| # TODO must be associated with either interface or service |
| utils.validate_dict_values(self.inputs) |
| utils.validate_dict_values(self.configurations) |
| utils.validate_dict_values(self.arguments) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.inputs, report_issues) |
| utils.coerce_dict_values(self.configurations, report_issues) |
| utils.coerce_dict_values(self.arguments, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts(context.style.node(self.name)) |
| if self.description: |
| console.puts(context.style.meta(self.description)) |
| with context.style.indent: |
| if self.implementation is not None: |
| console.puts('Implementation: {0}'.format( |
| context.style.literal(self.implementation))) |
| if self.dependencies: |
| console.puts( |
| 'Dependencies: {0}'.format( |
| ', '.join((str(context.style.literal(v)) for v in self.dependencies)))) |
| utils.dump_dict_values(self.inputs, 'Inputs') |
| if self.executor is not None: |
| console.puts('Executor: {0}'.format(context.style.literal(self.executor))) |
| if self.max_attempts is not None: |
| console.puts('Max attempts: {0}'.format(context.style.literal(self.max_attempts))) |
| if self.retry_interval is not None: |
| console.puts('Retry interval: {0}'.format( |
| context.style.literal(self.retry_interval))) |
| if self.plugin is not None: |
| console.puts('Plugin: {0}'.format( |
| context.style.literal(self.plugin.name))) |
| utils.dump_dict_values(self.configurations, 'Configuration') |
| if self.function is not None: |
| console.puts('Function: {0}'.format(context.style.literal(self.function))) |
| utils.dump_dict_values(self.arguments, 'Arguments') |
| |
| |
| 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 association proxies |
| |
| # endregion |
| |
| # region one_to_one relationships |
| |
| # endregion |
| |
| # 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)))) |
| |
| def validate(self): |
| utils.validate_dict_values(self.properties) |
| |
| def coerce_values(self, report_issues): |
| utils.coerce_dict_values(self.properties, report_issues) |
| |
| def dump(self): |
| context = ConsumptionContext.get_thread_local() |
| console.puts(context.style.node(self.name)) |
| if self.description: |
| console.puts(context.style.meta(self.description)) |
| with context.style.indent: |
| console.puts('Artifact type: {0}'.format(context.style.type(self.type.name))) |
| console.puts('Source path: {0}'.format(context.style.literal(self.source_path))) |
| if self.target_path is not None: |
| console.puts('Target path: {0}'.format(context.style.literal(self.target_path))) |
| if self.repository_url is not None: |
| console.puts('Repository URL: {0}'.format( |
| context.style.literal(self.repository_url))) |
| if self.repository_credential: |
| console.puts('Repository credential: {0}'.format( |
| context.style.literal(self.repository_credential))) |
| utils.dump_dict_values(self.properties, 'Properties') |