You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ariatosca.apache.org by em...@apache.org on 2017/06/28 02:50:21 UTC

[2/3] incubator-ariatosca git commit: Full documentation of models; fix to subtitution

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/4ebbbb15/aria/modeling/service_instance.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_instance.py b/aria/modeling/service_instance.py
index 033785d..fa79091 100644
--- a/aria/modeling/service_instance.py
+++ b/aria/modeling/service_instance.py
@@ -49,45 +49,9 @@ from . import (
 
 class ServiceBase(InstanceModelMixin):
     """
-    A service is usually an instance of a :class:`ServiceTemplate`.
-
-    You will usually not create it programmatically, but instead instantiate it from a service
-    template.
-
-    :ivar name: name (unique for this ARIA installation)
-    :vartype name: basestring
-    :ivar service_template: template from which this service was instantiated (optional)
-    :vartype service_template: ServiceTemplate
-    :ivar description: human-readable description
-    :vartype description: basestring
-    :ivar meta_data: custom annotations
-    :vartype meta_data: {:obj:`basestring`: :class:`Metadata`}
-    :ivar nodes: nodes
-    :vartype nodes: {:obj:`basestring`: :class:`Node`}
-    :ivar groups: groups of nodes
-    :vartype groups: {:obj:`basestring`: :class:`Group`}
-    :ivar policies: policies
-    :vartype policies: {:obj:`basestring`: :class:`Policy`]}
-    :ivar substitution: the entire service can appear as a node
-    :vartype substitution: :class:`Substitution`
-    :ivar inputs: externally provided parameters
-    :vartype inputs: {:obj:`basestring`: :class:`Input`}
-    :ivar outputs: these parameters are filled in after service installation
-    :vartype outputs: {:obj:`basestring`: :class:`Output`}
-    :ivar workflows: custom workflows that can be performed on the service
-    :vartype workflows: {:obj:`basestring`: :class:`Operation`}
-    :ivar plugins: plugins used by the service
-    :vartype plugins: {:obj:`basestring`: :class:`Plugin`}
-    :ivar created_at: creation timestamp
-    :vartype created_at: :class:`datetime.datetime`
-    :ivar updated_at: update timestamp
-    :vartype updated_at: :class:`datetime.datetime`
-    :ivar modifications: modifications of this service
-    :vartype modifications: [:class:`ServiceModification`]
-    :ivar updates: updates of this service
-    :vartype updates: [:class:`ServiceUpdate`]
-    :ivar executions: executions on this service
-    :vartype executions: [:class:`Execution`]
+    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'
@@ -95,20 +59,6 @@ class ServiceBase(InstanceModelMixin):
     __private_fields__ = ('substitution_fk',
                           'service_template_fk')
 
-    # 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
-
     # region association proxies
 
     @declared_attr
@@ -122,6 +72,11 @@ class ServiceBase(InstanceModelMixin):
 
     @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
@@ -130,38 +85,83 @@ class ServiceBase(InstanceModelMixin):
 
     @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
@@ -170,6 +170,11 @@ class ServiceBase(InstanceModelMixin):
 
     @declared_attr
     def service_template(cls):
+        """
+        Source service template (can be ``None``).
+
+        :type: :class:`ServiceTemplate`
+        """
         return relationship.many_to_one(cls, 'service_template')
 
     # endregion
@@ -178,18 +183,56 @@ class ServiceBase(InstanceModelMixin):
 
     @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
 
-    description = Column(Text)
-    created_at = Column(DateTime, nullable=False, index=True)
-    updated_at = Column(DateTime)
+    # 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
@@ -315,48 +358,12 @@ class ServiceBase(InstanceModelMixin):
 
 class NodeBase(InstanceModelMixin):
     """
-    Usually an instance of a :class:`NodeTemplate`.
+    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.
 
-    Nodes may have zero or more :class:`Relationship` instances to other nodes.
-
-    :ivar name: name (unique for this service)
-    :vartype name: basestring
-    :ivar node_template: template from which this node was instantiated (optional)
-    :vartype node_template: :class:`NodeTemplate`
-    :ivar type: node type
-    :vartype type: Type
-    :ivar description: human-readable description
-    :vartype description: basestring
-    :ivar properties: associated parameters
-    :vartype properties: {:obj:`basestring`: :class:`Property`}
-    :ivar interfaces: bundles of operations
-    :vartype interfaces: {:obj:`basestring`: :class:`Interface`}
-    :ivar artifacts: associated files
-    :vartype artifacts: {:obj:`basestring`: :class:`Artifact`}
-    :ivar capabilities: exposed capabilities
-    :vartype capabilities: {:obj:`basestring`: :class:`Capability`}
-    :ivar outbound_relationships: relationships to other nodes
-    :vartype outbound_relationships: [:class:`Relationship`]
-    :ivar inbound_relationships: relationships from other nodes
-    :vartype inbound_relationships: [:class:`Relationship`]
-    :ivar host: host node (can be self)
-    :vartype host: :class:`Node`
-    :ivar state: the state of the node, according to to the TOSCA-defined node states
-    :vartype state: string
-    :ivar version: used by :mod:`aria.storage.instrumentation`
-    :vartype version: int
-    :ivar service: containing service
-    :vartype service: :class:`Service`
-    :ivar groups: we are a member of these groups
-    :vartype groups: [:class:`Group`]
-    :ivar policies: policies enacted on this node
-    :vartype policies: [:class:`Policy`]
-    :ivar substitution_mapping: our contribution to service substitution
-    :vartype substitution_mapping: :class:`SubstitutionMapping`
-    :ivar tasks: tasks for this node
-    :vartype tasks: [:class:`Task`]
-    :ivar hosted_tasks: tasks on this node
-    :vartype hosted_tasks: [:class:`Task`]
+    Usually an instance of a :class:`NodeTemplate`.
     """
 
     __tablename__ = 'node'
@@ -375,67 +382,24 @@ class NodeBase(InstanceModelMixin):
     STARTED = 'started'
     STOPPING = 'stopping'
     DELETING = 'deleting'
-    # '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.
     DELETED = 'deleted'
     ERROR = 'error'
 
-    STATES = [INITIAL, CREATING, CREATED, CONFIGURING, CONFIGURED, STARTING, STARTED, STOPPING,
-              DELETING, DELETED, 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},
+    _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}}
 
-    @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)
-
-    # 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
-
     # region association proxies
 
     @declared_attr
@@ -454,6 +418,14 @@ class NodeBase(InstanceModelMixin):
 
     @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
@@ -462,30 +434,65 @@ class NodeBase(InstanceModelMixin):
 
     @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', child_fk='source_node_fk', back_populates='source_node',
             rel_kwargs=dict(
@@ -496,6 +503,11 @@ class NodeBase(InstanceModelMixin):
 
     @declared_attr
     def inbound_relationships(cls):
+        """
+        Relationships from other nodes.
+
+        :type: [:class:`Relationship`]
+        """
         return relationship.one_to_many(
             cls, 'relationship', child_fk='target_node_fk', back_populates='target_node',
             rel_kwargs=dict(
@@ -510,24 +522,97 @@ class NodeBase(InstanceModelMixin):
 
     @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
 
-    description = Column(Text)
-    state = Column(Enum(*STATES, name='node_state'), nullable=False, default=INITIAL)
-    version = Column(Integer, default=1)
+    # 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:
@@ -702,26 +787,9 @@ class NodeBase(InstanceModelMixin):
 
 class GroupBase(InstanceModelMixin):
     """
-    Usually an instance of a :class:`GroupTemplate`.
+    Typed logical container for zero or more :class:`Node` instances.
 
-    :ivar name: name (unique for this service)
-    :vartype name: basestring
-    :ivar group_template: template from which this group was instantiated (optional)
-    :vartype group_template: :class:`GroupTemplate`
-    :ivar type: group type
-    :vartype type: Type
-    :ivar description: human-readable description
-    :vartype description: basestring
-    :ivar nodes: members of this group
-    :vartype nodes: [:class:`Node`]
-    :ivar properties: associated parameters
-    :vartype properties: {:obj:`basestring`: :class:`Property`}
-    :ivar interfaces: bundles of operations
-    :vartype interfaces: {:obj:`basestring`: :class:`Interface`}
-    :ivar service: containing service
-    :vartype service: :class:`Service`
-    :ivar policies: policies enacted on this group
-    :vartype policies: [:class:`Policy`]
+    Usually an instance of a :class:`GroupTemplate`.
     """
 
     __tablename__ = 'group'
@@ -730,25 +798,6 @@ class GroupBase(InstanceModelMixin):
                           'service_fk',
                           'group_template_fk')
 
-    # 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
-
     # region association proxies
 
     # endregion
@@ -761,10 +810,20 @@ class GroupBase(InstanceModelMixin):
 
     @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
@@ -773,14 +832,29 @@ class GroupBase(InstanceModelMixin):
 
     @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
@@ -789,11 +863,39 @@ class GroupBase(InstanceModelMixin):
 
     @declared_attr
     def nodes(cls):
+        """
+        Member nodes.
+
+        :type: [:class:`Node`]
+        """
         return relationship.many_to_many(cls, 'node')
 
     # endregion
 
-    description = Column(Text)
+    # 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():
@@ -830,24 +932,10 @@ class GroupBase(InstanceModelMixin):
 
 class PolicyBase(InstanceModelMixin):
     """
-    Usually an instance of a :class:`PolicyTemplate`.
+    Typed set of orchestration hints applied to zero or more :class:`Node` or :class:`Group`
+    instances.
 
-    :ivar name: name (unique for this service)
-    :vartype name: basestring
-    :ivar policy_template: template from which this policy was instantiated (optional)
-    :vartype policy_template: :class:`PolicyTemplate`
-    :ivar type: policy type
-    :vartype type: Type
-    :ivar description: human-readable description
-    :vartype description: basestring
-    :ivar nodes: policy will be enacted on all these nodes
-    :vartype nodes: [:class:`Node`]
-    :ivar groups: policy will be enacted on all nodes in these groups
-    :vartype groups: [:class:`Group`]
-    :ivar properties: associated parameters
-    :vartype properties: {:obj:`basestring`: :class:`Property`}
-    :ivar service: containing service
-    :vartype service: :class:`Service`
+    Usually an instance of a :class:`PolicyTemplate`.
     """
 
     __tablename__ = 'policy'
@@ -856,25 +944,6 @@ class PolicyBase(InstanceModelMixin):
                           'service_fk',
                           'policy_template_fk')
 
-    # 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
-
     # region association proxies
 
     # endregion
@@ -887,6 +956,11 @@ class PolicyBase(InstanceModelMixin):
 
     @declared_attr
     def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
         return relationship.one_to_many(cls, 'property', dict_key='name')
 
     # endregion
@@ -895,14 +969,29 @@ class PolicyBase(InstanceModelMixin):
 
     @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
@@ -911,15 +1000,48 @@ class PolicyBase(InstanceModelMixin):
 
     @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
 
-    description = Column(Text)
+    # 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):
@@ -954,18 +1076,9 @@ class PolicyBase(InstanceModelMixin):
 
 class SubstitutionBase(InstanceModelMixin):
     """
-    Used to substitute a single node for the entire deployment.
+    Exposes the entire service as a single node.
 
     Usually an instance of a :class:`SubstitutionTemplate`.
-
-    :ivar substitution_template: template from which this substitution was instantiated (optional)
-    :vartype substitution_template: :class:`SubstitutionTemplate`
-    :ivar node_type: exposed node type
-    :vartype node_type: :class:`Type`
-    :ivar mappings: requirement and capability mappings
-    :vartype mappings: {:obj:`basestring`: :class:`SubstitutionTemplate`}
-    :ivar service: containing service
-    :vartype service: :class:`Service`
     """
 
     __tablename__ = 'substitution'
@@ -973,21 +1086,7 @@ class SubstitutionBase(InstanceModelMixin):
     __private_fields__ = ('node_type_fk',
                           'substitution_template_fk')
 
-    # 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
-
-    # region association proxies
+    # region association proxies
 
     # endregion
 
@@ -999,6 +1098,11 @@ class SubstitutionBase(InstanceModelMixin):
 
     @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
@@ -1007,18 +1111,47 @@ class SubstitutionBase(InstanceModelMixin):
 
     @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((
@@ -1041,85 +1174,98 @@ class SubstitutionBase(InstanceModelMixin):
 
 class SubstitutionMappingBase(InstanceModelMixin):
     """
-    Used by :class:`Substitution` to map a capability or a requirement to a node.
+    Used by :class:`Substitution` to map a capability or a requirement to the exposed node.
 
-    Only one of `capability_template` and `requirement_template` can be set.
+    The :attr:`name` field should match the capability or requirement template name on the exposed
+    node's type.
 
-    Usually an instance of a :class:`SubstitutionTemplate`.
+    Only one of :attr:`capability` and :attr:`requirement_template` can be set. If the latter is
+    set, then :attr:`node` must also be set.
 
-    :ivar name: exposed capability or requirement name
-    :vartype name: basestring
-    :ivar node: node
-    :vartype node: Node
-    :ivar capability: capability in the node
-    :vartype capability: :class:`Capability`
-    :ivar requirement_template: requirement template in the node template
-    :vartype requirement_template: :class:`RequirementTemplate`
-    :ivar substitution: containing substitution
-    :vartype substitution: :class:`Substitution`
+    Usually an instance of a :class:`SubstitutionMappingTemplate`.
     """
 
     __tablename__ = 'substitution_mapping'
 
     __private_fields__ = ('substitution_fk',
-                          'node_fk',
                           'capability_fk',
-                          'requirement_template_fk')
+                          'requirement_template_fk',
+                          'node_fk')
 
-    # region foreign keys
+    # region association proxies
 
-    @declared_attr
-    def substitution_fk(cls):
-        """For Substitution one-to-many to SubstitutionMapping"""
-        return relationship.foreign_key('substitution')
+    # endregion
+
+    # region one_to_one relationships
 
     @declared_attr
-    def node_fk(cls):
-        """For Substitution one-to-one to NodeTemplate"""
-        return relationship.foreign_key('node')
+    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 capability_fk(cls):
-        """For Substitution one-to-one to Capability"""
-        return relationship.foreign_key('capability', nullable=True)
+    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 requirement_template_fk(cls):
-        """For Substitution one-to-one to RequirementTemplate"""
-        return relationship.foreign_key('requirement_template', nullable=True)
+    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 association proxies
+    # region one_to_many relationships
 
     # endregion
 
-    # region one_to_one relationships
+    # 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')
 
-    @declared_attr
-    def node(cls):
-        return relationship.one_to_one(cls, 'node', back_populates=relationship.NO_BACK_POP)
+    # endregion
 
-    @declared_attr
-    def capability(cls):
-        return relationship.one_to_one(cls, 'capability', back_populates=relationship.NO_BACK_POP)
+    # region foreign keys
 
     @declared_attr
-    def requirement_template(cls):
-        return relationship.one_to_one(
-            cls, 'requirement_template', back_populates=relationship.NO_BACK_POP)
-
-    # endregion
+    def substitution_fk(cls):
+        """For Substitution one-to-many to SubstitutionMapping"""
+        return relationship.foreign_key('substitution')
 
-    # region one_to_many relationships
+    @declared_attr
+    def capability_fk(cls):
+        """For Substitution one-to-one to Capability"""
+        return relationship.foreign_key('capability', nullable=True)
 
-    # endregion
+    @declared_attr
+    def node_fk(cls):
+        """For Substitution one-to-one to Node"""
+        return relationship.foreign_key('node', nullable=True)
 
-    # region many_to_one relationships
+    @declared_attr
+    def requirement_template_fk(cls):
+        """For Substitution one-to-one to RequirementTemplate"""
+        return relationship.foreign_key('requirement_template', nullable=True)
 
     # endregion
 
@@ -1142,44 +1288,24 @@ class SubstitutionMappingBase(InstanceModelMixin):
 
     def dump(self):
         context = ConsumptionContext.get_thread_local()
-        console.puts('{0} -> {1}.{2}'.format(
-            context.style.node(self.name),
-            context.style.node(self.node.name),
-            context.style.node(self.capability.name
-                               if self.capability
-                               else self.requirement_template.name)))
+        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):
     """
-    Connects :class:`Node` to a capability in another node.
-
-    Might be an instance of a :class:`RelationshipTemplate`.
-
-    :ivar name: name (usually the name of the requirement at the source node template)
-    :vartype name: basestring
-    :ivar relationship_template: template from which this relationship was instantiated (optional)
-    :vartype relationship_template: :class:`RelationshipTemplate`
-    :ivar requirement_template: template from which this relationship was instantiated (optional)
-    :vartype requirement_template: :class:`RequirementTemplate`
-    :ivar type: relationship type
-    :vartype type: Type
-    :ivar target_capability: capability at the target node (optional)
-    :vartype target_capability: :class:`Capability`
-    :ivar properties: associated parameters
-    :vartype properties: {:obj:`basestring`: :class:`Property`}
-    :ivar interfaces: bundles of operations
-    :vartype interfaces: {:obj:`basestring`: :class:`Interfaces`}
-    :ivar source_position: position of the relationship in the outbound relationships.
-    :vartype source_position: int
-    :ivar target_position: position of the relationship in the inbound relationships.
-    :vartype target_position: int
-    :ivar source_node: source node
-    :vartype source_node: :class:`Node`
-    :ivar target_node: target node
-    :vartype target_node: :class:`Node`
-    :ivar tasks: tasks for this relationship
-    :vartype tasks: [:class:`Task`]
+    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'
@@ -1193,40 +1319,6 @@ class RelationshipBase(InstanceModelMixin):
                           'target_position',
                           'source_position')
 
