You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ariatosca.apache.org by ra...@apache.org on 2017/04/15 14:11:34 UTC

[4/9] incubator-ariatosca git commit: ARIA-92 Automatic operation task configuration

ARIA-92 Automatic operation task configuration


Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/a7e7826e
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/a7e7826e
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/a7e7826e

Branch: refs/heads/ARIA-48-aria-cli
Commit: a7e7826ed2d8b940b9e74ca15cb0284c39b01001
Parents: 8e1d059
Author: Tal Liron <ta...@gmail.com>
Authored: Fri Mar 24 16:33:11 2017 -0500
Committer: Tal Liron <ta...@gmail.com>
Committed: Fri Apr 14 13:54:21 2017 -0500

----------------------------------------------------------------------
 aria/cli/dry.py                                 |   9 +-
 aria/modeling/models.py                         |  11 +-
 aria/modeling/orchestration.py                  |  92 +++++----
 aria/modeling/service_common.py                 |  59 +-----
 aria/modeling/service_instance.py               | 199 +++++++++++++------
 aria/modeling/service_template.py               | 133 ++++++++++++-
 aria/orchestrator/execution_plugin/__init__.py  |   2 +
 aria/orchestrator/execution_plugin/common.py    |   2 +-
 .../execution_plugin/instantiation.py           | 191 ++++++++++++++++++
 .../execution_plugin/ssh/operations.py          |   4 +-
 aria/orchestrator/workflows/api/task.py         | 131 +++++-------
 aria/orchestrator/workflows/api/task_graph.py   |   2 +-
 aria/orchestrator/workflows/builtin/utils.py    |   6 +-
 aria/orchestrator/workflows/core/engine.py      |   4 +-
 .../workflows/core/events_handler.py            |   8 +-
 aria/orchestrator/workflows/core/task.py        |   9 +-
 aria/orchestrator/workflows/exceptions.py       |  10 +-
 aria/orchestrator/workflows/executor/process.py |   2 +-
 aria/parser/consumption/modeling.py             |  22 +-
 aria/storage/instrumentation.py                 |   4 +-
 .../custom_types/elasticsearch.yaml             |   2 +
 .../multi-tier-1/custom_types/kibana.yaml       |   2 +
 .../multi-tier-1/custom_types/logstash.yaml     |   2 +
 .../paypalpizzastore_nodejs_app.yaml            |   2 +-
 .../webserver-dbms-2/webserver-dbms-2.yaml      |   6 +-
 .../profiles/aria-1.0/aria-1.0.yaml             |  10 +
 .../profiles/tosca-simple-1.0/capabilities.yaml |   2 +
 .../profiles/tosca-simple-1.0/interfaces.yaml   |  16 ++
 .../profiles/tosca-simple-1.0/nodes.yaml        |   1 +
 .../simple_v1_0/assignments.py                  |  49 +++--
 .../simple_v1_0/modeling/__init__.py            | 104 +++++++---
 .../simple_v1_0/modeling/capabilities.py        |   5 +
 tests/modeling/test_models.py                   |  65 +++---
 tests/orchestrator/context/test_operation.py    |   3 +-
 tests/orchestrator/context/test_serialize.py    |   3 +-
 tests/orchestrator/execution_plugin/test_ssh.py |  17 +-
 tests/orchestrator/workflows/api/test_task.py   |  24 +--
 .../orchestrator/workflows/builtin/__init__.py  |   3 -
 .../workflows/builtin/test_execute_operation.py |   3 +-
 tests/orchestrator/workflows/core/test_task.py  |  29 +--
 .../test_task_graph_into_exececution_graph.py   |   3 +-
 .../workflows/executor/test_executor.py         |   1 +
 .../workflows/executor/test_process_executor.py |   1 +
 .../node-cellar/node-cellar.yaml                |  36 +++-
 .../node-cellar/types/nginx.yaml                |  15 +-
 45 files changed, 886 insertions(+), 418 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/cli/dry.py
----------------------------------------------------------------------
diff --git a/aria/cli/dry.py b/aria/cli/dry.py
index 098638f..fc6c0c5 100644
--- a/aria/cli/dry.py
+++ b/aria/cli/dry.py
@@ -43,14 +43,19 @@ def convert_to_dry(service):
                 for oper in interface.operations.itervalues():
                     convert_operation_to_dry(oper)
 
+    for group in service.groups.itervalues():
+        for interface in group.interfaces.itervalues():
+            for oper in interface.operations.itervalues():
+                convert_operation_to_dry(oper)
+
 
 def convert_operation_to_dry(oper):
     """
     Converts a single :class:`Operation` to run dryly.
     """
 
