# 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_outbound(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':
                # ENRICO
                if (the_relationship.target_capability is not None):
                    host = _find_host_outbound(the_relationship.target_node)
                    if host is not None:
                        print node.name + " -> " + host.name
                        return host
            return None

        def _find_host_inbound(node):
            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_inbound(the_relationship.source_node)
                    if host is not None:
                        print node.name + " -> " + host.name
                        return host

            return None

        self.host = _find_host_outbound(self)
        if (not self.host):
            self.host = _find_host_inbound(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')

# 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_outbound(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':
                # ENRICO
                if (the_relationship.target_capability is not None):
                    host = _find_host_outbound(the_relationship.target_node)
                    if host is not None:
                        print node.name + " -> " + host.name
                        return host
            return None

        def _find_host_inbound(node):
            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_inbound(the_relationship.source_node)
                    if host is not None:
                        print node.name + " -> " + host.name
                        return host

            return None

        self.host = _find_host_outbound(self)
        if (not self.host):
            self.host = _find_host_inbound(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')

# 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 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. # region association proxies # endregion # region one_to_one relationships Exposes the entire service as a single node.          :type: :class:`Substitution` # endregion # region one_to_many relationships Output parameters.          :type: {:obj:`basestring`: :class:`Output`} Externally provided parameters.          :type: {:obj:`basestring`: :class:`Input`} Service updates.          :type: [:class:`ServiceUpdate`] Service modifications.          :type: [:class:`ServiceModification`] Executions.          :type: [:class:`Execution`] Nodes.          :type: {:obj:`basestring`, :class:`Node`} Groups.          :type: {:obj:`basestring`, :class:`Group`} Policies.          :type: {:obj:`basestring`, :class:`Policy`} Workflows.          :type: {:obj:`basestring`, :class:`Operation`} # endregion # region many_to_one relationships Source service template (can be ``None``).          :type: :class:`ServiceTemplate` # endregion # region many_to_many relationships Associated metadata.          :type: {:obj:`basestring`, :class:`Metadata`} # Warning! We cannot use the attr name "metadata" because it's used by SQLAlchemy! Associated plugins.          :type: {:obj:`basestring`, :class:`Plugin`} # endregion # region foreign keys Service one-to-one to Substitution For Service many-to-one to ServiceTemplate # endregion Human-readable description.      :type: :obj:`basestring` Creation timestamp.      :type: :class:`~datetime.datetime` Update timestamp.      :type: :class:`~datetime.datetime` 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`. # '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. # region association proxies # endregion # region one_to_one relationships # 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` # endregion # region one_to_many relationships Associated tasks.          :type: [:class:`Task`] Associated interfaces.          :type: {:obj:`basestring`: :class:`Interface`} Associated immutable parameters.          :type: {:obj:`basestring`: :class:`Property`} Associated mutable parameters.          :type: {:obj:`basestring`: :class:`Attribute`} Associated artifacts.          :type: {:obj:`basestring`: :class:`Artifact`} Associated exposed capabilities.          :type: {:obj:`basestring`: :class:`Capability`} Relationships to other nodes.          :type: [:class:`Relationship`] Relationships from other nodes.          :type: [:class:`Relationship`] # endregion # region many_to_one relationships Containing service.          :type: :class:`Service` Source node template (can be ``None``).          :type: :class:`NodeTemplate` Node type.          :type: :class:`Type` # endregion # region foreign_keys For Node many-to-one to Type For Node one-to-one to Node For Service one-to-many to Node For Node many-to-one to NodeTemplate # endregion Human-readable description.      :type: :obj:`basestring` TOSCA state.      :type: :obj:`basestring` Used by :mod:`aria.storage.instrumentation`.      :type: :obj:`int` # Enable SQLAlchemy automatic version counting :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 # Find target template # Find target nodes # Relate to the first target node that has capacity # Use first target node #if (the_relationship.target_capability is not None) and \ #        the_relationship.target_capability.type.role == 'host': # ENRICO # TODO: validate that node template is of type? Typed logical container for zero or more :class:`Node` instances.      Usually an instance of a :class:`GroupTemplate`. # region association proxies # endregion # region one_to_one relationships # endregion # region one_to_many relationships Associated immutable parameters.          :type: {:obj:`basestring`: :class:`Property`} Associated interfaces.          :type: {:obj:`basestring`: :class:`Interface`} # endregion # region many_to_one relationships Containing service.          :type: :class:`Service` Source group template (can be ``None``).          :type: :class:`GroupTemplate` Group type.          :type: :class:`Type` # endregion # region many_to_many relationships Member nodes.          :type: [:class:`Node`] # endregion # region foreign_keys For Group many-to-one to Type For Service one-to-many to Group For Group many-to-one to GroupTemplate # endregion Human-readable description.      :type: :obj:`basestring` Typed set of orchestration hints applied to zero or more :class:`Node` or :class:`Group`     instances.      Usually an instance of a :class:`PolicyTemplate`. # region association proxies # endregion # region one_to_one relationships # endregion # region one_to_many relationships Associated immutable parameters.          :type: {:obj:`basestring`: :class:`Property`} # endregion # region many_to_one relationships Containing service.          :type: :class:`Service` Source policy template (can be ``None``).          :type: :class:`PolicyTemplate` Group type.          :type: :class:`Type` # endregion # region many_to_many relationships Policy is enacted on these nodes.          :type: {:obj:`basestring`: :class:`Node`} Policy is enacted on nodes in these groups.          :type: {:obj:`basestring`: :class:`Group`} # endregion # region foreign_keys For Policy many-to-one to Type For Service one-to-many to Policy For Policy many-to-one to PolicyTemplate # endregion Human-readable description.      :type: :obj:`basestring` Exposes the entire service as a single node.      Usually an instance of a :class:`SubstitutionTemplate`. # region association proxies # endregion # region one_to_one relationships # endregion # region one_to_many relationships Map requirement and capabilities to exposed node.          :type: {:obj:`basestring`: :class:`SubstitutionMapping`} # endregion # region many_to_one relationships Containing service.          :type: :class:`Service` Source substitution template (can be ``None``).          :type: :class:`SubstitutionTemplate` Exposed node type.          :type: :class:`Type` # endregion # region foreign_keys For Substitution many-to-one to Type For Substitution many-to-one to SubstitutionTemplate # endregion 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`. # region association proxies # endregion # region one_to_one relationships Capability to expose (can be ``None``).          :type: :class:`Capability` Requirement template to expose (can be ``None``).          :type: :class:`RequirementTemplate` Node for which to expose :attr:`requirement_template` (can be ``None``).          :type: :class:`Node` # endregion # region one_to_many relationships # endregion # region many_to_one relationships Containing substitution.          :type: :class:`Substitution` # endregion # region foreign keys For Substitution one-to-many to SubstitutionMapping For Substitution one-to-one to Capability For Substitution one-to-one to Node For Substitution one-to-one to RequirementTemplate # endregion 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`. # region association proxies # endregion # region one_to_one relationships Target capability.          :type: :class:`Capability` # endregion # region one_to_many relationships Associated tasks.          :type: [:class:`Task`] Associated interfaces.          :type: {:obj:`basestring`: :class:`Interface`} Associated immutable parameters.          :type: {:obj:`basestring`: :class:`Property`} # endregion # region many_to_one relationships Source node.          :type: :class:`Node` Target node.          :type: :class:`Node` Source relationship template (can be ``None``).          :type: :class:`RelationshipTemplate` Source requirement template (can be ``None``).          :type: :class:`RequirementTemplate` Relationship type.          :type: :class:`Type` # endregion # region foreign keys For Relationship many-to-one to Type For Node one-to-many to Relationship For Node one-to-many to Relationship For Relationship one-to-one to Capability For Relationship many-to-one to RequirementTemplate For Relationship many-to-one to RelationshipTemplate # endregion Position at source.      :type: :obj:`int` Position at target.      :type: :obj:`int` 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`. # region association proxies # endregion # region one_to_one relationships # endregion # region one_to_many relationships Associated immutable parameters.          :type: {:obj:`basestring`: :class:`Property`} # endregion # region many_to_one relationships Containing node.          :type: :class:`Node` Source capability template (can be ``None``).          :type: :class:`CapabilityTemplate` Capability type.          :type: :class:`Type` # endregion # region foreign_keys For Capability many-to-one to Type For Node one-to-many to Capability For Capability many-to-one to CapabilityTemplate # endregion Minimum number of requirement matches required.      :type: :obj:`int` Maximum number of requirement matches allowed.      :type: :obj:`int` Number of requirement matches.      :type: :obj:`int` 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`. # region association proxies # endregion # region one_to_one relationships # endregion # region one_to_many relationships Parameters for all operations of the interface.          :type: {:obj:`basestring`: :class:`Input`} Associated operations.          :type: {:obj:`basestring`: :class:`Operation`} # endregion # region many_to_one relationships Containing node (can be ``None``).          :type: :class:`Node` Containing group (can be ``None``).          :type: :class:`Group` Containing relationship (can be ``None``).          :type: :class:`Relationship` Source interface template (can be ``None``).          :type: :class:`InterfaceTemplate` Interface type.          :type: :class:`Type` # endregion # region foreign_keys For Interface many-to-one to Type For Node one-to-many to Interface For Group one-to-many to Interface For Relationship one-to-many to Interface For Interface many-to-one to InterfaceTemplate # endregion Human-readable description.      :type: :obj:`basestring` 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`. # region association proxies # endregion # region one_to_one relationships Associated plugin.          :type: :class:`Plugin` # endregion # region one_to_many relationships Parameters provided to the :attr:`implementation`.          :type: {:obj:`basestring`: :class:`Input`} Arguments sent to the Python :attr:`function`.          :type: {:obj:`basestring`: :class:`Argument`} Configuration parameters for the Python :attr:`function`.          :type: {:obj:`basestring`: :class:`Configuration`} # endregion # region many_to_one relationships Containing service (can be ``None``). For workflow operations.          :type: :class:`Service` Containing interface (can be ``None``).          :type: :class:`Interface` Source operation template (can be ``None``).          :type: :class:`OperationTemplate` # endregion # region many_to_many relationships # endregion # region foreign_keys For Service one-to-many to Operation For Interface one-to-many to Operation For Operation one-to-one to Plugin For Operation many-to-one to OperationTemplate # endregion Human-readable description.      :type: :obj:`basestring` 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 (usually the name of an artifact).      :type: :obj:`basestring` Dependencies (usually names of artifacts).      :type: [:obj:`basestring`] Full path to Python function.      :type: :obj:`basestring` Name of executor.      :type: :obj:`basestring` Maximum number of attempts allowed in case of task failure.      :type: :obj:`int` Interval between task retry attemps (in seconds).      :type: :obj:`float` # ("interface" is None for workflow operations, which do not currently use "plugin") # The default (None) plugin is the execution plugin # 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. # Send all inputs as extra arguments # Note that they will override existing arguments of the same names # Check for reserved arguments # TODO must be associated with either interface or service Typed file, either provided in a CSAR or downloaded from a repository.      Usually an instance of :class:`ArtifactTemplate`. # region association proxies # endregion # region one_to_one relationships # endregion # region one_to_many relationships Associated immutable parameters.          :type: {:obj:`basestring`: :class:`Property`} # endregion # region many_to_one relationships Containing node.          :type: :class:`Node` Source artifact template (can be ``None``).          :type: :class:`ArtifactTemplate` Artifact type.          :type: :class:`Type` # endregion # region foreign_keys For Artifact many-to-one to Type For Node one-to-many to Artifact For Artifact many-to-one to ArtifactTemplate # endregion Human-readable description.      :type: :obj:`basestring` Source path (in CSAR or repository).      :type: :obj:`basestring` Path at which to install at destination.      :type: :obj:`basestring` Repository URL.      :type: :obj:`basestring` Credentials for accessing the repository.      :type: {:obj:`basestring`, :obj:`basestring`}