-    # 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
-
     # region association proxies
 
     @declared_attr
@@ -1245,6 +1337,11 @@ class RelationshipBase(InstanceModelMixin):
 
     @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
@@ -1253,14 +1350,29 @@ class RelationshipBase(InstanceModelMixin):
 
     @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
@@ -1269,30 +1381,98 @@ class RelationshipBase(InstanceModelMixin):
 
     @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
 
-    source_position = Column(Integer)
-    target_position = Column(Integer)
+    # 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():
@@ -1340,30 +1520,10 @@ class RelationshipBase(InstanceModelMixin):
 
 class CapabilityBase(InstanceModelMixin):
     """
-    A capability of a :class:`Node`.
+    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`.
-
-    :ivar name: Name (unique for the node)
-    :vartype name: basestring
-    :ivar capability_template: template from which this capability was instantiated (optional)
-    :vartype capability_template: :class:`capabilityTemplate`
-    :ivar type: capability type
-    :vartype type: Type
-    :ivar min_occurrences: minimum number of requirement matches required
-    :vartype min_occurrences: int
-    :ivar max_occurrences: maximum number of requirement matches allowed
-    :vartype min_occurrences: int
-    :ivar occurrences: actual number of requirement matches
-    :vartype occurrences: int
-    :ivar properties: associated parameters
-    :vartype properties: {:obj:`basestring`: :class:`Property`}
-    :ivar node: containing node
-    :vartype node: Node
-    :ivar relationship: available when we are the target of a relationship
-    :vartype relationship: :class:`Relationship`
-    :ivar substitution_mapping: our contribution to service substitution
-    :vartype substitution_mapping: :class:`SubstitutionMapping`
     """
 
     __tablename__ = 'capability'