-    plugin = oper.plugin_specification.name \
-        if oper.plugin_specification is not None else None
+    plugin = oper.plugin.name \
+        if oper.plugin is not None else None
     if oper.inputs is None:
         oper.inputs = OrderedDict()
     oper.inputs['_implementation'] = models.Parameter(name='_implementation',

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/modeling/models.py
----------------------------------------------------------------------
diff --git a/aria/modeling/models.py b/aria/modeling/models.py
index a01783b..170efb2 100644
--- a/aria/modeling/models.py
+++ b/aria/modeling/models.py
@@ -48,6 +48,7 @@ __all__ = (
     'InterfaceTemplate',
     'OperationTemplate',
     'ArtifactTemplate',
+    'PluginSpecification',
 
     # Service instance models
     'Service',
@@ -71,7 +72,6 @@ __all__ = (
     'Parameter',
     'Type',
     'Metadata',
-    'PluginSpecification',
 
     # Orchestration models
     'Execution',
@@ -131,6 +131,9 @@ class OperationTemplate(aria_declarative_base, service_template.OperationTemplat
 class ArtifactTemplate(aria_declarative_base, service_template.ArtifactTemplateBase):
     pass
 
+class PluginSpecification(aria_declarative_base, service_template.PluginSpecificationBase):
+    pass
+
 # endregion
 
 
@@ -211,10 +214,6 @@ class Type(aria_declarative_base, service_common.TypeBase):
 class Metadata(aria_declarative_base, service_common.MetadataBase):
     pass
 
-
-class PluginSpecification(aria_declarative_base, service_common.PluginSpecificationBase):
-    pass
-
 # endregion
 
 
@@ -253,6 +252,7 @@ models_to_register = [
     InterfaceTemplate,
     OperationTemplate,
     ArtifactTemplate,
+    PluginSpecification,
 
     # Service instance models
     Service,
@@ -276,7 +276,6 @@ models_to_register = [
     Parameter,
     Type,
     Metadata,
-    PluginSpecification,
 
     # Orchestration models
     Execution,

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/modeling/orchestration.py
----------------------------------------------------------------------
diff --git a/aria/modeling/orchestration.py b/aria/modeling/orchestration.py
index f0bd4b2..b32a8a1 100644
--- a/aria/modeling/orchestration.py
+++ b/aria/modeling/orchestration.py
@@ -67,13 +67,13 @@ class ExecutionBase(ModelMixin):
     CANCELLING = 'cancelling'
     FORCE_CANCELLING = 'force_cancelling'
 
-    STATES = [TERMINATED, FAILED, CANCELLED, PENDING, STARTED, CANCELLING, FORCE_CANCELLING]
-    END_STATES = [TERMINATED, FAILED, CANCELLED]
+    STATES = (TERMINATED, FAILED, CANCELLED, PENDING, STARTED, CANCELLING, FORCE_CANCELLING)
+    END_STATES = (TERMINATED, FAILED, CANCELLED)
 
     VALID_TRANSITIONS = {
-        PENDING: [STARTED, CANCELLED],
-        STARTED: END_STATES + [CANCELLING],
-        CANCELLING: END_STATES + [FORCE_CANCELLING]
+        PENDING: (STARTED, CANCELLED),
+        STARTED: END_STATES + (CANCELLING,),
+        CANCELLING: END_STATES + (FORCE_CANCELLING,)
     }
 
     @orm.validates('status')
@@ -219,7 +219,44 @@ class PluginBase(ModelMixin):
 
 class TaskBase(ModelMixin):
     """
-    A Model which represents an task
+    Represents the smallest unit of stateful execution in ARIA. The task state includes inputs,
+    outputs, as well as an atomic status, ensuring that the task can only be running once at any
+    given time.
+
+    Tasks may be "one shot" or may be configured to run repeatedly in the case of failure.
+
+    Tasks are often based on :class:`Operation`, and thus act on either a :class:`Node` or a
+    :class:`Relationship`, however this is not required.
+
+    :ivar node: The node actor (optional)
+    :vartype node: :class:`Node`
+    :ivar relationship: The relationship actor (optional)
+    :vartype relationship: :class:`Relationship`
+    :ivar plugin: The implementing plugin (set to None for default execution plugin)
+    :vartype plugin: :class:`Plugin`
+    :ivar inputs: Parameters that can be used by this task
+    :vartype inputs: {basestring: :class:`Parameter`}
+    :ivar implementation: Python path to an ``@operation`` function
+    :vartype implementation: basestring
+    :ivar max_attempts: Maximum number of retries allowed in case of failure
+    :vartype max_attempts: int
+    :ivar retry_interval: Interval between retries (in seconds)
+    :vartype retry_interval: int
+    :ivar ignore_failure: Set to True to ignore failures
+    :vartype ignore_failure: bool
+    :ivar due_at: Timestamp to start the task
+    :vartype due_at: datetime
+    :ivar execution: Assigned execution
+    :vartype execution: :class:`Execution`
+    :ivar status: Current atomic status ('pending', 'retrying', 'sent', 'started', 'success',
+                  'failed')
+    :vartype status: basestring
+    :ivar started_at: Timestamp for when task started
+    :vartype started_at: datetime
+    :ivar ended_at: Timestamp for when task ended
+    :vartype ended_at: datetime
+    :ivar retry_count: How many retries occurred
+    :vartype retry_count: int
     """
 
     __tablename__ = 'task'
@@ -227,7 +264,7 @@ class TaskBase(ModelMixin):
     __private_fields__ = ['node_fk',
                           'relationship_fk',
                           'plugin_fk',
-                          'execution_fk',
+                          'execution_fk'
                           'node_name',
                           'relationship_name',
                           'execution_name']
@@ -247,11 +284,6 @@ class TaskBase(ModelMixin):
         FAILED,
     )
 
-    RUNS_ON_SOURCE = 'source'
-    RUNS_ON_TARGET = 'target'
-    RUNS_ON_NODE = 'node'
-    RUNS_ON = (RUNS_ON_NODE, RUNS_ON_SOURCE, RUNS_ON_TARGET)
-
     INFINITE_RETRIES = -1
 
     @declared_attr
@@ -278,37 +310,25 @@ class TaskBase(ModelMixin):
     def inputs(cls):
         return relationship.many_to_many(cls, 'parameter', prefix='inputs', dict_key='name')
 
-    status = Column(Enum(*STATES, name='status'), default=PENDING)
+    implementation = Column(String)
+    max_attempts = Column(Integer, default=1)
+    retry_interval = Column(Float, default=0)
+    ignore_failure = Column(Boolean, default=False)
 
+    # State
+    status = Column(Enum(*STATES, name='status'), default=PENDING)
     due_at = Column(DateTime, nullable=False, index=True, default=datetime.utcnow())
     started_at = Column(DateTime, default=None)
     ended_at = Column(DateTime, default=None)
-    max_attempts = Column(Integer, default=1)
     retry_count = Column(Integer, default=0)
-    retry_interval = Column(Float, default=0)
-    ignore_failure = Column(Boolean, default=False)
-
-    # Operation specific fields
-    implementation = Column(String)
-    _runs_on = Column(Enum(*RUNS_ON, name='runs_on'), name='runs_on')
 
     @property
     def has_ended(self):
-        return self.status in [self.SUCCESS, self.FAILED]
+        return self.status in (self.SUCCESS, self.FAILED)
 
     @property
     def is_waiting(self):
-        return self.status in [self.PENDING, self.RETRYING]
-
-    @property
-    def runs_on(self):
-        if self._runs_on == self.RUNS_ON_NODE:
-            return self.node
-        elif self._runs_on == self.RUNS_ON_SOURCE:
-            return self.relationship.source_node  # pylint: disable=no-member
-        elif self._runs_on == self.RUNS_ON_TARGET:
-            return self.relationship.target_node  # pylint: disable=no-member
-        return None
+        return self.status in (self.PENDING, self.RETRYING)
 
     @property
     def actor(self):
@@ -366,12 +386,12 @@ class TaskBase(ModelMixin):
     # endregion
 
     @classmethod
-    def for_node(cls, instance, runs_on, **kwargs):
-        return cls(node=instance, _runs_on=runs_on, **kwargs)
+    def for_node(cls, actor, **kwargs):
+        return cls(node=actor, **kwargs)
 
     @classmethod
-    def for_relationship(cls, instance, runs_on, **kwargs):
-        return cls(relationship=instance, _runs_on=runs_on, **kwargs)
+    def for_relationship(cls, actor, **kwargs):
+        return cls(relationship=actor, **kwargs)
 
     @staticmethod
     def abort(message=None):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/modeling/service_common.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_common.py b/aria/modeling/service_common.py
index 48c3170..1fcbc5f 100644
--- a/aria/modeling/service_common.py
+++ b/aria/modeling/service_common.py
@@ -101,7 +101,8 @@ class ParameterBase(TemplateModelMixin):
 
         from . import models
         return models.Parameter(name=name,
-                                type_name=formatting.full_type_name(value),
+                                type_name=formatting.full_type_name(value)
+                                if value is not None else None,
                                 value=value,
                                 description=description)
 
@@ -248,59 +249,3 @@ class MetadataBase(TemplateModelMixin):
         console.puts('{0}: {1}'.format(
             context.style.property(self.name),
             context.style.literal(self.value)))
-
-
-class PluginSpecificationBase(TemplateModelMixin):
-    """
-    Plugin specification.
-
-    :ivar name: Required plugin name
-    :vartype name: basestring
-    :ivar version: Minimum plugin version
-    :vartype version: basestring
-    """
-
-    __tablename__ = 'plugin_specification'
-
-    __private_fields__ = ['service_template_fk']
-
-    version = Column(Text, nullable=True)
-
-    # region foreign keys
-
-    @declared_attr
-    def service_template_fk(cls):
-        """For ServiceTemplate one-to-many to PluginSpecification"""
-        return relationship.foreign_key('service_template', nullable=True)
-
-    # endregion
-
-    @declared_attr
-    def service_template(cls):
-        return relationship.many_to_one(cls, 'service_template')
-
-    @property
-    def as_raw(self):
-        return collections.OrderedDict((
-            ('name', self.name),
-            ('version', self.version)))
-
-    def coerce_values(self, container, report_issues):
-        pass
-
-    def instantiate(self, container):
-        from . import models
-        return models.PluginSpecification(name=self.name,
-                                          version=self.version)
-
-    def find_plugin(self, plugins):
-        matching_plugins = []
-        for plugin in plugins:
-            # TODO: we need to use a version comparator
-            if (plugin.name == self.name) and \
-                ((self.version is None) or (plugin.package_version >= self.version)):
-                matching_plugins.append(plugin)
-        if matching_plugins:
-            # Return highest version of plugin
-            return sorted(matching_plugins, key=lambda plugin: plugin.package_version)[-1]
-        return None

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/modeling/service_instance.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_instance.py b/aria/modeling/service_instance.py
index d15aa7e..40d43fa 100644
--- a/aria/modeling/service_instance.py
+++ b/aria/modeling/service_instance.py
@@ -20,12 +20,14 @@ from sqlalchemy import (
     Text,
     Integer,
     Enum,
+    Boolean
 )
 from sqlalchemy import DateTime
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.ext.declarative import declared_attr
 
 from .mixins import InstanceModelMixin
+from ..orchestrator import execution_plugin
 from ..parser import validation
 from ..parser.consumption import ConsumptionContext
 from ..utils import collections, formatting, console
@@ -65,8 +67,8 @@ class ServiceBase(InstanceModelMixin):
     :vartype outputs: {basestring: :class:`Parameter`}
     :ivar workflows: Custom workflows that can be performed on the service
     :vartype workflows: {basestring: :class:`Operation`}
-    :ivar plugin_specifications: Plugins used by the service
-    :vartype plugin_specifications: {basestring: :class:`PluginSpecification`}
+    :ivar plugins: Plugins used by the service
+    :vartype plugins: {basestring: :class:`Plugin`}
     :ivar created_at: Creation timestamp
     :vartype created_at: :class:`datetime.datetime`
     :ivar updated_at: Update timestamp
@@ -176,13 +178,12 @@ class ServiceBase(InstanceModelMixin):
         return relationship.many_to_many(cls, 'parameter', prefix='outputs', dict_key='name')
 
     @declared_attr
-    def plugin_specifications(cls):
-        return relationship.many_to_many(cls, 'plugin_specification', dict_key='name')
+    def plugins(cls):
+        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)
 
@@ -207,6 +208,18 @@ class ServiceBase(InstanceModelMixin):
                 satisfied = False
         return satisfied
 
+    def find_hosts(self):
+        for node in self.nodes.itervalues():
+            node.find_host()
+
+    def configure_operations(self):
+        for node in self.nodes.itervalues():
+            node.configure_operations()
+        for group in self.groups.itervalues():
+            group.configure_operations()
+        for operation in self.workflows.itervalues():
+            operation.configure()
+
     def is_node_a_target(self, target_node):
         for node in self.nodes.itervalues():
             if self._is_node_a_target(node, target_node):
@@ -215,11 +228,11 @@ class ServiceBase(InstanceModelMixin):
 
     def _is_node_a_target(self, source_node, target_node):
         if source_node.outbound_relationships:
-            for the_relationship in source_node.outbound_relationships:
-                if the_relationship.target_node.name == target_node.name:
+            for relationship_model in source_node.outbound_relationships:
+                if relationship_model.target_node.name == target_node.name:
                     return True
                 else:
-                    node = the_relationship.target_node
+                    node = relationship_model.target_node
                     if node is not None:
                         if self._is_node_a_target(node, target_node):
                             return True
@@ -282,38 +295,25 @@ class ServiceBase(InstanceModelMixin):
             if not self.is_node_a_target(node):
                 self._dump_graph_node(node)
 
-    def _dump_graph_node(self, node):
+    def _dump_graph_node(self, node, capability=None):
         context = ConsumptionContext.get_thread_local()
         console.puts(context.style.node(node.name))
+        if capability is not None:
+            console.puts('{0} ({1})'.format(context.style.property(capability.name),
+                                            context.style.type(capability.type.name)))
         if node.outbound_relationships:
             with context.style.indent:
-                for the_relationship in node.outbound_relationships:
-                    relationship_name = context.style.property(the_relationship.name)
-                    if the_relationship.type is not None:
-                        relationship_type = context.style.type(the_relationship.type.name)
+                for relationship_model in node.outbound_relationships:
+                    relationship_name = context.style.property(relationship_model.name)
+                    if relationship_model.type is not None:
+                        console.puts('-> {0} ({1})'.format(relationship_name,
+                                                           context.style.type(
+                                                               relationship_model.type.name)))
                     else:
-                        relationship_type = None
-                    if the_relationship.target_capability is not None:
-                        capability_name = \
-                            context.style.node(the_relationship.target_capability.name)
-                    else:
-                        capability_name = None
-                    if capability_name is not None:
-                        if relationship_type is not None:
-                            console.puts('-> {0} ({1}) {2}'.format(relationship_name,
-                                                                   relationship_type,
-                                                                   capability_name))
-                        else:
-                            console.puts('-> {0} {1}'.format(relationship_name, capability_name))
-                    else:
-                        if relationship_type is not None:
-                            console.puts('-> {0} ({1})'.format(relationship_name,
-                                                               relationship_type))
-                        else:
-                            console.puts('-> {0}'.format(relationship_name))
-                    target_node = the_relationship.target_node
+                        console.puts('-> {0}'.format(relationship_name))
                     with console.indent(3):
-                        self._dump_graph_node(target_node)
+                        self._dump_graph_node(relationship_model.target_node,
+                                              relationship_model.target_capability)
 
 
 class NodeBase(InstanceModelMixin):
@@ -360,8 +360,10 @@ class NodeBase(InstanceModelMixin):
     :vartype policies: [:class:`Policy`]
     :ivar substitution_mapping: Our contribution to service substitution
     :vartype substitution_mapping: :class:`SubstitutionMapping`
-    :ivar tasks: Tasks on this node
+    :ivar tasks: Tasks for this node
     :vartype tasks: [:class:`Task`]
+    :ivar hosted_tasks: Tasks on this node
+    :vartype hosted_tasks: [:class:`Task`]
     """
 
     __tablename__ = 'node'
@@ -417,7 +419,7 @@ class NodeBase(InstanceModelMixin):
 
     @property
     def is_available(self):
-        return self.state not in [self.INITIAL, self.DELETED, self.ERROR]
+        return self.state not in (self.INITIAL, self.DELETED, self.ERROR)
 
     # region foreign_keys
 
@@ -455,7 +457,7 @@ class NodeBase(InstanceModelMixin):
     # region one_to_one relationships
 
     @declared_attr
-    def host(cls):
+    def host(cls): # pylint: disable=method-hidden
         return relationship.one_to_one_self(cls, 'host_fk')
 
     # endregion
@@ -522,17 +524,9 @@ class NodeBase(InstanceModelMixin):
     __mapper_args__ = {'version_id_col': version} # Enable SQLAlchemy automatic version counting
 
     @property
-    def ip(self):
-        # TODO: totally broken
-        if not self.host_fk:
-            return None
-        host_node = self.host
-        if 'ip' in host_node.runtime_properties:  # pylint: disable=no-member
-            return host_node.runtime_properties['ip']  # pylint: disable=no-member
-        host_node = host_node.node_template  # pylint: disable=no-member
-        host_ip_property = host_node.properties.get('ip')
-        if host_ip_property:
-            return host_ip_property.value
+    def host_address(self):
+        if self.host and self.host.runtime_properties:
+            return self.host.runtime_properties.get('ip')
         return None
 
     def satisfy_requirements(self):
@@ -567,9 +561,10 @@ class NodeBase(InstanceModelMixin):
             if target_node_capability is not None:
                 # Relate to the first target node that has capacity
                 for node in target_nodes:
-                    target_capability = node.capabilities.get(target_node_capability.name)
-                    if target_capability.relate():
+                    a_target_capability = node.capabilities.get(target_node_capability.name)
+                    if a_target_capability.relate():
                         target_node = node
+                        target_capability = a_target_capability
                         break
             else:
                 # Use first target node
@@ -577,14 +572,15 @@ class NodeBase(InstanceModelMixin):
 
             if target_node is not None:
                 if requirement_template.relationship_template is not None:
-                    the_relationship = \
+                    relationship_model = \
                         requirement_template.relationship_template.instantiate(self)
                 else:
-                    the_relationship = models.Relationship(target_capability=target_capability)
-                the_relationship.name = requirement_template.name
-                the_relationship.requirement_template = requirement_template
-                the_relationship.target_node = target_node
-                self.outbound_relationships.append(the_relationship)
+                    relationship_model = models.Relationship()
+                relationship_model.name = requirement_template.name
+                relationship_model.requirement_template = requirement_template
+                relationship_model.target_node = target_node
+                relationship_model.target_capability = target_capability
+                self.outbound_relationships.append(relationship_model)
                 return True
             else:
                 context.validation.report('requirement "{0}" of node "{1}" targets node '
@@ -619,6 +615,32 @@ class NodeBase(InstanceModelMixin):
                 satisfied = False
         return satisfied
 
+    def find_host(self):
+        def _find_host(node):
+            if node.type.role == 'host':
+                return node
+            for the_relationship in node.outbound_relationships:
+                if (the_relationship.target_capability is not None) and \
+                    the_relationship.target_capability.type.role == 'host':
+                    host = _find_host(the_relationship.target_node)
+                    if host is not None:
+                        return host
+            for the_relationship in node.inbound_relationships:
+                if (the_relationship.target_capability is not None) and \
+                    the_relationship.target_capability.type.role == 'feature':
+                    host = _find_host(the_relationship.source_node)
+                    if host is not None:
+                        return host
+            return None
+
+        self.host = _find_host(self)
+
+    def configure_operations(self):
+        for interface in self.interfaces.itervalues():
+            interface.configure_operations()
+        for the_relationship in self.outbound_relationships:
+            the_relationship.configure_operations()
+
     @property
     def as_raw(self):
         return collections.OrderedDict((
@@ -761,6 +783,10 @@ class GroupBase(InstanceModelMixin):
 
     description = Column(Text)
 
+    def configure_operations(self):
+        for interface in self.interfaces.itervalues():
+            interface.configure_operations()
+
     @property
     def as_raw(self):
         return collections.OrderedDict((
@@ -1146,7 +1172,7 @@ class RelationshipBase(InstanceModelMixin):
     :vartype source_node: :class:`Node`
     :ivar target_node: Target node
     :vartype target_node: :class:`Node`
-    :ivar tasks: Tasks on this node
+    :ivar tasks: Tasks for this relationship
     :vartype tasks: [:class:`Task`]
     """
 
@@ -1266,6 +1292,10 @@ class RelationshipBase(InstanceModelMixin):
     source_position = Column(Integer) # ???
     target_position = Column(Integer) # ???
 
+    def configure_operations(self):
+        for interface in self.interfaces.itervalues():
+            interface.configure_operations()
+
     @property
     def as_raw(self):
         return collections.OrderedDict((
@@ -1552,6 +1582,10 @@ class InterfaceBase(InstanceModelMixin):
 
     description = Column(Text)
 
+    def configure_operations(self):
+        for operation in self.operations.itervalues():
+            operation.configure()
+
     @property
     def as_raw(self):
         return collections.OrderedDict((
@@ -1592,10 +1626,16 @@ class OperationBase(InstanceModelMixin):
     :vartype operation_template: :class:`OperationTemplate`
     :ivar description: Human-readable description
     :vartype description: string
-    :ivar plugin_specification: Associated plugin
-    :vartype plugin_specification: :class:`PluginSpecification`
-    :ivar implementation: Implementation string (interpreted by the plugin)
+    :ivar plugin: Associated plugin
+    :vartype plugin: :class:`Plugin`
+    :ivar relationship_edge: When true specified 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 configuration: Configuration (interpreted by the plugin)
+    :vartype configuration: {basestring, object}
     :ivar dependencies: Dependency strings (interpreted by the plugin)
     :vartype dependencies: [basestring]
     :ivar inputs: Parameters that can be used by this operation
@@ -1632,9 +1672,9 @@ class OperationBase(InstanceModelMixin):
         return relationship.foreign_key('interface', nullable=True)
 
     @declared_attr
-    def plugin_specification_fk(cls):
-        """For Operation one-to-one to PluginSpecification"""
-        return relationship.foreign_key('plugin_specification', nullable=True)
+    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):
@@ -1650,9 +1690,8 @@ class OperationBase(InstanceModelMixin):
     # region one_to_one relationships
 
     @declared_attr
-    def plugin_specification(cls):
-        return relationship.one_to_one(
-            cls, 'plugin_specification', back_populates=relationship.NO_BACK_POP)
+    def plugin(cls):
+        return relationship.one_to_one(cls, 'plugin', back_populates=relationship.NO_BACK_POP)
 
     # endregion
 
@@ -1685,12 +1724,32 @@ class OperationBase(InstanceModelMixin):
     # endregion
 
     description = Column(Text)
+    relationship_edge = Column(Boolean)
     implementation = Column(Text)
+    configuration = Column(modeling_types.StrictDict(key_cls=basestring))
     dependencies = Column(modeling_types.StrictList(item_cls=basestring))
     executor = Column(Text)
     max_retries = Column(Integer)
     retry_interval = Column(Integer)
 
+    def configure(self):
+        from . import models
+        # Note: for workflows (operations attached directly to the service) "interface" will be None
+        if (self.implementation is None) or (self.interface is None):
+            return
+
+        if self.plugin is None:
+            arguments = execution_plugin.instantiation.configure_operation(self)
+        else:
+            # In the future plugins may be able to add their own "configure_operation" hook that
+            # can validate the configuration and otherwise return specially derived arguments
+            arguments = self.configuration
+
+        # Note: the arguments will *override* operation inputs of the same name
+        if arguments:
+            for k, v in arguments.iteritems():
+                self.inputs[k] = models.Parameter.wrap(k, v)
+
     @property
     def as_raw(self):
         return collections.OrderedDict((
@@ -1716,9 +1775,17 @@ class OperationBase(InstanceModelMixin):
         if self.description:
             console.puts(context.style.meta(self.description))
         with context.style.indent:
+            if self.plugin is not None:
+                console.puts('Plugin: {0}'.format(
+                    context.style.literal(self.plugin.name)))
             if self.implementation is not None:
                 console.puts('Implementation: {0}'.format(
                     context.style.literal(self.implementation)))
+            if self.configuration:
+                with context.style.indent:
+                    for k, v in self.configuration.iteritems():
+                        console.puts('{0}: {1}'.format(context.style.property(k),
+                                                       context.style.literal(v)))
             if self.dependencies:
                 console.puts(
                     'Dependencies: {0}'.format(

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/modeling/service_template.py
----------------------------------------------------------------------
diff --git a/aria/modeling/service_template.py b/aria/modeling/service_template.py
index 8355521..51fea2f 100644
--- a/aria/modeling/service_template.py
+++ b/aria/modeling/service_template.py
@@ -24,6 +24,7 @@ from sqlalchemy import (
     Column,
     Text,
     Integer,
+    Boolean,
     DateTime
 )
 from sqlalchemy.ext.declarative import declared_attr
@@ -291,6 +292,16 @@ class ServiceTemplateBase(TemplateModelMixin):
 
         context.modeling.instance = service
 
+        for plugin_specification in self.plugin_specifications.itervalues():
+            if plugin_specification.enabled:
+                if plugin_specification.resolve():
+                    plugin = plugin_specification.plugin
+                    service.plugins[plugin.name] = plugin
+                else:
+                    context = ConsumptionContext.get_thread_local()
+                    context.validation.report('specified plugin not found: {0}'.format(
+                        plugin_specification.name), level=validation.Issue.EXTERNAL)
+
         utils.instantiate_dict(self, service.meta_data, self.meta_data)
 
         for node_template in self.node_templates.itervalues():
@@ -301,7 +312,6 @@ class ServiceTemplateBase(TemplateModelMixin):
         utils.instantiate_dict(self, service.groups, self.group_templates)
         utils.instantiate_dict(self, service.policies, self.policy_templates)
         utils.instantiate_dict(self, service.workflows, self.workflow_templates)
-        utils.instantiate_dict(self, service.plugin_specifications, self.plugin_specifications)
 
         if self.substitution_template is not None:
             service.substitution = self.substitution_template.instantiate(container)
@@ -1740,8 +1750,14 @@ class OperationTemplateBase(TemplateModelMixin):
     :vartype description: basestring
     :ivar plugin_specification: Associated plugin
     :vartype plugin_specification: :class:`PluginSpecification`
-    :ivar implementation: Implementation string (interpreted by the plugin)
+    :ivar relationship_edge: When true specified 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 configuration: Configuration (interpreted by the plugin)
+    :vartype configuration: {basestring, object}
     :ivar dependencies: Dependency strings (interpreted by the plugin)
     :vartype dependencies: [basestring]
     :ivar inputs: Parameters that can be used by this operation
@@ -1766,8 +1782,6 @@ class OperationTemplateBase(TemplateModelMixin):
                           'interface_template_fk',
                           'plugin_fk']
 
-    description = Column(Text)
-
     # region foreign keys
 
     @declared_attr
@@ -1828,7 +1842,10 @@ class OperationTemplateBase(TemplateModelMixin):
 
     # endregion
 
+    description = Column(Text)
+    relationship_edge = Column(Boolean)
     implementation = Column(Text)
+    configuration = Column(modeling_types.StrictDict(key_cls=basestring))
     dependencies = Column(modeling_types.StrictList(item_cls=basestring))
     executor = Column(Text)
     max_retries = Column(Integer)
@@ -1848,11 +1865,23 @@ class OperationTemplateBase(TemplateModelMixin):
 
     def instantiate(self, container):
         from . import models
+        if self.plugin_specification and self.plugin_specification.enabled:
+            plugin = self.plugin_specification.plugin
+            implementation = self.implementation if plugin is not None else None
+            # "plugin" would be none if a match was not found. In that case, a validation error
+            # should already have been reported in ServiceTemplateBase.instantiate, so we will
+            # continue silently here
+        else:
+            # If the plugin is disabled, the operation should be disabled, too
+            plugin = None
+            implementation = None
         operation = models.Operation(name=self.name,
                                      description=deepcopy_with_locators(self.description),
-                                     implementation=self.implementation,
+                                     relationship_edge=self.relationship_edge,
+                                     plugin=plugin,
+                                     implementation=implementation,
+                                     configuration=self.configuration,
                                      dependencies=self.dependencies,
-                                     plugin_specification=self.plugin_specification,
                                      executor=self.executor,
                                      max_retries=self.max_retries,
                                      retry_interval=self.retry_interval,
@@ -1872,9 +1901,17 @@ class OperationTemplateBase(TemplateModelMixin):
         if self.description:
             console.puts(context.style.meta(self.description))
         with context.style.indent:
+            if self.plugin_specification is not None:
+                console.puts('Plugin specification: {0}'.format(
+                    context.style.literal(self.plugin_specification.name)))
             if self.implementation is not None:
                 console.puts('Implementation: {0}'.format(
                     context.style.literal(self.implementation)))
+            if self.configuration:
+                with context.style.indent:
+                    for k, v in self.configuration.iteritems():
+                        console.puts('{0}: {1}'.format(context.style.property(k),
+                                                       context.style.literal(v)))
             if self.dependencies:
                 console.puts('Dependencies: {0}'.format(
                     ', '.join((str(context.style.literal(v)) for v in self.dependencies))))
@@ -2023,3 +2060,87 @@ class ArtifactTemplateBase(TemplateModelMixin):
                 console.puts('Repository credential: {0}'.format(
                     context.style.literal(self.repository_credential)))
             utils.dump_dict_values(self.properties, 'Properties')
+
+
+class PluginSpecificationBase(TemplateModelMixin):
+    """
+    Plugin specification.
+
+    :ivar name: Required plugin name
+    :vartype name: basestring
+    :ivar version: Minimum plugin version
+    :vartype version: basestring
+    :ivar enabled: Whether the plugin is enabled
+    :vartype enabled: bool
+    :ivar plugin: The matching plugin (or None if not matched)
+    :vartype plugin: :class:`Plugin`
+    """
+
+    __tablename__ = 'plugin_specification'
+
+    __private_fields__ = ['service_template_fk',
+                          'plugin_fk']
+
+    version = Column(Text)
+    enabled = Column(Boolean, nullable=False, default=True)
+
+    # region foreign keys
+
+    @declared_attr
+    def service_template_fk(cls):
+        """For ServiceTemplate one-to-many to PluginSpecification"""
+        return relationship.foreign_key('service_template', nullable=True)
+
+    @declared_attr
+    def plugin_fk(cls):
+        """For PluginSpecification many-to-one to Plugin"""
+        return relationship.foreign_key('plugin', nullable=True)
+
+    # endregion
+
+    # region many_to_one relationships
+
+    @declared_attr
+    def service_template(cls):
+        return relationship.many_to_one(cls, 'service_template')
+
+    @declared_attr
+    def plugin(cls): # pylint: disable=method-hidden
+        return relationship.many_to_one(cls, 'plugin', back_populates=relationship.NO_BACK_POP)
+
+    # endregion
+
+    @property
+    def as_raw(self):
+        return collections.OrderedDict((
+            ('name', self.name),
+            ('version', self.version),
+            ('enabled', self.enabled)))
+
+    def coerce_values(self, container, report_issues):
+        pass
+
+    def resolve(self):
+        # TODO: we are planning a separate "instantiation" module where this will be called or
+        # moved to. There, we will probably have a context with a storage manager. Until then,
+        # this is the only potentially available context, which of course will only be available
+        # if we're in a workflow.
+        from ..orchestrator import context
+        try:
+            workflow_context = context.workflow.current.get()
+            plugins = workflow_context.model.plugin.list()
+        except context.exceptions.ContextException:
+            plugins = None
+
+        matching_plugins = []
+        if plugins:
+            for plugin in plugins:
+                # TODO: we need to use a version comparator
+                if (plugin.name == self.name) and \
+                    ((self.version is None) or (plugin.package_version >= self.version)):
+                    matching_plugins.append(plugin)
+        self.plugin = None
+        if matching_plugins:
+            # Return highest version of plugin
+            self.plugin = sorted(matching_plugins, key=lambda plugin: plugin.package_version)[-1]
+        return self.plugin is not None

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/execution_plugin/__init__.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/execution_plugin/__init__.py b/aria/orchestrator/execution_plugin/__init__.py
index 372022f..3624264 100644
--- a/aria/orchestrator/execution_plugin/__init__.py
+++ b/aria/orchestrator/execution_plugin/__init__.py
@@ -14,6 +14,8 @@
 # limitations under the License.
 
 from contextlib import contextmanager
+from . import instantiation
+
 
 # Populated during execution of python scripts
 ctx = None

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/execution_plugin/common.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/execution_plugin/common.py b/aria/orchestrator/execution_plugin/common.py
index 7915c47..32e4575 100644
--- a/aria/orchestrator/execution_plugin/common.py
+++ b/aria/orchestrator/execution_plugin/common.py
@@ -34,7 +34,7 @@ def download_script(ctx, script_path):
     file_descriptor, dest_script_path = tempfile.mkstemp(suffix='-{0}'.format(suffix))
     os.close(file_descriptor)
     try:
-        if schema in ['http', 'https']:
+        if schema in ('http', 'https'):
             response = requests.get(script_path)
             if response.status_code == 404:
                 ctx.task.abort('Failed to download script: {0} (status code: {1})'

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/execution_plugin/instantiation.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/execution_plugin/instantiation.py b/aria/orchestrator/execution_plugin/instantiation.py
new file mode 100644
index 0000000..960835c
--- /dev/null
+++ b/aria/orchestrator/execution_plugin/instantiation.py
@@ -0,0 +1,191 @@
+# 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.
+
+# TODO: this module will eventually be moved to a new "aria.instantiation" package
+
+from ...utils.formatting import full_type_name
+from ...utils.collections import OrderedDict
+from ...parser import validation
+from ...parser.consumption import ConsumptionContext
+
+
+def configure_operation(operation):
+    configuration = OrderedDict(operation.configuration) if operation.configuration else {}
+
+    arguments = OrderedDict()
+    arguments['script_path'] = operation.implementation
+    arguments['process'] = _get_process(configuration.pop('process')) \
+        if 'process' in configuration else None
+
+    host = None
+    interface = operation.interface
+    if interface.node is not None:
+        host = interface.node.host
+    elif interface.relationship is not None:
+        if operation.relationship_edge is True:
+            host = interface.relationship.target_node.host
+        else: # either False or None
+            host = interface.relationship.source_node.host
+
+    if host is None:
+        _configure_local(operation)
+    else:
+        _configure_remote(operation, configuration, arguments)
+
+    # Any remaining unhandled configuration values will become extra arguments, available as kwargs
+    # in either "run_script_locally" or "run_script_with_ssh"
+    arguments.update(configuration)
+
+    return arguments
+
+def _configure_local(operation):
+    """
+    Local operation.
+    """
+    from . import operations
+    operation.implementation = '{0}.{1}'.format(operations.__name__,
+                                                operations.run_script_locally.__name__)
+
+
+def _configure_remote(operation, configuration, arguments):
+    """
+    Remote SSH operation via Fabric.
+    """
+    # TODO: find a way to configure these generally in the service template
+    default_user = ''
+    default_password = ''
+
+    ssh = _get_ssh(configuration.pop('ssh')) if 'ssh' in configuration else {}
+    if 'user' not in ssh:
+        ssh['user'] = default_user
+    if ('password' not in ssh) and ('key' not in ssh) and ('key_filename' not in ssh):
+        ssh['password'] = default_password
+
+    arguments['use_sudo'] = ssh.get('use_sudo')
+    arguments['hide_output'] = ssh.get('hide_output')
+    arguments['fabric_env'] = {}
+    if 'warn_only' in ssh:
+        arguments['fabric_env']['warn_only'] = ssh['warn_only']
+    arguments['fabric_env']['user'] = ssh.get('user')
+    arguments['fabric_env']['password'] = ssh.get('password')
+    arguments['fabric_env']['key'] = ssh.get('key')
+    arguments['fabric_env']['key_filename'] = ssh.get('key_filename')
+    if 'address' in ssh:
+        arguments['fabric_env']['host_string'] = ssh['address']
+
+    if arguments['fabric_env'].get('user') is None:
+        context = ConsumptionContext.get_thread_local()
+        context.validation.report('must configure "ssh.user" for "{0}"'
+                                  .format(operation.implementation),
+                                  level=validation.Issue.BETWEEN_TYPES)
+    if (arguments['fabric_env'].get('password') is None) and \
+        (arguments['fabric_env'].get('key') is None) and \
+        (arguments['fabric_env'].get('key_filename') is None):
+        context = ConsumptionContext.get_thread_local()
+        context.validation.report('must configure "ssh.password", "ssh.key", or "ssh.key_filename" '
+                                  'for "{0}"'
+                                  .format(operation.implementation),
+                                  level=validation.Issue.BETWEEN_TYPES)
+
+    from . import operations
+    operation.implementation = '{0}.{1}'.format(operations.__name__,
+                                                operations.run_script_with_ssh.__name__)
+
+
+def _get_process(value):
+    if value is None:
+        return None
+    _validate_type(value, dict, 'process')
+    for k, v in value.iteritems():
+        if k == 'eval_python':
+            value[k] = _str_to_bool(v, 'process.eval_python')
+        elif k == 'cwd':
+            _validate_type(v, basestring, 'process.cwd')
+        elif k == 'command_prefix':
+            _validate_type(v, basestring, 'process.command_prefix')
+        elif k == 'args':
+            value[k] = _dict_to_list(v, 'process.args')
+        elif k == 'env':
+            _validate_type(v, dict, 'process.env')
+        else:
+            context = ConsumptionContext.get_thread_local()
+            context.validation.report('unsupported configuration: "process.{0}"'.format(k),
+                                      level=validation.Issue.BETWEEN_TYPES)
+    return value
+
+
+def _get_ssh(value):
+    if value is None:
+        return {}
+    _validate_type(value, dict, 'ssh')
+    for k, v in value.iteritems():
+        if k == 'use_sudo':
+            value[k] = _str_to_bool(v, 'ssh.use_sudo')
+        elif k == 'hide_output':
+            value[k] = _dict_to_list(v, 'ssh.hide_output')
+        elif k == 'warn_only':
+            value[k] = _str_to_bool(v, 'ssh.warn_only')
+        elif k == 'user':
+            _validate_type(v, basestring, 'ssh.user')
+        elif k == 'password':
+            _validate_type(v, basestring, 'ssh.password')
+        elif k == 'key':
+            _validate_type(v, basestring, 'ssh.key')
+        elif k == 'key_filename':
+            _validate_type(v, basestring, 'ssh.key_filename')
+        elif k == 'address':
+            _validate_type(v, basestring, 'ssh.address')
+        else:
+            context = ConsumptionContext.get_thread_local()
+            context.validation.report('unsupported configuration: "ssh.{0}"'.format(k),
+                                      level=validation.Issue.BETWEEN_TYPES)
+    return value
+
+
+def _validate_type(value, the_type, name):
+    if not isinstance(value, the_type):
+        context = ConsumptionContext.get_thread_local()
+        context.validation.report('"{0}" configuration is not a {1}'
+                                  .format(name, full_type_name(the_type)),
+                                  level=validation.Issue.BETWEEN_TYPES)
+
+
+def _str_to_bool(value, name):
+    if value is None:
+        return None
+    _validate_type(value, basestring, name)
+    if value == 'true':
+        return True
+    elif value == 'false':
+        return False
+    else:
+        context = ConsumptionContext.get_thread_local()
+        context.validation.report('"{0}" configuration is not "true" or "false": {1}'
+                                  .format(name, repr(value)),
+                                  level=validation.Issue.BETWEEN_TYPES)
+
+
+def _dict_to_list(the_dict, name):
+    _validate_type(the_dict, dict, name)
+    value = []
+    for k in sorted(the_dict):
+        v = the_dict[k]
+        if not isinstance(v, basestring):
+            context = ConsumptionContext.get_thread_local()
+            context.validation.report('"{0}.{1}" configuration is not a string: {2}'
+                                      .format(name, k, repr(v)),
+                                      level=validation.Issue.BETWEEN_TYPES)
+        value.append(v)
+    return value

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/execution_plugin/ssh/operations.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/execution_plugin/ssh/operations.py b/aria/orchestrator/execution_plugin/ssh/operations.py
index f240beb..7147a30 100644
--- a/aria/orchestrator/execution_plugin/ssh/operations.py
+++ b/aria/orchestrator/execution_plugin/ssh/operations.py
@@ -143,9 +143,9 @@ def _fabric_env(ctx, fabric_env, warn_only):
     env = constants.FABRIC_ENV_DEFAULTS.copy()
     env.update(fabric_env or {})
     env.setdefault('warn_only', warn_only)
-    if 'host_string' not in env:
-        env['host_string'] = ctx.task.runs_on.ip
     # validations
+    if (not env.get('host_string')) and (ctx.task) and (ctx.task.actor) and (ctx.task.actor.host):
+        env['host_string'] = ctx.task.actor.host.host_address
     if not env.get('host_string'):
         ctx.task.abort('`host_string` not supplied and ip cannot be deduced automatically')
     if not (env.get('password') or env.get('key_filename') or env.get('key')):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/api/task.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/api/task.py b/aria/orchestrator/workflows/api/task.py
index f49ec2e..49c584c 100644
--- a/aria/orchestrator/workflows/api/task.py
+++ b/aria/orchestrator/workflows/api/task.py
@@ -19,7 +19,7 @@ Provides the tasks to be entered into the task graph
 import copy
 
 from ....modeling import models
-from ....utils.collections import OrderedDict
+from ....utils.collections import (OrderedDict, FrozenDict)
 from ....utils.uuid import generate_uuid
 from ... import context
 from .. import exceptions
@@ -56,7 +56,7 @@ class BaseTask(object):
 
 class OperationTask(BaseTask):
     """
-    Represents an operation task in the task_graph
+    Represents an operation task in the task graph.
     """
 
     NAME_FORMAT = '{interface}:{operation}@{type}:{name}'
@@ -66,32 +66,50 @@ class OperationTask(BaseTask):
                  actor_type,
                  interface_name,
                  operation_name,
-                 runs_on=None,
+                 inputs=None,
                  max_attempts=None,
                  retry_interval=None,
-                 ignore_failure=None,
-                 inputs=None):
+                 ignore_failure=None):
         """
         Do not call this constructor directly. Instead, use :meth:`for_node` or
         :meth:`for_relationship`.
         """
 
-        assert isinstance(actor, (models.Node, models.Relationship))
-        assert actor_type in ('node', 'relationship')
         assert interface_name and operation_name
-        assert runs_on in models.Task.RUNS_ON
         super(OperationTask, self).__init__()
 
+        operation = None
+        interface = actor.interfaces.get(interface_name)
+        if interface is not None:
+            operation = interface.operations.get(operation_name)
+
+        if operation is None:
+            raise exceptions.OperationNotFoundException(
+                'Could not find operation "{0}" on interface "{1}" for {2} "{3}"'
+                .format(operation_name, interface_name, actor_type, actor.name))
+
+        if operation.implementation is None:
+            raise exceptions.OperationNotFoundException(
+                'Empty operation "{0}" on interface "{1}" for {2} "{3}"'
+                .format(operation_name, interface_name, actor_type, actor.name))
+
         self.actor = actor
+        self.actor_type = actor_type
+        self.interface_name = interface_name
+        self.operation_name = operation_name
+
+        self.name = OperationTask.NAME_FORMAT.format(type=actor_type,
+                                                     name=actor.name,
+                                                     interface=interface_name,
+                                                     operation=operation_name)
         self.max_attempts = (self.workflow_context._task_max_attempts
                              if max_attempts is None else max_attempts)
         self.retry_interval = (self.workflow_context._task_retry_interval
                                if retry_interval is None else retry_interval)
         self.ignore_failure = (self.workflow_context._task_ignore_failure
                                if ignore_failure is None else ignore_failure)
-        self.runs_on = runs_on
-        self.interface_name = interface_name
-        self.operation_name = operation_name
+        self.implementation = operation.implementation
+        self.plugin = operation.plugin
 
         # Wrap inputs
         inputs = copy.deepcopy(inputs) if inputs else {}
@@ -99,65 +117,33 @@ class OperationTask(BaseTask):
             if not isinstance(v, models.Parameter):
                 inputs[k] = models.Parameter.wrap(k, v)
 
-        # TODO: Suggestion: these extra inputs could be stored as a separate entry in the task
-        # model, because they are different from the operation inputs. If we do this, then the two
-        # kinds of inputs should *not* be merged here.
-
-        operation = self._get_operation()
-        if operation is None:
-            raise exceptions.OperationNotFoundException(
-                'Could not find operation "{0}" on interface "{1}" for {2} "{3}"'
-                .format(self.operation_name, self.interface_name, actor_type, actor.name))
-
-        self.plugin = None
-        if operation.plugin_specification:
-            self.plugin = OperationTask._find_plugin(operation.plugin_specification)
-            if self.plugin is None:
-                raise exceptions.PluginNotFoundException(
-                    'Could not find plugin of operation "{0}" on interface "{1}" for {2} "{3}"'
-                    .format(self.operation_name, self.interface_name, actor_type, actor.name))
-
-        self.implementation = operation.implementation
-        self.inputs = OperationTask._merge_inputs(operation.inputs, inputs)
-
-        self.name = OperationTask.NAME_FORMAT.format(type=actor_type,
-                                                     name=actor.name,
-                                                     interface=self.interface_name,
-                                                     operation=self.operation_name)
-
-    def __repr__(self):
-        return self.name
-
-    def _get_operation(self):
-        interface = self.actor.interfaces.get(self.interface_name)
-        if interface:
-            return interface.operations.get(self.operation_name)
-        return None
-
-
+        self.inputs = OrderedDict(operation.inputs)
+        if inputs:
+            self.inputs.update(inputs)
+        self.inputs = FrozenDict(self.inputs)
 
     @classmethod
     def for_node(cls,
                  node,
                  interface_name,
                  operation_name,
+                 inputs=None,
                  max_attempts=None,
                  retry_interval=None,
-                 ignore_failure=None,
-                 inputs=None):
+                 ignore_failure=None):
         """
         Creates an operation on a node.
 
         :param node: The node on which to run the operation
         :param interface_name: The interface name
         :param operation_name: The operation name within the interface
+        :param inputs: Override the operation's inputs
         :param max_attempts: The maximum number of attempts in case the operation fails
-                             (if not specified the defaults it taken from the workflow context)
+                             (if not specified the defaults is taken from the workflow context)
         :param retry_interval: The interval in seconds between attempts when the operation fails
-                               (if not specified the defaults it taken from the workflow context)
+                               (if not specified the defaults is taken from the workflow context)
         :param ignore_failure: Whether to ignore failures
-                               (if not specified the defaults it taken from the workflow context)
-        :param inputs: Additional operation inputs
+                               (if not specified the defaults is taken from the workflow context)
         """
 
         assert isinstance(node, models.Node)
@@ -166,62 +152,45 @@ class OperationTask(BaseTask):
             actor_type='node',
             interface_name=interface_name,
             operation_name=operation_name,
+            inputs=inputs,
             max_attempts=max_attempts,
             retry_interval=retry_interval,
-            ignore_failure=ignore_failure,
-            inputs=inputs,
-            runs_on=models.Task.RUNS_ON_NODE)
+            ignore_failure=ignore_failure)
 
     @classmethod
     def for_relationship(cls,
                          relationship,
                          interface_name,
                          operation_name,
-                         runs_on=models.Task.RUNS_ON_SOURCE,
+                         inputs=None,
                          max_attempts=None,
                          retry_interval=None,
-                         ignore_failure=None,
-                         inputs=None):
+                         ignore_failure=None):
         """
-        Creates an operation on a relationship edge.
+        Creates an operation on a relationship.
 
         :param relationship: The relationship on which to run the operation
         :param interface_name: The interface name
         :param operation_name: The operation name within the interface
-        :param runs_on: where to run the operation ("source" or "target"); defaults to "source"
+        :param inputs: Override the operation's inputs
         :param max_attempts: The maximum number of attempts in case the operation fails
-                             (if not specified the defaults it taken from the workflow context)
+                             (if not specified the defaults is taken from the workflow context)
         :param retry_interval: The interval in seconds between attempts when the operation fails
-                               (if not specified the defaults it taken from the workflow context)
+                               (if not specified the defaults is taken from the workflow context)
         :param ignore_failure: Whether to ignore failures
-                               (if not specified the defaults it taken from the workflow context)
-        :param inputs: Additional operation inputs
+                               (if not specified the defaults is taken from the workflow context)
         """
 
         assert isinstance(relationship, models.Relationship)
-        assert runs_on in models.Task.RUNS_ON
         return cls(
             actor=relationship,
             actor_type='relationship',
             interface_name=interface_name,
             operation_name=operation_name,
-            runs_on=runs_on,
+            inputs=inputs,
             max_attempts=max_attempts,
             retry_interval=retry_interval,
-            ignore_failure=ignore_failure,
-            inputs=inputs)
-
-    @staticmethod
-    def _find_plugin(plugin_specification):
-        workflow_context = context.workflow.current.get()
-        return plugin_specification.find_plugin(workflow_context.model.plugin.list())
-
-    @staticmethod
-    def _merge_inputs(operation_inputs, override_inputs=None):
-        final_inputs = OrderedDict(operation_inputs)
-        if override_inputs:
-            final_inputs.update(override_inputs)
-        return final_inputs
+            ignore_failure=ignore_failure)
 
 
 class WorkflowTask(BaseTask):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/api/task_graph.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/api/task_graph.py b/aria/orchestrator/workflows/api/task_graph.py
index 92a39d2..9f0d13b 100644
--- a/aria/orchestrator/workflows/api/task_graph.py
+++ b/aria/orchestrator/workflows/api/task_graph.py
@@ -37,7 +37,7 @@ def _filter_out_empty_tasks(func=None):
         return lambda f: _filter_out_empty_tasks(func=f)
 
     def _wrapper(task, *tasks, **kwargs):
-        return func(*(t for t in [task] + list(tasks) if t), **kwargs)
+        return func(*(t for t in (task,) + tuple(tasks) if t), **kwargs)
     return _wrapper
 
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/builtin/utils.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/builtin/utils.py b/aria/orchestrator/workflows/builtin/utils.py
index d79318f..752fe35 100644
--- a/aria/orchestrator/workflows/builtin/utils.py
+++ b/aria/orchestrator/workflows/builtin/utils.py
@@ -71,15 +71,13 @@ def relationship_tasks(
         operations.append(
             OperationTask.for_relationship(relationship=relationship,
                                            interface_name=interface_name,
-                                           operation_name=source_operation_name,
-                                           runs_on='source')
+                                           operation_name=source_operation_name)
         )
     if target_operation_name:
         operations.append(
             OperationTask.for_relationship(relationship=relationship,
                                            interface_name=interface_name,
-                                           operation_name=target_operation_name,
-                                           runs_on='target')
+                                           operation_name=target_operation_name)
         )
 
     return operations

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/core/engine.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/core/engine.py b/aria/orchestrator/workflows/core/engine.py
index d32abb8..f73cade 100644
--- a/aria/orchestrator/workflows/core/engine.py
+++ b/aria/orchestrator/workflows/core/engine.py
@@ -82,8 +82,8 @@ class Engine(logger.LoggerMixin):
         events.on_cancelling_workflow_signal.send(self._workflow_context)
 
     def _is_cancel(self):
-        return self._workflow_context.execution.status in [models.Execution.CANCELLING,
-                                                           models.Execution.CANCELLED]
+        return self._workflow_context.execution.status in (models.Execution.CANCELLING,
+                                                           models.Execution.CANCELLED)
 
     def _executable_tasks(self):
         now = datetime.utcnow()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/core/events_handler.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/core/events_handler.py b/aria/orchestrator/workflows/core/events_handler.py
index 8534aae..7f61bfa 100644
--- a/aria/orchestrator/workflows/core/events_handler.py
+++ b/aria/orchestrator/workflows/core/events_handler.py
@@ -125,8 +125,12 @@ def _workflow_cancelling(workflow_context, *args, **kwargs):
 
 
 def _update_node_state_if_necessary(task, is_transitional=False):
-    if task.interface_name in ['tosca.interfaces.node.lifecycle.Standard', 'Standard']:
-        node = task.runs_on
+    # TODO: this is not the right way to check! the interface name is arbitrary
+    # and also will *never* be the type name
+    model_task = task.model_task
+    node = model_task.node if model_task is not None else None
+    if (node is not None) and \
+        (task.interface_name in ('Standard', 'tosca.interfaces.node.lifecycle.Standard')):
         state = node.determine_state(op_name=task.operation_name, is_transitional=is_transitional)
         if state:
             node.state = state

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/core/task.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/core/task.py b/aria/orchestrator/workflows/core/task.py
index aa8963f..ba93e21 100644
--- a/aria/orchestrator/workflows/core/task.py
+++ b/aria/orchestrator/workflows/core/task.py
@@ -71,11 +71,11 @@ class StubTask(BaseTask):
 
     @property
     def has_ended(self):
-        return self.status in [models.Task.SUCCESS, models.Task.FAILED]
+        return self.status in (models.Task.SUCCESS, models.Task.FAILED)
 
     @property
     def is_waiting(self):
-        return self.status in [models.Task.PENDING, models.Task.RETRYING]
+        return self.status in (models.Task.PENDING, models.Task.RETRYING)
 
 
 class StartWorkflowTask(StubTask):
@@ -133,15 +133,14 @@ class OperationTask(BaseTask):
         task_model = create_task_model(
             name=api_task.name,
             implementation=api_task.implementation,
-            instance=api_task.actor,
+            actor=api_task.actor,
             inputs=api_task.inputs,
             status=base_task_model.PENDING,
             max_attempts=api_task.max_attempts,
             retry_interval=api_task.retry_interval,
             ignore_failure=api_task.ignore_failure,
             plugin=plugin,
-            execution=self._workflow_context.execution,
-            runs_on=api_task.runs_on
+            execution=self._workflow_context.execution
         )
         self._workflow_context.model.task.put(task_model)
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/exceptions.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/exceptions.py b/aria/orchestrator/workflows/exceptions.py
index 4fb8dd7..0ca263f 100644
--- a/aria/orchestrator/workflows/exceptions.py
+++ b/aria/orchestrator/workflows/exceptions.py
@@ -70,13 +70,19 @@ class TaskException(exceptions.AriaError):
     """
 
 
-class OperationNotFoundException(TaskException):
+class TaskCreationException(TaskException):
+    """
+    Could not create the task.
+    """
+
+
+class OperationNotFoundException(TaskCreationException):
     """
     Could not find an operation on the node or relationship.
     """
 
 
-class PluginNotFoundException(TaskException):
+class PluginNotFoundException(TaskCreationException):
     """
     Could not find a plugin matching the plugin specification.
     """

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/orchestrator/workflows/executor/process.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/executor/process.py b/aria/orchestrator/workflows/executor/process.py
index 6397e88..f814c4d 100644
--- a/aria/orchestrator/workflows/executor/process.py
+++ b/aria/orchestrator/workflows/executor/process.py
@@ -177,7 +177,7 @@ class ProcessExecutor(base.BaseExecutor):
                 pythonpath_dirs = [os.path.join(
                     plugin_prefix, 'lib{0}'.format(b),
                     'python{0}.{1}'.format(sys.version_info[0], sys.version_info[1]),
-                    'site-packages') for b in ['', '64']]
+                    'site-packages') for b in ('', '64')]
 
         # Add used supplied directories to injected PYTHONPATH
         pythonpath_dirs.extend(self._python_path)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/parser/consumption/modeling.py
----------------------------------------------------------------------
diff --git a/aria/parser/consumption/modeling.py b/aria/parser/consumption/modeling.py
index 4847ba7..6c616b4 100644
--- a/aria/parser/consumption/modeling.py
+++ b/aria/parser/consumption/modeling.py
@@ -103,7 +103,7 @@ class InstantiateServiceInstance(Consumer):
     def consume(self):
         if self.context.modeling.template is None:
             self.context.validation.report('InstantiateServiceInstance consumer: missing service '
-                                           'model')
+                                           'template')
             return
 
         self.context.modeling.template.instantiate(None)
@@ -145,6 +145,24 @@ class ValidateCapabilities(Consumer):
         self.context.modeling.instance.validate_capabilities()
 
 
+class FindHosts(Consumer):
+    """
+    Find hosts for all nodes in the service instance.
+    """
+
+    def consume(self):
+        self.context.modeling.instance.find_hosts()
+
+
+class ConfigureOperations(Consumer):
+    """
+    Configures all operations in the service instance.
+    """
+
+    def consume(self):
+        self.context.modeling.instance.configure_operations()
+
+
 class ServiceInstance(ConsumerChain):
     """
     Generates the service instance by instantiating the service template.
@@ -158,6 +176,8 @@ class ServiceInstance(ConsumerChain):
                                                         SatisfyRequirements,
                                                         CoerceServiceInstanceValues,
                                                         ValidateCapabilities,
+                                                        FindHosts,
+                                                        ConfigureOperations,
                                                         CoerceServiceInstanceValues))
 
     def dump(self):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/aria/storage/instrumentation.py
----------------------------------------------------------------------
diff --git a/aria/storage/instrumentation.py b/aria/storage/instrumentation.py
index fb95fcf..138432a 100644
--- a/aria/storage/instrumentation.py
+++ b/aria/storage/instrumentation.py
@@ -110,9 +110,9 @@ class _Instrumentation(object):
                         current = copy.deepcopy(attribute_type(initial))
                     tracked_attributes[attribute_name] = _Value(initial, current)
                 target.__dict__[attribute_name] = tracked_attributes[attribute_name].current
-        for listener_args in [(instrumented_class, 'load', listener),
+        for listener_args in ((instrumented_class, 'load', listener),
                               (instrumented_class, 'refresh', listener),
-                              (instrumented_class, 'refresh_flush', listener)]:
+                              (instrumented_class, 'refresh_flush', listener)):
             sqlalchemy.event.listen(*listener_args)
             self.listeners.append(listener_args)
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/elasticsearch.yaml
----------------------------------------------------------------------
diff --git a/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/elasticsearch.yaml b/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/elasticsearch.yaml
index 32623d1..72b210a 100644
--- a/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/elasticsearch.yaml
+++ b/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/elasticsearch.yaml
@@ -4,3 +4,5 @@ node_types:
 
   tosca.nodes.SoftwareComponent.Elasticsearch:
     derived_from: tosca.nodes.SoftwareComponent
+    capabilities:
+      app: tosca.capabilities.Endpoint

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/kibana.yaml
----------------------------------------------------------------------
diff --git a/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/kibana.yaml b/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/kibana.yaml
index 7af00d0..4ee8700 100644
--- a/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/kibana.yaml
+++ b/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/kibana.yaml
@@ -8,3 +8,5 @@ node_types:
       - search_endpoint:
           capability: tosca.capabilities.Endpoint
           relationship: tosca.relationships.ConnectsTo
+    capabilities:
+      app: tosca.capabilities.Endpoint

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/logstash.yaml
----------------------------------------------------------------------
diff --git a/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/logstash.yaml b/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/logstash.yaml
index a3eebbe..ea74c7e 100644
--- a/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/logstash.yaml
+++ b/examples/tosca-simple-1.0/use-cases/multi-tier-1/custom_types/logstash.yaml
@@ -8,3 +8,5 @@ node_types:
       - search_endpoint:
           capability: tosca.capabilities.Endpoint
           relationship: tosca.relationships.ConnectsTo
+    capabilities:
+      app: tosca.capabilities.Endpoint

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/custom_types/paypalpizzastore_nodejs_app.yaml
----------------------------------------------------------------------
diff --git a/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/custom_types/paypalpizzastore_nodejs_app.yaml b/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/custom_types/paypalpizzastore_nodejs_app.yaml
index 4723a3f..02bb399 100644
--- a/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/custom_types/paypalpizzastore_nodejs_app.yaml
+++ b/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/custom_types/paypalpizzastore_nodejs_app.yaml
@@ -9,7 +9,7 @@ node_types:
         type: string
     requirements:
       - database_connection:
-          capability: tosca.capabilities.Container
+          capability: tosca.capabilities.Node
 
   tosca.nodes.WebServer.Nodejs:
     derived_from: tosca.nodes.WebServer

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/webserver-dbms-2.yaml
----------------------------------------------------------------------
diff --git a/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/webserver-dbms-2.yaml b/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/webserver-dbms-2.yaml
index 66eab8e..91f0b35 100644
--- a/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/webserver-dbms-2.yaml
+++ b/examples/tosca-simple-1.0/use-cases/webserver-dbms-2/webserver-dbms-2.yaml
@@ -53,7 +53,7 @@ topology_template:
              implementation: scripts/nodejs/configure.sh
              inputs:
                github_url: { get_property: [ SELF, github_url ] }
-               mongodb_ip: { get_attribute: [mongo_server, private_address] }
+               mongodb_ip: { get_attribute: [ mongo_server, private_address ] }
            start: scripts/nodejs/start.sh
 
     nodejs:
@@ -86,7 +86,7 @@ topology_template:
           configure:
             implementation: mongodb/config.sh
             inputs:
-              mongodb_ip: { get_attribute: [mongo_server, private_address] }
+              mongodb_ip: { get_attribute: [ mongo_server, private_address ] }
           start: mongodb/start.sh
 
     mongo_server:
@@ -109,7 +109,7 @@ topology_template:
 
     nodejs_url:
       description: URL for the nodejs server, http://<IP>:3000
-      value: { get_attribute: [app_server, private_address] }
+      value: { get_attribute: [ app_server, private_address ] }
     mongodb_url:
       description: URL for the mongodb server.
       value: { get_attribute: [ mongo_server, private_address ] }

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/extensions/aria_extension_tosca/profiles/aria-1.0/aria-1.0.yaml
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/profiles/aria-1.0/aria-1.0.yaml b/extensions/aria_extension_tosca/profiles/aria-1.0/aria-1.0.yaml
index 09cef57..0c5e77f 100644
--- a/extensions/aria_extension_tosca/profiles/aria-1.0/aria-1.0.yaml
+++ b/extensions/aria_extension_tosca/profiles/aria-1.0/aria-1.0.yaml
@@ -17,6 +17,8 @@ policy_types:
 
   aria.Plugin:
     _extensions:
+      shorthand_name: Plugin
+      type_qualified_name: aria:Plugin
       role: plugin
     description: >-
       Policy used to specify plugins used by services. For an operation to be able to use a plugin
@@ -29,9 +31,17 @@ policy_types:
           Minimum plugin version.
         type: version
         required: false
+      enabled:
+        description: >-
+          If the policy is to disable the plugin then it will be ignored and all operations and
+          workflows depending on it will also be disabled.
+        type: boolean
+        default: true
 
   aria.Workflow:
     _extensions:
+      shorthand_name: Workflow
+      type_qualified_name: aria:Workflow
       role: workflow
     description: >-
       Policy used to specify custom workflows. A workflow is usually a workload of interconnected

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/capabilities.yaml
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/capabilities.yaml b/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/capabilities.yaml
index 72f6f0e..0b81a16 100644
--- a/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/capabilities.yaml
+++ b/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/capabilities.yaml
@@ -32,6 +32,7 @@ capability_types:
       specification: tosca-simple-1.0
       specification_section: 5.4.2
       specification_url: 'http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html#DEFN_TYPE_CAPABILITIES_NODE'
+      role: feature
     description: >-
       The Node capability indicates the base capabilities of a TOSCA Node Type.
     derived_from: tosca.capabilities.Root
@@ -43,6 +44,7 @@ capability_types:
       specification: tosca-simple-1.0
       specification_section: 5.4.3
       specification_url: 'http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html#DEFN_TYPE_CAPABILITIES_CONTAINER'
+      role: host
     description: >-
       The Container capability, when included on a Node Type or Template definition, indicates that the node can act as a container
       for (or a host for) one or more other declared Node Types.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/interfaces.yaml
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/interfaces.yaml b/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/interfaces.yaml
index de1d34f..ff6ba6c 100644
--- a/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/interfaces.yaml
+++ b/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/interfaces.yaml
@@ -63,24 +63,40 @@ interface_types:
     pre_configure_source:
       description: >-
         Operation to pre-configure the source endpoint.
+      _extensions:
+        relationship_edge: source
     pre_configure_target:
       description: >-
         Operation to pre-configure the target endpoint.
+      _extensions:
+        relationship_edge: target
     post_configure_source:
       description: >-
         Operation to post-configure the source endpoint.
+      _extensions:
+        relationship_edge: source
     post_configure_target:
       description: >-
         Operation to post-configure the target endpoint.
+      _extensions:
+        relationship_edge: target
     add_target:
       description: >-
         Operation to notify the source node of a target node being added via a relationship.
+      _extensions:
+        relationship_edge: source
     add_source:
       description: >-
         Operation to notify the target node of a source node which is now available via a relationship.
+      _extensions:
+        relationship_edge: target
     target_changed:
       description: >-
         Operation to notify source some property or attribute of the target changed
+      _extensions:
+        relationship_edge: source
     remove_target:
       description: >-
         Operation to remove a target node.
+      _extensions:
+        relationship_edge: source

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/nodes.yaml
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/nodes.yaml b/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/nodes.yaml
index 414a388..bb33b6f 100644
--- a/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/nodes.yaml
+++ b/extensions/aria_extension_tosca/profiles/tosca-simple-1.0/nodes.yaml
@@ -60,6 +60,7 @@ node_types:
       specification: tosca-simple-1.0
       specification_section: 5.8.2
       specification_url: 'http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html#DEFN_TYPE_NODES_COMPUTE'
+      role: host
     description: >-
       The TOSCA Compute node represents one or more real or virtual processors of software applications or services along with
       other essential local resources. Collectively, the resources the compute node represents can logically be viewed as a (real

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a7e7826e/extensions/aria_extension_tosca/simple_v1_0/assignments.py
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/simple_v1_0/assignments.py b/extensions/aria_extension_tosca/simple_v1_0/assignments.py
index 2a39ed9..6e36ba8 100644
--- a/extensions/aria_extension_tosca/simple_v1_0/assignments.py
+++ b/extensions/aria_extension_tosca/simple_v1_0/assignments.py
@@ -84,6 +84,25 @@ class OperationAssignment(ExtensiblePresentation):
         :rtype: dict of str, :class:`PropertyAssignment`
         """
 
+    @cachedmethod
+    def _get_extensions(self, context):
+        def update_inherited_extensions(extensions, interface_type):
+            parent = interface_type._get_parent(context)
+            if parent is not None:
+                update_inherited_extensions(extensions, parent)
+            operation_definition = interface_type.operations.get(self._name)
+            if operation_definition is not None:
+                if operation_definition._extensions:
+                    extensions.update(operation_definition._extensions)
+    
+        extensions = {}
+        update_inherited_extensions(extensions, self._container._get_type(context))
+        if self._container._extensions:
+            extensions.update(self._container._extensions)
+        if self._extensions:
+            extensions.update(self._extensions)
+        return extensions
+
 @allow_unknown_fields
 @has_fields
 @dsl_specification('3.5.14-2', 'tosca-simple-1.0')
@@ -247,15 +266,18 @@ class RequirementAssignment(ExtensiblePresentation):
 
     @cachedmethod
     def _get_node(self, context):
-        node_name = self.node
-        if node_name is not None:
-            node = context.presentation.get_from_dict('service_template', 'topology_template',
-                                                      'node_templates', node_name)
-            if node is not None:
-                return node, 'node_template'
-            node = context.presentation.get_from_dict('service_template', 'node_types', node_name)
-            if node is not None:
-                return node, 'node_type'
+        node = self.node
+
+        if node is not None:
+            node_template = context.presentation.get_from_dict('service_template',
+                                                               'topology_template',
+                                                               'node_templates', node)
+            if node_template is not None:
+                return node_template, 'node_template'
+            node_type = get_type_by_full_or_shorthand_name(context, node, 'node_types')
+            if node_type is not None:
+                return node_type, 'node_type'
+
         return None, None
 
     @cachedmethod
@@ -268,11 +290,10 @@ class RequirementAssignment(ExtensiblePresentation):
                 capabilities = node._get_capabilities(context)
                 if capability in capabilities:
                     return capabilities[capability], 'capability_assignment'
-            else:
-                capability_types = context.presentation.get_from_dict('service_template',
-                                                                      'capability_types')
-                if (capability_types is not None) and (capability in capability_types):
-                    return capability_types[capability], 'capability_type'
+            capability_type = get_type_by_full_or_shorthand_name(context, capability,
+                                                                 'capability_types')
+            if capability_type is not None:
+                return capability_type, 'capability_type'
 
         return None, None