vFW and vDNS support added to azure-plugin
[multicloud/azure.git] / azure / aria / aria-extension-cloudify / src / aria / aria / modeling / service_instance.py
diff --git a/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_instance.py b/azure/aria/aria-extension-cloudify/src/aria/aria/modeling/service_instance.py
new file mode 100644 (file)
index 0000000..01c4da9
--- /dev/null
@@ -0,0 +1,1695 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+ARIA modeling service instance module
+"""
+
+# pylint: disable=too-many-lines, no-self-argument, no-member, abstract-method
+
+from sqlalchemy import (
+    Column,
+    Text,
+    Integer,
+    Enum,
+    Boolean
+)
+from sqlalchemy import DateTime
+from sqlalchemy.ext.declarative import declared_attr
+from sqlalchemy.ext.orderinglist import ordering_list
+
+from . import (
+    relationship,
+    types as modeling_types
+)
+from .mixins import InstanceModelMixin
+
+from ..utils import (
+    collections,
+    formatting
+)
+
+
+class ServiceBase(InstanceModelMixin):
+    """
+    Usually an instance of a :class:`ServiceTemplate` and its many associated templates (node
+    templates, group templates, policy templates, etc.). However, it can also be created
+    programmatically.
+    """
+
+    __tablename__ = 'service'
+
+    __private_fields__ = ('substitution_fk',
+                          'service_template_fk')
+
+    # region one_to_one relationships
+
+    @declared_attr
+    def substitution(cls):
+        """
+        Exposes the entire service as a single node.
+
+        :type: :class:`Substitution`
+        """
+        return relationship.one_to_one(cls, 'substitution', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def outputs(cls):
+        """
+        Output parameters.
+
+        :type: {:obj:`basestring`: :class:`Output`}
+        """
+        return relationship.one_to_many(cls, 'output', dict_key='name')
+
+    @declared_attr
+    def inputs(cls):
+        """
+        Externally provided parameters.
+
+        :type: {:obj:`basestring`: :class:`Input`}
+        """
+        return relationship.one_to_many(cls, 'input', dict_key='name')
+
+    @declared_attr
+    def updates(cls):
+        """
+        Service updates.
+
+        :type: [:class:`ServiceUpdate`]
+        """
+        return relationship.one_to_many(cls, 'service_update')
+
+    @declared_attr
+    def modifications(cls):
+        """
+        Service modifications.
+
+        :type: [:class:`ServiceModification`]
+        """
+        return relationship.one_to_many(cls, 'service_modification')
+
+    @declared_attr
+    def executions(cls):
+        """
+        Executions.
+
+        :type: [:class:`Execution`]
+        """
+        return relationship.one_to_many(cls, 'execution')
+
+    @declared_attr
+    def nodes(cls):
+        """
+        Nodes.
+
+        :type: {:obj:`basestring`, :class:`Node`}
+        """
+        return relationship.one_to_many(cls, 'node', dict_key='name')
+
+    @declared_attr
+    def groups(cls):
+        """
+        Groups.
+
+        :type: {:obj:`basestring`, :class:`Group`}
+        """
+        return relationship.one_to_many(cls, 'group', dict_key='name')
+
+    @declared_attr
+    def policies(cls):
+        """
+        Policies.
+
+        :type: {:obj:`basestring`, :class:`Policy`}
+        """
+        return relationship.one_to_many(cls, 'policy', dict_key='name')
+
+    @declared_attr
+    def workflows(cls):
+        """
+        Workflows.
+
+        :type: {:obj:`basestring`, :class:`Operation`}
+        """
+        return relationship.one_to_many(cls, 'operation', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def service_template(cls):
+        """
+        Source service template (can be ``None``).
+
+        :type: :class:`ServiceTemplate`
+        """
+        return relationship.many_to_one(cls, 'service_template')
+
+    # endregion
+
+    # region many_to_many relationships
+
+    @declared_attr
+    def meta_data(cls):
+        """
+        Associated metadata.
+
+        :type: {:obj:`basestring`, :class:`Metadata`}
+        """
+        # Warning! We cannot use the attr name "metadata" because it's used by SQLAlchemy!
+        return relationship.many_to_many(cls, 'metadata', dict_key='name')
+
+    @declared_attr
+    def plugins(cls):
+        """
+        Associated plugins.
+
+        :type: {:obj:`basestring`, :class:`Plugin`}
+        """
+        return relationship.many_to_many(cls, 'plugin', dict_key='name')
+
+    # endregion
+
+    # region association proxies
+
+    @declared_attr
+    def service_template_name(cls):
+        return relationship.association_proxy('service_template', 'name', type=':obj:`basestring`')
+
+    # endregion
+
+    # region foreign keys
+
+    @declared_attr
+    def substitution_fk(cls):
+        """Service one-to-one to Substitution"""
+        return relationship.foreign_key('substitution', nullable=True)
+
+    @declared_attr
+    def service_template_fk(cls):
+        """For Service many-to-one to ServiceTemplate"""
+        return relationship.foreign_key('service_template', nullable=True)
+
+    # endregion
+
+    description = Column(Text, doc="""
+    Human-readable description.
+
+    :type: :obj:`basestring`
+    """)
+
+    created_at = Column(DateTime, nullable=False, index=True, doc="""
+    Creation timestamp.
+
+    :type: :class:`~datetime.datetime`
+    """)
+
+    updated_at = Column(DateTime, doc="""
+    Update timestamp.
+
+    :type: :class:`~datetime.datetime`
+    """)
+
+    def get_node_by_type(self, type_name):
+        """
+        Finds the first node of a type (or descendent type).
+        """
+        service_template = self.service_template
+
+        if service_template is not None:
+            node_types = service_template.node_types
+            if node_types is not None:
+                for node in self.nodes.itervalues():
+                    if node_types.is_descendant(type_name, node.type.name):
+                        return node
+
+        return None
+
+    def get_policy_by_type(self, type_name):
+        """
+        Finds the first policy of a type (or descendent type).
+        """
+        service_template = self.service_template
+
+        if service_template is not None:
+            policy_types = service_template.policy_types
+            if policy_types is not None:
+                for policy in self.policies.itervalues():
+                    if policy_types.is_descendant(type_name, policy.type.name):
+                        return policy
+
+        return None
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('description', self.description),
+            ('metadata', formatting.as_raw_dict(self.meta_data)),
+            ('nodes', formatting.as_raw_list(self.nodes)),
+            ('groups', formatting.as_raw_list(self.groups)),
+            ('policies', formatting.as_raw_list(self.policies)),
+            ('substitution', formatting.as_raw(self.substitution)),
+            ('inputs', formatting.as_raw_dict(self.inputs)),
+            ('outputs', formatting.as_raw_dict(self.outputs)),
+            ('workflows', formatting.as_raw_list(self.workflows))))
+
+
+class NodeBase(InstanceModelMixin):
+    """
+    Typed vertex in the service topology.
+
+    Nodes may have zero or more :class:`Relationship` instances to other nodes, together forming
+    a many-to-many node graph.
+
+    Usually an instance of a :class:`NodeTemplate`.
+    """
+
+    __tablename__ = 'node'
+
+    __private_fields__ = ('type_fk',
+                          'host_fk',
+                          'service_fk',
+                          'node_template_fk')
+
+    INITIAL = 'initial'
+    CREATING = 'creating'
+    CREATED = 'created'
+    CONFIGURING = 'configuring'
+    CONFIGURED = 'configured'
+    STARTING = 'starting'
+    STARTED = 'started'
+    STOPPING = 'stopping'
+    DELETING = 'deleting'
+    DELETED = 'deleted'
+    ERROR = 'error'
+
+    # Note: 'deleted' isn't actually part of the TOSCA spec, since according the description of the
+    # 'deleting' state: "Node is transitioning from its current state to one where it is deleted and
+    # its state is no longer tracked by the instance model." However, we prefer to be able to
+    # retrieve information about deleted nodes, so we chose to add this 'deleted' state to enable us
+    # to do so.
+
+    STATES = (INITIAL, CREATING, CREATED, CONFIGURING, CONFIGURED, STARTING, STARTED, STOPPING,
+              DELETING, DELETED, ERROR)
+
+    _OP_TO_STATE = {'create': {'transitional': CREATING, 'finished': CREATED},
+                    'configure': {'transitional': CONFIGURING, 'finished': CONFIGURED},
+                    'start': {'transitional': STARTING, 'finished': STARTED},
+                    'stop': {'transitional': STOPPING, 'finished': CONFIGURED},
+                    'delete': {'transitional': DELETING, 'finished': DELETED}}
+
+    # region one_to_one relationships
+
+    @declared_attr
+    def host(cls): # pylint: disable=method-hidden
+        """
+        Node in which we are hosted (can be ``None``).
+
+        Normally the host node is found by following the relationship graph (relationships with
+        ``host`` roles) to final nodes (with ``host`` roles).
+
+        :type: :class:`Node`
+        """
+        return relationship.one_to_one_self(cls, 'host_fk')
+
+    # endregion
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def tasks(cls):
+        """
+        Associated tasks.
+
+        :type: [:class:`Task`]
+        """
+        return relationship.one_to_many(cls, 'task')
+
+    @declared_attr
+    def interfaces(cls):
+        """
+        Associated interfaces.
+
+        :type: {:obj:`basestring`: :class:`Interface`}
+        """
+        return relationship.one_to_many(cls, 'interface', dict_key='name')
+
+    @declared_attr
+    def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
+        return relationship.one_to_many(cls, 'property', dict_key='name')
+
+    @declared_attr
+    def attributes(cls):
+        """
+        Associated mutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Attribute`}
+        """
+        return relationship.one_to_many(cls, 'attribute', dict_key='name')
+
+    @declared_attr
+    def artifacts(cls):
+        """
+        Associated artifacts.
+
+        :type: {:obj:`basestring`: :class:`Artifact`}
+        """
+        return relationship.one_to_many(cls, 'artifact', dict_key='name')
+
+    @declared_attr
+    def capabilities(cls):
+        """
+        Associated exposed capabilities.
+
+        :type: {:obj:`basestring`: :class:`Capability`}
+        """
+        return relationship.one_to_many(cls, 'capability', dict_key='name')
+
+    @declared_attr
+    def outbound_relationships(cls):
+        """
+        Relationships to other nodes.
+
+        :type: [:class:`Relationship`]
+        """
+        return relationship.one_to_many(
+            cls, 'relationship', other_fk='source_node_fk', back_populates='source_node',
+            rel_kwargs=dict(
+                order_by='Relationship.source_position',
+                collection_class=ordering_list('source_position', count_from=0)
+            )
+        )
+
+    @declared_attr
+    def inbound_relationships(cls):
+        """
+        Relationships from other nodes.
+
+        :type: [:class:`Relationship`]
+        """
+        return relationship.one_to_many(
+            cls, 'relationship', other_fk='target_node_fk', back_populates='target_node',
+            rel_kwargs=dict(
+                order_by='Relationship.target_position',
+                collection_class=ordering_list('target_position', count_from=0)
+            )
+        )
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def service(cls):
+        """
+        Containing service.
+
+        :type: :class:`Service`
+        """
+        return relationship.many_to_one(cls, 'service')
+
+    @declared_attr
+    def node_template(cls):
+        """
+        Source node template (can be ``None``).
+
+        :type: :class:`NodeTemplate`
+        """
+        return relationship.many_to_one(cls, 'node_template')
+
+    @declared_attr
+    def type(cls):
+        """
+        Node type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region association proxies
+
+    @declared_attr
+    def service_name(cls):
+        return relationship.association_proxy('service', 'name', type=':obj:`basestring`')
+
+    @declared_attr
+    def node_template_name(cls):
+        return relationship.association_proxy('node_template', 'name', type=':obj:`basestring`')
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def type_fk(cls):
+        """For Node many-to-one to Type"""
+        return relationship.foreign_key('type')
+
+    @declared_attr
+    def host_fk(cls):
+        """For Node one-to-one to Node"""
+        return relationship.foreign_key('node', nullable=True)
+
+    @declared_attr
+    def service_fk(cls):
+        """For Service one-to-many to Node"""
+        return relationship.foreign_key('service')
+
+    @declared_attr
+    def node_template_fk(cls):
+        """For Node many-to-one to NodeTemplate"""
+        return relationship.foreign_key('node_template')
+
+    # endregion
+
+    description = Column(Text, doc="""
+    Human-readable description.
+
+    :type: :obj:`basestring`
+    """)
+
+    state = Column(Enum(*STATES, name='node_state'), nullable=False, default=INITIAL, doc="""
+    TOSCA state.
+
+    :type: :obj:`basestring`
+    """)
+
+    version = Column(Integer, default=1, doc="""
+    Used by :mod:`aria.storage.instrumentation`.
+
+    :type: :obj:`int`
+    """)
+
+    __mapper_args__ = {'version_id_col': version} # Enable SQLAlchemy automatic version counting
+
+    @classmethod
+    def determine_state(cls, op_name, is_transitional):
+        """
+        :returns the state the node should be in as a result of running the operation on this node.
+
+        E.g. if we are running tosca.interfaces.node.lifecycle.Standard.create, then
+        the resulting state should either 'creating' (if the task just started) or 'created'
+        (if the task ended).
+
+        If the operation is not a standard TOSCA lifecycle operation, then we return None.
+        """
+
+        state_type = 'transitional' if is_transitional else 'finished'
+        try:
+            return cls._OP_TO_STATE[op_name][state_type]
+        except KeyError:
+            return None
+
+    def is_available(self):
+        return self.state not in (self.INITIAL, self.DELETED, self.ERROR)
+
+    def get_outbound_relationship_by_name(self, name):
+        for the_relationship in self.outbound_relationships:
+            if the_relationship.name == name:
+                return the_relationship
+        return None
+
+    def get_inbound_relationship_by_name(self, name):
+        for the_relationship in self.inbound_relationships:
+            if the_relationship.name == name:
+                return the_relationship
+        return None
+
+    @property
+    def host_address(self):
+        if self.host and self.host.attributes:
+            attribute = self.host.attributes.get('ip')
+            if attribute is not None:
+                return attribute.value
+        return None
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('type_name', self.type.name),
+            ('properties', formatting.as_raw_dict(self.properties)),
+            ('attributes', formatting.as_raw_dict(self.properties)),
+            ('interfaces', formatting.as_raw_list(self.interfaces)),
+            ('artifacts', formatting.as_raw_list(self.artifacts)),
+            ('capabilities', formatting.as_raw_list(self.capabilities)),
+            ('relationships', formatting.as_raw_list(self.outbound_relationships))))
+
+
+class GroupBase(InstanceModelMixin):
+    """
+    Typed logical container for zero or more :class:`Node` instances.
+
+    Usually an instance of a :class:`GroupTemplate`.
+    """
+
+    __tablename__ = 'group'
+
+    __private_fields__ = ('type_fk',
+                          'service_fk',
+                          'group_template_fk')
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
+        return relationship.one_to_many(cls, 'property', dict_key='name')
+
+    @declared_attr
+    def interfaces(cls):
+        """
+        Associated interfaces.
+
+        :type: {:obj:`basestring`: :class:`Interface`}
+        """
+        return relationship.one_to_many(cls, 'interface', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def service(cls):
+        """
+        Containing service.
+
+        :type: :class:`Service`
+        """
+        return relationship.many_to_one(cls, 'service')
+
+    @declared_attr
+    def group_template(cls):
+        """
+        Source group template (can be ``None``).
+
+        :type: :class:`GroupTemplate`
+        """
+        return relationship.many_to_one(cls, 'group_template')
+
+    @declared_attr
+    def type(cls):
+        """
+        Group type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region many_to_many relationships
+
+    @declared_attr
+    def nodes(cls):
+        """
+        Member nodes.
+
+        :type: [:class:`Node`]
+        """
+        return relationship.many_to_many(cls, 'node')
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def type_fk(cls):
+        """For Group many-to-one to Type"""
+        return relationship.foreign_key('type')
+
+    @declared_attr
+    def service_fk(cls):
+        """For Service one-to-many to Group"""
+        return relationship.foreign_key('service')
+
+    @declared_attr
+    def group_template_fk(cls):
+        """For Group many-to-one to GroupTemplate"""
+        return relationship.foreign_key('group_template', nullable=True)
+
+    # endregion
+
+    description = Column(Text, doc="""
+    Human-readable description.
+
+    :type: :obj:`basestring`
+    """)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('properties', formatting.as_raw_dict(self.properties)),
+            ('interfaces', formatting.as_raw_list(self.interfaces))))
+
+
+class PolicyBase(InstanceModelMixin):
+    """
+    Typed set of orchestration hints applied to zero or more :class:`Node` or :class:`Group`
+    instances.
+
+    Usually an instance of a :class:`PolicyTemplate`.
+    """
+
+    __tablename__ = 'policy'
+
+    __private_fields__ = ('type_fk',
+                          'service_fk',
+                          'policy_template_fk')
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
+        return relationship.one_to_many(cls, 'property', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def service(cls):
+        """
+        Containing service.
+
+        :type: :class:`Service`
+        """
+        return relationship.many_to_one(cls, 'service')
+
+    @declared_attr
+    def policy_template(cls):
+        """
+        Source policy template (can be ``None``).
+
+        :type: :class:`PolicyTemplate`
+        """
+        return relationship.many_to_one(cls, 'policy_template')
+
+    @declared_attr
+    def type(cls):
+        """
+        Group type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region many_to_many relationships
+
+    @declared_attr
+    def nodes(cls):
+        """
+        Policy is enacted on these nodes.
+
+        :type: {:obj:`basestring`: :class:`Node`}
+        """
+        return relationship.many_to_many(cls, 'node')
+
+    @declared_attr
+    def groups(cls):
+        """
+        Policy is enacted on nodes in these groups.
+
+        :type: {:obj:`basestring`: :class:`Group`}
+        """
+        return relationship.many_to_many(cls, 'group')
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def type_fk(cls):
+        """For Policy many-to-one to Type"""
+        return relationship.foreign_key('type')
+
+    @declared_attr
+    def service_fk(cls):
+        """For Service one-to-many to Policy"""
+        return relationship.foreign_key('service')
+
+    @declared_attr
+    def policy_template_fk(cls):
+        """For Policy many-to-one to PolicyTemplate"""
+        return relationship.foreign_key('policy_template', nullable=True)
+
+    # endregion
+
+    description = Column(Text, doc="""
+    Human-readable description.
+
+    :type: :obj:`basestring`
+    """)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('type_name', self.type.name),
+            ('properties', formatting.as_raw_dict(self.properties))))
+
+
+class SubstitutionBase(InstanceModelMixin):
+    """
+    Exposes the entire service as a single node.
+
+    Usually an instance of a :class:`SubstitutionTemplate`.
+    """
+
+    __tablename__ = 'substitution'
+
+    __private_fields__ = ('node_type_fk',
+                          'substitution_template_fk')
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def mappings(cls):
+        """
+        Map requirement and capabilities to exposed node.
+
+        :type: {:obj:`basestring`: :class:`SubstitutionMapping`}
+        """
+        return relationship.one_to_many(cls, 'substitution_mapping', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def service(cls):
+        """
+        Containing service.
+
+        :type: :class:`Service`
+        """
+        return relationship.one_to_one(cls, 'service', back_populates=relationship.NO_BACK_POP)
+
+    @declared_attr
+    def substitution_template(cls):
+        """
+        Source substitution template (can be ``None``).
+
+        :type: :class:`SubstitutionTemplate`
+        """
+        return relationship.many_to_one(cls, 'substitution_template')
+
+    @declared_attr
+    def node_type(cls):
+        """
+        Exposed node type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def node_type_fk(cls):
+        """For Substitution many-to-one to Type"""
+        return relationship.foreign_key('type')
+
+    @declared_attr
+    def substitution_template_fk(cls):
+        """For Substitution many-to-one to SubstitutionTemplate"""
+        return relationship.foreign_key('substitution_template', nullable=True)
+
+    # endregion
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('node_type_name', self.node_type.name),
+            ('mappings', formatting.as_raw_dict(self.mappings))))
+
+
+class SubstitutionMappingBase(InstanceModelMixin):
+    """
+    Used by :class:`Substitution` to map a capability or a requirement to the exposed node.
+
+    The :attr:`name` field should match the capability or requirement template name on the exposed
+    node's type.
+
+    Only one of :attr:`capability` and :attr:`requirement_template` can be set. If the latter is
+    set, then :attr:`node` must also be set.
+
+    Usually an instance of a :class:`SubstitutionMappingTemplate`.
+    """
+
+    __tablename__ = 'substitution_mapping'
+
+    __private_fields__ = ('substitution_fk',
+                          'node_fk',
+                          'capability_fk',
+                          'requirement_template_fk')
+
+    # region one_to_one relationships
+
+    @declared_attr
+    def capability(cls):
+        """
+        Capability to expose (can be ``None``).
+
+        :type: :class:`Capability`
+        """
+        return relationship.one_to_one(cls, 'capability', back_populates=relationship.NO_BACK_POP)
+
+    @declared_attr
+    def requirement_template(cls):
+        """
+        Requirement template to expose (can be ``None``).
+
+        :type: :class:`RequirementTemplate`
+        """
+        return relationship.one_to_one(cls, 'requirement_template',
+                                       back_populates=relationship.NO_BACK_POP)
+
+    @declared_attr
+    def node(cls):
+        """
+        Node for which to expose :attr:`requirement_template` (can be ``None``).
+
+        :type: :class:`Node`
+        """
+        return relationship.one_to_one(cls, 'node', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def substitution(cls):
+        """
+        Containing substitution.
+
+        :type: :class:`Substitution`
+        """
+        return relationship.many_to_one(cls, 'substitution', back_populates='mappings')
+
+    # endregion
+
+    # region foreign keys
+
+    @declared_attr
+    def substitution_fk(cls):
+        """For Substitution one-to-many to SubstitutionMapping"""
+        return relationship.foreign_key('substitution')
+
+    @declared_attr
+    def capability_fk(cls):
+        """For Substitution one-to-one to Capability"""
+        return relationship.foreign_key('capability', nullable=True)
+
+    @declared_attr
+    def node_fk(cls):
+        """For Substitution one-to-one to Node"""
+        return relationship.foreign_key('node', nullable=True)
+
+    @declared_attr
+    def requirement_template_fk(cls):
+        """For Substitution one-to-one to RequirementTemplate"""
+        return relationship.foreign_key('requirement_template', nullable=True)
+
+    # endregion
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),))
+
+
+class RelationshipBase(InstanceModelMixin):
+    """
+    Optionally-typed edge in the service topology, connecting a :class:`Node` to a
+    :class:`Capability` of another node.
+
+    Might be an instance of :class:`RelationshipTemplate` and/or :class:`RequirementTemplate`.
+    """
+
+    __tablename__ = 'relationship'
+
+    __private_fields__ = ('type_fk',
+                          'source_node_fk',
+                          'target_node_fk',
+                          'target_capability_fk',
+                          'requirement_template_fk',
+                          'relationship_template_fk',
+                          'target_position',
+                          'source_position')
+
+    # region one_to_one relationships
+
+    @declared_attr
+    def target_capability(cls):
+        """
+        Target capability.
+
+        :type: :class:`Capability`
+        """
+        return relationship.one_to_one(cls, 'capability', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def tasks(cls):
+        """
+        Associated tasks.
+
+        :type: [:class:`Task`]
+        """
+        return relationship.one_to_many(cls, 'task')
+
+    @declared_attr
+    def interfaces(cls):
+        """
+        Associated interfaces.
+
+        :type: {:obj:`basestring`: :class:`Interface`}
+        """
+        return relationship.one_to_many(cls, 'interface', dict_key='name')
+
+    @declared_attr
+    def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
+        return relationship.one_to_many(cls, 'property', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def source_node(cls):
+        """
+        Source node.
+
+        :type: :class:`Node`
+        """
+        return relationship.many_to_one(
+            cls, 'node', fk='source_node_fk', back_populates='outbound_relationships')
+
+    @declared_attr
+    def target_node(cls):
+        """
+        Target node.
+
+        :type: :class:`Node`
+        """
+        return relationship.many_to_one(
+            cls, 'node', fk='target_node_fk', back_populates='inbound_relationships')
+
+    @declared_attr
+    def relationship_template(cls):
+        """
+        Source relationship template (can be ``None``).
+
+        :type: :class:`RelationshipTemplate`
+        """
+        return relationship.many_to_one(cls, 'relationship_template')
+
+    @declared_attr
+    def requirement_template(cls):
+        """
+        Source requirement template (can be ``None``).
+
+        :type: :class:`RequirementTemplate`
+        """
+        return relationship.many_to_one(cls, 'requirement_template')
+
+    @declared_attr
+    def type(cls):
+        """
+        Relationship type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region association proxies
+
+    @declared_attr
+    def source_node_name(cls):
+        return relationship.association_proxy('source_node', 'name')
+
+    @declared_attr
+    def target_node_name(cls):
+        return relationship.association_proxy('target_node', 'name')
+
+    # endregion
+
+    # region foreign keys
+
+    @declared_attr
+    def type_fk(cls):
+        """For Relationship many-to-one to Type"""
+        return relationship.foreign_key('type', nullable=True)
+
+    @declared_attr
+    def source_node_fk(cls):
+        """For Node one-to-many to Relationship"""
+        return relationship.foreign_key('node')
+
+    @declared_attr
+    def target_node_fk(cls):
+        """For Node one-to-many to Relationship"""
+        return relationship.foreign_key('node')
+
+    @declared_attr
+    def target_capability_fk(cls):
+        """For Relationship one-to-one to Capability"""
+        return relationship.foreign_key('capability', nullable=True)
+
+    @declared_attr
+    def requirement_template_fk(cls):
+        """For Relationship many-to-one to RequirementTemplate"""
+        return relationship.foreign_key('requirement_template', nullable=True)
+
+    @declared_attr
+    def relationship_template_fk(cls):
+        """For Relationship many-to-one to RelationshipTemplate"""
+        return relationship.foreign_key('relationship_template', nullable=True)
+
+    # endregion
+
+    source_position = Column(Integer, doc="""
+    Position at source.
+
+    :type: :obj:`int`
+    """)
+
+    target_position = Column(Integer, doc="""
+    Position at target.
+
+    :type: :obj:`int`
+    """)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('target_node_id', self.target_node.name),
+            ('type_name', self.type.name
+             if self.type is not None else None),
+            ('template_name', self.relationship_template.name
+             if self.relationship_template is not None else None),
+            ('properties', formatting.as_raw_dict(self.properties)),
+            ('interfaces', formatting.as_raw_list(self.interfaces))))
+
+
+class CapabilityBase(InstanceModelMixin):
+    """
+    Typed attachment serving two purposes: to provide extra properties and attributes to a
+    :class:`Node`, and to expose targets for :class:`Relationship` instances from other nodes.
+
+    Usually an instance of a :class:`CapabilityTemplate`.
+    """
+
+    __tablename__ = 'capability'
+
+    __private_fields__ = ('capability_fk',
+                          'node_fk',
+                          'capability_template_fk')
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
+        return relationship.one_to_many(cls, 'property', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def node(cls):
+        """
+        Containing node.
+
+        :type: :class:`Node`
+        """
+        return relationship.many_to_one(cls, 'node')
+
+    @declared_attr
+    def capability_template(cls):
+        """
+        Source capability template (can be ``None``).
+
+        :type: :class:`CapabilityTemplate`
+        """
+        return relationship.many_to_one(cls, 'capability_template')
+
+    @declared_attr
+    def type(cls):
+        """
+        Capability type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def type_fk(cls):
+        """For Capability many-to-one to Type"""
+        return relationship.foreign_key('type')
+
+    @declared_attr
+    def node_fk(cls):
+        """For Node one-to-many to Capability"""
+        return relationship.foreign_key('node')
+
+    @declared_attr
+    def capability_template_fk(cls):
+        """For Capability many-to-one to CapabilityTemplate"""
+        return relationship.foreign_key('capability_template', nullable=True)
+
+    # endregion
+
+    min_occurrences = Column(Integer, default=None, doc="""
+    Minimum number of requirement matches required.
+
+    :type: :obj:`int`
+    """)
+
+    max_occurrences = Column(Integer, default=None, doc="""
+    Maximum number of requirement matches allowed.
+
+    :type: :obj:`int`
+    """)
+
+    occurrences = Column(Integer, default=0, doc="""
+    Number of requirement matches.
+
+    :type: :obj:`int`
+    """)
+
+    @property
+    def has_enough_relationships(self):
+        if self.min_occurrences is not None:
+            return self.occurrences >= self.min_occurrences
+        return True
+
+    def relate(self):
+        if self.max_occurrences is not None:
+            if self.occurrences == self.max_occurrences:
+                return False
+        self.occurrences += 1
+        return True
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('type_name', self.type.name),
+            ('properties', formatting.as_raw_dict(self.properties))))
+
+
+class InterfaceBase(InstanceModelMixin):
+    """
+    Typed bundle of :class:`Operation` instances.
+
+    Can be associated with a :class:`Node`, a :class:`Group`, or a :class:`Relationship`.
+
+    Usually an instance of a :class:`InterfaceTemplate`.
+    """
+
+    __tablename__ = 'interface'
+
+    __private_fields__ = ('type_fk',
+                          'node_fk',
+                          'group_fk',
+                          'relationship_fk',
+                          'interface_template_fk')
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def inputs(cls):
+        """
+        Parameters for all operations of the interface.
+
+        :type: {:obj:`basestring`: :class:`Input`}
+        """
+        return relationship.one_to_many(cls, 'input', dict_key='name')
+
+    @declared_attr
+    def operations(cls):
+        """
+        Associated operations.
+
+        :type: {:obj:`basestring`: :class:`Operation`}
+        """
+        return relationship.one_to_many(cls, 'operation', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def node(cls):
+        """
+        Containing node (can be ``None``).
+
+        :type: :class:`Node`
+        """
+        return relationship.many_to_one(cls, 'node')
+
+    @declared_attr
+    def group(cls):
+        """
+        Containing group (can be ``None``).
+
+        :type: :class:`Group`
+        """
+        return relationship.many_to_one(cls, 'group')
+
+    @declared_attr
+    def relationship(cls):
+        """
+        Containing relationship (can be ``None``).
+
+        :type: :class:`Relationship`
+        """
+        return relationship.many_to_one(cls, 'relationship')
+
+    @declared_attr
+    def interface_template(cls):
+        """
+        Source interface template (can be ``None``).
+
+        :type: :class:`InterfaceTemplate`
+        """
+        return relationship.many_to_one(cls, 'interface_template')
+
+    @declared_attr
+    def type(cls):
+        """
+        Interface type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def type_fk(cls):
+        """For Interface many-to-one to Type"""
+        return relationship.foreign_key('type')
+
+    @declared_attr
+    def node_fk(cls):
+        """For Node one-to-many to Interface"""
+        return relationship.foreign_key('node', nullable=True)
+
+    @declared_attr
+    def group_fk(cls):
+        """For Group one-to-many to Interface"""
+        return relationship.foreign_key('group', nullable=True)
+
+    @declared_attr
+    def relationship_fk(cls):
+        """For Relationship one-to-many to Interface"""
+        return relationship.foreign_key('relationship', nullable=True)
+
+    @declared_attr
+    def interface_template_fk(cls):
+        """For Interface many-to-one to InterfaceTemplate"""
+        return relationship.foreign_key('interface_template', nullable=True)
+
+    # endregion
+
+    description = Column(Text, doc="""
+    Human-readable description.
+
+    :type: :obj:`basestring`
+    """)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('description', self.description),
+            ('type_name', self.type.name),
+            ('inputs', formatting.as_raw_dict(self.inputs)),
+            ('operations', formatting.as_raw_list(self.operations))))
+
+
+class OperationBase(InstanceModelMixin):
+    """
+    Entry points to Python functions called as part of a workflow execution.
+
+    The operation signature (its :attr:`name` and its :attr:`inputs`'s names and types) is declared
+    by the type of the :class:`Interface`, however each operation can provide its own
+    :attr:`implementation` as well as additional inputs.
+
+    The Python :attr:`function` is usually provided by an associated :class:`Plugin`. Its purpose is
+    to execute the implementation, providing it with both the operation's and interface's inputs.
+    The :attr:`arguments` of the function should be set according to the specific signature of the
+    function.
+
+    Additionally, :attr:`configuration` parameters can be provided as hints to configure the
+    function's behavior. For example, they can be used to configure remote execution credentials.
+
+    Might be an instance of :class:`OperationTemplate`.
+    """
+
+    __tablename__ = 'operation'
+
+    __private_fields__ = ('service_fk',
+                          'interface_fk',
+                          'plugin_fk',
+                          'operation_template_fk')
+
+    # region one_to_one relationships
+
+    @declared_attr
+    def plugin(cls):
+        """
+        Associated plugin.
+
+        :type: :class:`Plugin`
+        """
+        return relationship.one_to_one(cls, 'plugin', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def inputs(cls):
+        """
+        Parameters provided to the :attr:`implementation`.
+
+        :type: {:obj:`basestring`: :class:`Input`}
+        """
+        return relationship.one_to_many(cls, 'input', dict_key='name')
+
+    @declared_attr
+    def arguments(cls):
+        """
+        Arguments sent to the Python :attr:`function`.
+
+        :type: {:obj:`basestring`: :class:`Argument`}
+        """
+        return relationship.one_to_many(cls, 'argument', dict_key='name')
+
+    @declared_attr
+    def configurations(cls):
+        """
+        Configuration parameters for the Python :attr:`function`.
+
+        :type: {:obj:`basestring`: :class:`Configuration`}
+        """
+        return relationship.one_to_many(cls, 'configuration', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def service(cls):
+        """
+        Containing service (can be ``None``). For workflow operations.
+
+        :type: :class:`Service`
+        """
+        return relationship.many_to_one(cls, 'service', back_populates='workflows')
+
+    @declared_attr
+    def interface(cls):
+        """
+        Containing interface (can be ``None``).
+
+        :type: :class:`Interface`
+        """
+        return relationship.many_to_one(cls, 'interface')
+
+    @declared_attr
+    def operation_template(cls):
+        """
+        Source operation template (can be ``None``).
+
+        :type: :class:`OperationTemplate`
+        """
+        return relationship.many_to_one(cls, 'operation_template')
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def service_fk(cls):
+        """For Service one-to-many to Operation"""
+        return relationship.foreign_key('service', nullable=True)
+
+    @declared_attr
+    def interface_fk(cls):
+        """For Interface one-to-many to Operation"""
+        return relationship.foreign_key('interface', nullable=True)
+
+    @declared_attr
+    def plugin_fk(cls):
+        """For Operation one-to-one to Plugin"""
+        return relationship.foreign_key('plugin', nullable=True)
+
+    @declared_attr
+    def operation_template_fk(cls):
+        """For Operation many-to-one to OperationTemplate"""
+        return relationship.foreign_key('operation_template', nullable=True)
+
+    # endregion
+
+    description = Column(Text, doc="""
+    Human-readable description.
+
+    :type: :obj:`basestring`
+    """)
+
+    relationship_edge = Column(Boolean, doc="""
+    When ``True`` specifies that the operation is on the relationship's target edge; ``False`` is
+    the source edge (only used by operations on relationships)
+
+    :type: :obj:`bool`
+    """)
+
+    implementation = Column(Text, doc="""
+    Implementation (usually the name of an artifact).
+
+    :type: :obj:`basestring`
+    """)
+
+    dependencies = Column(modeling_types.StrictList(item_cls=basestring), doc="""
+    Dependencies (usually names of artifacts).
+
+    :type: [:obj:`basestring`]
+    """)
+
+    function = Column(Text, doc="""
+    Full path to Python function.
+
+    :type: :obj:`basestring`
+    """)
+
+    executor = Column(Text, doc="""
+    Name of executor.
+
+    :type: :obj:`basestring`
+    """)
+
+    max_attempts = Column(Integer, doc="""
+    Maximum number of attempts allowed in case of task failure.
+
+    :type: :obj:`int`
+    """)
+
+    retry_interval = Column(Integer, doc="""
+    Interval between task retry attempts (in seconds).
+
+    :type: :obj:`float`
+    """)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('description', self.description),
+            ('implementation', self.implementation),
+            ('dependencies', self.dependencies),
+            ('inputs', formatting.as_raw_dict(self.inputs))))
+
+
+class ArtifactBase(InstanceModelMixin):
+    """
+    Typed file, either provided in a CSAR or downloaded from a repository.
+
+    Usually an instance of :class:`ArtifactTemplate`.
+    """
+
+    __tablename__ = 'artifact'
+
+    __private_fields__ = ('type_fk',
+                          'node_fk',
+                          'artifact_template_fk')
+
+    # region one_to_many relationships
+
+    @declared_attr
+    def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
+        return relationship.one_to_many(cls, 'property', dict_key='name')
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def node(cls):
+        """
+        Containing node.
+
+        :type: :class:`Node`
+        """
+        return relationship.many_to_one(cls, 'node')
+
+    @declared_attr
+    def artifact_template(cls):
+        """
+        Source artifact template (can be ``None``).
+
+        :type: :class:`ArtifactTemplate`
+        """
+        return relationship.many_to_one(cls, 'artifact_template')
+
+    @declared_attr
+    def type(cls):
+        """
+        Artifact type.
+
+        :type: :class:`Type`
+        """
+        return relationship.many_to_one(cls, 'type', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    # region foreign_keys
+
+    @declared_attr
+    def type_fk(cls):
+        """For Artifact many-to-one to Type"""
+        return relationship.foreign_key('type')
+
+    @declared_attr
+    def node_fk(cls):
+        """For Node one-to-many to Artifact"""
+        return relationship.foreign_key('node')
+
+    @declared_attr
+    def artifact_template_fk(cls):
+        """For Artifact many-to-one to ArtifactTemplate"""
+        return relationship.foreign_key('artifact_template', nullable=True)
+
+    # endregion
+
+    description = Column(Text, doc="""
+    Human-readable description.
+
+    :type: :obj:`basestring`
+    """)
+
+    source_path = Column(Text, doc="""
+    Source path (in CSAR or repository).
+
+    :type: :obj:`basestring`
+    """)
+
+    target_path = Column(Text, doc="""
+    Path at which to install at destination.
+
+    :type: :obj:`basestring`
+    """)
+
+    repository_url = Column(Text, doc="""
+    Repository URL.
+
+    :type: :obj:`basestring`
+    """)
+
+    repository_credential = Column(modeling_types.StrictDict(basestring, basestring), doc="""
+    Credentials for accessing the repository.
+
+    :type: {:obj:`basestring`, :obj:`basestring`}
+    """)
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('description', self.description),
+            ('type_name', self.type.name),
+            ('source_path', self.source_path),
+            ('target_path', self.target_path),
+            ('repository_url', self.repository_url),
+            ('repository_credential', formatting.as_agnostic(self.repository_credential)),
+            ('properties', formatting.as_raw_dict(self.properties))))