@@ -1372,25 +1532,6 @@ class CapabilityBase(InstanceModelMixin):
                           'node_fk',
                           'capability_template_fk')
 
-    # 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
-
     # region association proxies
 
     # endregion
@@ -1403,6 +1544,11 @@ class CapabilityBase(InstanceModelMixin):
 
     @declared_attr
     def properties(cls):
+        """
+        Associated immutable parameters.
+
+        :type: {:obj:`basestring`: :class:`Property`}
+        """
         return relationship.one_to_many(cls, 'property', dict_key='name')
 
     # endregion
@@ -1411,21 +1557,69 @@ class CapabilityBase(InstanceModelMixin):
 
     @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
 
-    min_occurrences = Column(Integer, default=None)
-    max_occurrences = Column(Integer, default=None)
-    occurrences = Column(Integer, default=0)
+    # 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):
@@ -1469,28 +1663,11 @@ class CapabilityBase(InstanceModelMixin):
 
 class InterfaceBase(InstanceModelMixin):
     """
-    A typed set of :class:`Operation`.
-
-    Usually an instance of :class:`InterfaceTemplate`.
-
-    :ivar name: name (unique for the node, group, or relationship)
-    :vartype name: basestring
-    :ivar interface_template: template from which this interface was instantiated (optional)
-    :vartype interface_template: :class:`InterfaceTemplate`
-    :ivar type: interface type
-    :vartype type: Type
-    :ivar description: human-readable description
-    :vartype description: basestring
-    :ivar inputs: inputs that can be used by all operations in the interface
-    :vartype inputs: {:obj:`basestring`: :class:`Input`}
-    :ivar operations: operations
-    :vartype operations: {:obj:`basestring`: :class:`Operation`}
-    :ivar node: Containing node
-    :vartype node: Node
-    :ivar group: containing group
-    :vartype group: :class:`Group`
-    :ivar relationship: containing relationship
-    :vartype relationship: :class:`Relationship`
+    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'
@@ -1501,35 +1678,6 @@ class InterfaceBase(InstanceModelMixin):
                           'relationship_fk',
                           'interface_template_fk')
 
-    # 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
-
     # region association proxies
 
     # endregion
@@ -1542,10 +1690,20 @@ class InterfaceBase(InstanceModelMixin):
 
     @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
@@ -1554,27 +1712,85 @@ class InterfaceBase(InstanceModelMixin):
 
     @declared_attr
     def node(cls):
-        return relationship.many_to_one(cls, 'node')
+        """
+        Containing node (can be ``None``).
 
-    @declared_attr
-    def relationship(cls):
-        return relationship.many_to_one(cls, 'relationship')
+        :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
 
-    description = Column(Text)
+    # 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():
@@ -1610,44 +1826,21 @@ class InterfaceBase(InstanceModelMixin):
 
 class OperationBase(InstanceModelMixin):
     """
-    An operation in a :class:`Interface`.
+    Entry points to Python functions called as part of a workflow execution.
 
-    Might be an instance of :class:`OperationTemplate`.
+    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.
 
-    :ivar name: name (unique for the interface or service)
-    :vartype name: basestring
-    :ivar operation_template: template from which this operation was instantiated (optional)
-    :vartype operation_template: :class:`OperationTemplate`
-    :ivar description: human-readable description
-    :vartype description: basestring
-    :ivar relationship_edge: when ``True`` specifies that the operation is on the relationship's
-                             target edge instead of its source (only used by relationship
-                             operations)
-    :vartype relationship_edge: bool
-    :ivar implementation: implementation (interpreted by the plugin)
-    :vartype implementation: basestring
-    :ivar dependencies: dependency strings (interpreted by the plugin)
-    :vartype dependencies: [obj:`basestring`]
-    :ivar inputs: input that can be used by this operation
-    :vartype inputs: {:obj:`basestring`: :class:`Input`}
-    :ivar plugin: associated plugin
-    :vartype plugin: Plugin
-    :ivar configurations: configuration (interpreted by the plugin)
-    :vartype configurations: {obj:`basestring`: :class:`Configuration`}
-    :ivar function: name of the operation function
-    :vartype function: basestring
-    :ivar arguments: arguments to send to the operation function
-    :vartype arguments: {:obj:`basestring`: :class:`Argument`}
-    :ivar executor: name of executor to run the operation with
-    :vartype executor: basestring
-    :ivar max_attempts: maximum number of attempts allowed in case of failure
-    :vartype max_attempts: int
-    :ivar retry_interval: interval between retries (in seconds)
-    :vartype retry_interval: int
-    :ivar interface: containing interface
-    :vartype interface: Interface
-    :ivar service: containing service
-    :vartype service: Service
+    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'
@@ -1657,30 +1850,6 @@ class OperationBase(InstanceModelMixin):
                           'plugin_fk',
                           'operation_template_fk')
 
-    # 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
-
     # region association proxies
 
     # endregion
@@ -1689,6 +1858,11 @@ class OperationBase(InstanceModelMixin):
 
     @declared_attr
     def plugin(cls):
+        """
+        Associated plugin.
+
+        :type: :class:`Plugin`
+        """
         return relationship.one_to_one(cls, 'plugin', back_populates=relationship.NO_BACK_POP)
 
     # endregion
@@ -1697,30 +1871,60 @@ class OperationBase(InstanceModelMixin):
 
     @declared_attr
     def inputs(cls):
-        return relationship.one_to_many(cls, 'input', dict_key='name')
+        """
+        Parameters provided to the :attr:`implementation`.
 
-    @declared_attr
-    def configurations(cls):
-        return relationship.one_to_many(cls, 'configuration', dict_key='name')
+        :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
@@ -1729,14 +1933,78 @@ class OperationBase(InstanceModelMixin):
 
     # endregion
 
-    description = Column(Text)
-    relationship_edge = Column(Boolean)
-    implementation = Column(Text)
-    dependencies = Column(modeling_types.StrictList(item_cls=basestring))
-    function = Column(Text)
-    executor = Column(Text)
-    max_attempts = Column(Integer)
-    retry_interval = Column(Integer)
+    # 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):
@@ -1829,30 +2097,9 @@ class OperationBase(InstanceModelMixin):
 
 class ArtifactBase(InstanceModelMixin):
     """
-    A file associated with a :class:`Node`.
+    Typed file, either provided in a CSAR or downloaded from a repository.
 
     Usually an instance of :class:`ArtifactTemplate`.
-
-    :ivar name: name (unique for the node)
-    :vartype name: basestring
-    :ivar artifact_template: template from which this artifact was instantiated (optional)
-    :vartype artifact_template: ArtifactTemplate
-    :ivar type: artifact type
-    :vartype type: Type
-    :ivar description: human-readable description
-    :vartype description: basestring
-    :ivar source_path: source path (CSAR or repository)
-    :vartype source_path: basestring
-    :ivar target_path: path at destination machine
-    :vartype target_path: basestring
-    :ivar repository_url: repository URL
-    :vartype repository_path: basestring
-    :ivar repository_credential: credentials for accessing the repository
-    :vartype repository_credential: {:obj:`basestring`: :obj:`basestring`}
-    :ivar properties: associated parameters
-    :vartype properties: {:obj:`basestring`: :class:`Property`}
-    :ivar node: containing node
-    :vartype node: Node
     """
 
     __tablename__ = 'artifact'
@@ -1861,25 +2108,6 @@ class ArtifactBase(InstanceModelMixin):
                           'node_fk',
                           'artifact_template_fk')
 
-    # 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
-
     # region association proxies
 
     # endregion
@@ -1892,29 +2120,94 @@ class ArtifactBase(InstanceModelMixin):
 
     @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)
-    source_path = Column(Text)
-    target_path = Column(Text)
-    repository_url = Column(Text)
-    repository_credential = Column(modeling_types.StrictDict(basestring, basestring))
+    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):