Repository: xlab-si/xopera-opera

Issue:
Getting inputs from relationship types fails
## Description
The issue talks about a bug in opera that prevents getting inputs from relationship types.

So, let's take the following example:

```yaml
tosca_definitions_version: tosca_simple_yaml_1_3

node_types:
  radon.nodes.test:
    derived_from: tosca.nodes.Compute
    interfaces:
      Standard:
        type: tosca.interfaces.node.lifecycle.Standard
        operations:
          create: playbooks/create.yaml
          delete: playbooks/delete.yaml
    requirements:
      - host:
          capability: tosca.capabilities.Compute
          relationship: radon.relationships.test

relationship_types:
  radon.relationships.test:
    derived_from: tosca.relationships.HostedOn
    attributes:
      relationship_attribute:
        type: string
        default: relationship_attribute
    properties:
      relationship_property:
        type: string
        default: relationship_property
    interfaces:
      Configure:
        operations:
          post_configure_source:
            inputs:
              relationship_attribute:
                default: { get_attribute: [ SELF, relationship_attribute ] }
                type: string
              relationship_property:
                default: { get_property: [ SELF, relationship_property ] }
                type: string
              relationship_input:
                default: { get_input: test_relationship_input }
                type: string
            implementation: playbooks/configure_target.yaml

topology_template:
  inputs:
    host_ip:
      type: string
      default: localhost
    test_relationship_input:
      type: string
      default: test_relationship_input

  node_templates:
    my-workstation:
      type: tosca.nodes.Compute
      attributes:
        private_address: { get_input: host_ip }
        public_address: { get_input: host_ip }

    hello:
      type: radon.nodes.test
      requirements:
        - host: my-workstation
```

Here everything seems designed fine. TOSCA functions `get_attribute` and `get_property` work fine within the `radon.relationships.test` relationship. However when we try to get an input using `get_input` TOSCA function, opera returns the following result:

```console
(.venv) anzoman@ubuntu:~/Desktop/xopera-opera$ opera deploy templates/test.yaml 
[Worker_0]   Deploying my-workstation_0
[Worker_0]   Deployment of my-workstation_0 complete
[Worker_0]   Deploying hello_0
[Worker_0]     Executing create on hello_0
[Worker_0]     Executing post_configure_source on hello_0--my-workstation_0
Error processing node hello_0: 'Relationship' object has no attribute 'topology'
Traceback (most recent call last):
  File "/home/anzoman/Desktop/xopera-opera/.venv/bin/opera", line 8, in <module>
    sys.exit(main())
  File "/home/anzoman/Desktop/xopera-opera/.venv/lib/python3.8/site-packages/opera/cli.py", line 41, in main
    return args.func(args)
  File "/home/anzoman/Desktop/xopera-opera/.venv/lib/python3.8/site-packages/opera/commands/deploy.py", line 116, in _parser_callback
    deploy(service_template, inputs, storage, args.verbose, args.workers,
  File "/home/anzoman/Desktop/xopera-opera/.venv/lib/python3.8/site-packages/opera/commands/deploy.py", line 156, in deploy
    topology.deploy(verbose_mode, num_workers)
  File "/home/anzoman/Desktop/xopera-opera/.venv/lib/python3.8/site-packages/opera/instance/topology.py", line 27, in deploy
    do_deploy = executor.wait_results()
  File "/home/anzoman/Desktop/xopera-opera/.venv/lib/python3.8/site-packages/opera/threading/node_executor.py", line 45, in wait_results
    raise OperationError("Failed")
opera.error.OperationError: Failed
```
So, can we see that there is some error present in the opera code that makes the deployment fail, when we use `get_input` from the relationship type.

## Steps
To fix the issue in opera, we should take a look at places where we use `get_input` calls from the relationships and find out where the bug is.

## Current behaviour
Currently now opera returns an operation error when trying to get an input from the relationship type.

## Expected results
To be able to use `get_input` function within relationship types without any errors.


Discussion:
anzoman: @sstanovnik thanks for your review! The latest changes now include CI testing of the example located in `examples/relationship_outputs`.

Patch:
diff --git a/examples/relationship_outputs/files/file.txt b/examples/relationship_outputs/files/file.txt
new file mode 100644
index 0000000..ed43a70
--- /dev/null
+++ b/examples/relationship_outputs/files/file.txt
@@ -0,0 +1,1 @@
+This is an example file content.
diff --git a/examples/relationship_outputs/playbooks/create.yaml b/examples/relationship_outputs/playbooks/create.yaml
new file mode 100644
index 0000000..4d48a7e
--- /dev/null
+++ b/examples/relationship_outputs/playbooks/create.yaml
@@ -0,0 +1,12 @@
+- hosts: all
+  gather_facts: false
+
+  tasks:
+    - name: Say create
+      debug:
+        msg: "create"
+
+    - name: Set node attribute
+      set_stats:
+        data:
+          node_attribute: "Node attribute"
diff --git a/examples/relationship_outputs/playbooks/post_configure_source.yaml b/examples/relationship_outputs/playbooks/post_configure_source.yaml
new file mode 100644
index 0000000..bc4ae4d
--- /dev/null
+++ b/examples/relationship_outputs/playbooks/post_configure_source.yaml
@@ -0,0 +1,43 @@
+- hosts: all
+  gather_facts: false
+
+  tasks:
+    - name: Say post_configure_source
+      debug:
+        msg: "post_configure_source"
+
+    - name: Print out the value of relationship_attribute
+      debug:
+        msg: "{{ relationship_attribute }}"
+
+    - name: Print out the value of relationship_property
+      debug:
+        msg: "{{ relationship_property }}"
+
+    - name: Print out the value of relationship_input
+      debug:
+        msg: "{{ relationship_input }}"
+
+    - name: Set attribute
+      set_stats:
+        data:
+          post_configure_source_attribute: "{{ relationship_attribute }}"
+
+    - name: Set attribute
+      set_stats:
+        data:
+          post_configure_source_property_attribute: "{{ relationship_property }}"
+
+    - name: Set attribute
+      set_stats:
+        data:
+          post_configure_source_input_attribute: "{{ relationship_input }}"
+
+    - name: See what lies in that dependecy text file
+      command: "cat file.txt"
+      register: text_file_contents
+
+    - name: Set text file attribute
+      set_stats:
+        data:
+          post_configure_source_txt_file_attribute: "{{ text_file_contents.stdout }}"
diff --git a/examples/relationship_outputs/playbooks/post_configure_target.yaml b/examples/relationship_outputs/playbooks/post_configure_target.yaml
new file mode 100644
index 0000000..ce93b40
--- /dev/null
+++ b/examples/relationship_outputs/playbooks/post_configure_target.yaml
@@ -0,0 +1,12 @@
+- hosts: all
+  gather_facts: false
+
+  tasks:
+    - name: Say post_configure_target
+      debug:
+        msg: "post_configure_target"
+
+    - name: Set attribute
+      set_stats:
+        data:
+          post_configure_target_attribute: "This is post configure target attribute"
diff --git a/examples/relationship_outputs/playbooks/pre_configure_source.yaml b/examples/relationship_outputs/playbooks/pre_configure_source.yaml
new file mode 100644
index 0000000..f16b5a3
--- /dev/null
+++ b/examples/relationship_outputs/playbooks/pre_configure_source.yaml
@@ -0,0 +1,12 @@
+- hosts: all
+  gather_facts: false
+
+  tasks:
+    - name: Say pre_configure_source
+      debug:
+        msg: "pre_configure_source"
+
+    - name: Set attribute
+      set_stats:
+        data:
+          pre_configure_source_attribute: "This is pre configure source attribute"
diff --git a/examples/relationship_outputs/playbooks/pre_configure_target.yaml b/examples/relationship_outputs/playbooks/pre_configure_target.yaml
new file mode 100644
index 0000000..ffe65df
--- /dev/null
+++ b/examples/relationship_outputs/playbooks/pre_configure_target.yaml
@@ -0,0 +1,12 @@
+- hosts: all
+  gather_facts: false
+
+  tasks:
+    - name: Say pre_configure_target
+      debug:
+        msg: "pre_configure_target"
+
+    - name: Set attribute
+      set_stats:
+        data:
+          pre_configure_target_attribute: "This is pre configure target attribute"
diff --git a/examples/relationship_outputs/service.yaml b/examples/relationship_outputs/service.yaml
new file mode 100644
index 0000000..6aed878
--- /dev/null
+++ b/examples/relationship_outputs/service.yaml
@@ -0,0 +1,149 @@
+tosca_definitions_version: tosca_simple_yaml_1_3
+
+node_types:
+  radon.nodes.test:
+    derived_from: tosca.nodes.Compute
+    attributes:
+      node_attribute:
+        type: string
+    interfaces:
+      Standard:
+        type: tosca.interfaces.node.lifecycle.Standard
+        operations:
+          create: playbooks/create.yaml
+    requirements:
+      - host:
+          capability: tosca.capabilities.Compute
+          relationship: radon.relationships.test
+
+relationship_types:
+  radon.relationships.test:
+    derived_from: tosca.relationships.HostedOn
+    attributes:
+      relationship_attribute:
+        type: string
+        default: Default relationship attribute
+      pre_configure_source_attribute:
+        description: Attribute set by pre_configure_source interface operation
+        type: string
+      pre_configure_target_attribute:
+        description: Attribute set by pre_configure_target interface operation
+        type: string
+      post_configure_source_attribute:
+        description: >
+          Attribute set by post_configure_source interface operation from the
+          relationship_attribute attribute that enters as an operation input
+        type: string
+      post_configure_source_property_attribute:
+        description: >
+          Attribute set by post_configure_source interface operation from the
+          relationship_property property that enters as an operation input
+        type: string
+      post_configure_source_input_attribute:
+        description: >
+          Attribute set by post_configure_source interface operation from the
+          relationship_input input that enters as an operation input
+        type: string
+      post_configure_source_txt_file_attribute:
+        description: >
+          Attribute set by post_configure_source interface operation that
+          includes the contents of the dependent txt file
+        type: string
+      post_configure_target_attribute:
+        description: Attribute set by post_configure_target interface operation
+        type: string
+    properties:
+      relationship_property:
+        type: string
+        default: Default relationship property
+    interfaces:
+      Configure:
+        operations:
+          pre_configure_source:
+            implementation: playbooks/pre_configure_source.yaml
+          pre_configure_target:
+            implementation: playbooks/pre_configure_target.yaml
+          post_configure_source:
+            inputs:
+              relationship_attribute:
+                type: string
+                default: { get_attribute: [ SELF, relationship_attribute ] }
+              relationship_property:
+                type: string
+                default: { get_property: [ SELF, relationship_property ] }
+              relationship_input:
+                type: string
+                default: { get_input: relationship_input }
+            implementation:
+              primary: playbooks/post_configure_source.yaml
+              dependencies:
+                - files/file.txt
+          post_configure_target:
+            implementation: playbooks/post_configure_target.yaml
+
+topology_template:
+  inputs:
+    host_ip:
+      type: string
+      default: localhost
+    relationship_input:
+      type: string
+      default: Relationship input
+
+  node_templates:
+    my_workstation:
+      type: tosca.nodes.Compute
+      attributes:
+        private_address: { get_input: host_ip }
+        public_address: { get_input: host_ip }
+
+    test_node:
+      type: radon.nodes.test
+      requirements:
+        - host:
+            node: my_workstation
+            relationship: test_relationship
+
+  relationship_templates:
+    test_relationship:
+      type: radon.relationships.test
+      attributes:
+        relationship_attribute: Relationship attribute
+      properties:
+        relationship_property: Relationship property
+
+  outputs:
+    output_node_attribute:
+      description: Node attribute output
+      value: { get_attribute: [ test_node, node_attribute ] }
+    output_relationship_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, relationship_attribute ] }
+    output_relationship_property:
+      description: Relationship property output
+      value: { get_property: [ test_relationship, relationship_property ] }
+    output_relationship_input:
+      description: Relationship input output
+      value: { get_input: relationship_input }
+    output_pre_configure_source_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, pre_configure_source_attribute ] }
+    output_pre_configure_target_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, pre_configure_target_attribute ] }
+    output_post_configure_source_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, post_configure_source_attribute ] }
+    output_post_configure_source_property_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, post_configure_source_property_attribute ] }
+    output_post_configure_source_input_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, post_configure_source_input_attribute ] }
+    output_post_configure_source_txt_file_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, post_configure_source_txt_file_attribute ] }
+    output_post_configure_target_attribute:
+      description: Relationship attribute output
+      value: { get_attribute: [ test_relationship, post_configure_target_attribute ] }
+...
diff --git a/examples/runme.sh b/examples/runme.sh
index 4da3603..8b2c0a1 100755
--- a/examples/runme.sh
+++ b/examples/runme.sh
@@ -48,6 +48,12 @@ echo "Testing an example from ./policy_triggers ..."
 mkdir policy_triggers/.opera
 $opera_executable deploy -p policy_triggers/.opera policy_triggers/service.yaml
 
+# test an example from ./relationship_outputs
+echo "Testing an example from ./relationship_outputs ..."
+mkdir relationship_outputs/.opera
+$opera_executable deploy -p relationship_outputs/.opera relationship_outputs/service.yaml
+$opera_executable outputs -p relationship_outputs/.opera
+
 echo "All tests have finished successfully."
 
 # an end-to-end example from ./nginx_openstack cannot be deployed directly
diff --git a/src/opera/instance/base.py b/src/opera/instance/base.py
index 4943345..e1b23ed 100644
--- a/src/opera/instance/base.py
+++ b/src/opera/instance/base.py
@@ -70,7 +70,7 @@ class Base:
             raise OperationError("Failed")
 
         for params, value in outputs:
-            self.map_attribute(params, value)
+            self.template.map_attribute(params, value)
         self.update_attributes(attributes)
         self.write()
 
diff --git a/src/opera/instance/node.py b/src/opera/instance/node.py
index 129ed47..7654f23 100644
--- a/src/opera/instance/node.py
+++ b/src/opera/instance/node.py
@@ -20,10 +20,13 @@ class Node(Base):
             for target in requirement.target.instances.values():
                 target.in_edges[rname] = target.in_edges.get(rname, {})
 
-                rel = requirement.relationship.instantiate(self, target)
-                rel.topology = self.topology
-                self.out_edges[rname][target.tosca_id] = rel
-                target.in_edges[rname][self.tosca_id] = rel
+                rel_instances = requirement.relationship.instantiate(self, target)
+                for rel in rel_instances:
+                    rel.topology = self.topology
+                    self.out_edges[rname][target.tosca_id] = rel
+                    target.in_edges[rname][self.tosca_id] = rel
+                    target.in_edges[rname][self.tosca_id] = rel
+                    rel.read()
 
     @property
     def deployed(self):
@@ -124,12 +127,14 @@ class Node(Base):
     def get_attribute(self, params):
         host, attr, *rest = params
 
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
         if host != "SELF":
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Attribute host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the attribute "
+                "is referenced locally from something in the node itself."
             )
-        if host == "HOST":
-            raise DataError("HOST is not yet supported in opera.")
 
         # TODO(@tadeboro): Add support for nested attribute values once we
         # have data type support.
@@ -175,12 +180,14 @@ class Node(Base):
     def map_attribute(self, params, value):
         host, attr, *rest = params
 
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
         if host != "SELF":
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Attribute host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the attribute "
+                "is referenced locally from something in the node itself."
             )
-        if host == "HOST":
-            raise DataError("HOST is not yet supported in opera.")
 
         # TODO(@tadeboro): Add support for nested attribute values once we
         # have data type support.
diff --git a/src/opera/instance/relationship.py b/src/opera/instance/relationship.py
index 11ee89e..3bd9ccb 100644
--- a/src/opera/instance/relationship.py
+++ b/src/opera/instance/relationship.py
@@ -18,7 +18,7 @@ class Relationship(Base):
 
         if host not in ("SELF", "SOURCE", "TARGET"):
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Attribute host should be set to 'SELF', 'SOURCE' or 'TARGET'."
             )
 
         if host == "SOURCE":
@@ -37,7 +37,7 @@ class Relationship(Base):
 
         if host not in ("SELF", "SOURCE", "TARGET"):
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Property host should be set to 'SELF', 'SOURCE' or 'TARGET'."
             )
 
         if host == "SOURCE":
@@ -54,7 +54,7 @@ class Relationship(Base):
 
         if host not in ("SELF", "SOURCE", "TARGET"):
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Attribute host should be set to 'SELF', 'SOURCE' or 'TARGET'."
             )
 
         if host == "SOURCE":
@@ -69,7 +69,7 @@ class Relationship(Base):
 
         if host not in ("SELF", "SOURCE", "TARGET"):
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Artifact host should be set to 'SELF', 'SOURCE' or 'TARGET'."
             )
 
         if host == "SOURCE":
diff --git a/src/opera/parser/tosca/v_1_3/topology_template.py b/src/opera/parser/tosca/v_1_3/topology_template.py
index a973590..495bfbf 100644
--- a/src/opera/parser/tosca/v_1_3/topology_template.py
+++ b/src/opera/parser/tosca/v_1_3/topology_template.py
@@ -31,7 +31,12 @@ class TopologyTemplate(Entity):
             outputs=self.collect_outputs(service_ast),
             nodes={
                 name: node_ast.get_template(name, service_ast)
-                for name, node_ast in self.node_templates.items()
+                for name, node_ast in self.get("node_templates", {}).items()
+            },
+            relationships={
+                name: rel_ast.get_template(name, service_ast)
+                for name, rel_ast in
+                self.get("relationship_templates", {}).items()
             },
         )
         topology.resolve_requirements()
diff --git a/src/opera/template/node.py b/src/opera/template/node.py
index 50e528f..4eb1c76 100644
--- a/src/opera/template/node.py
+++ b/src/opera/template/node.py
@@ -2,9 +2,6 @@ from opera.error import DataError
 from opera.instance.node import Node as Instance
 from pathlib import Path
 
-import tempfile, os
-from ..executor import utils
-
 
 class Node:
     def __init__(
@@ -89,12 +86,14 @@ class Node:
     def get_property(self, params):
         host, prop, *rest = params
 
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
         if host != "SELF":
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Property host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the property is "
+                "referenced locally from something in the node itself."
             )
-        if host == "HOST":
-            raise DataError("HOST is not yet supported in opera.")
 
         # TODO(@tadeboro): Add support for nested property values.
         if prop in self.properties:
@@ -131,14 +130,16 @@ class Node:
         return requirements[0].target.get_property(["SELF"] + rest)
 
     def get_attribute(self, params):
-        host, prop, *rest = params
+        host, attr, *rest = params
 
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
         if host != "SELF":
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Attribute host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the attribute "
+                "is referenced locally from something in the node itself."
             )
-        if host == "HOST":
-            raise DataError("HOST is not yet supported in opera.")
         if len(self.instances) != 1:
             raise DataError("Cannot get an attribute from multiple instances")
 
@@ -150,12 +151,14 @@ class Node:
     def map_attribute(self, params, value):
         host, prop, *rest = params
 
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
         if host != "SELF":
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Attribute host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the attribute "
+                "is referenced locally from something in the node itself."
             )
-        if host == "HOST":
-            raise DataError("HOST is not yet supported in opera.")
 
         if len(self.instances) != 1:
             raise DataError(
@@ -175,12 +178,14 @@ class Node:
         if len(rest) == 2:
             location, remove = rest
 
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
         if host != "SELF":
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Artifact host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the artifact "
+                "is referenced locally from something in the node itself."
             )
-        if host == "HOST":
-            raise DataError("HOST is not yet supported in opera.")
 
         if location == "LOCAL_FILE":
             raise DataError("Location get_artifact property is "
diff --git a/src/opera/template/relationship.py b/src/opera/template/relationship.py
index 27cc41e..27e435e 100644
--- a/src/opera/template/relationship.py
+++ b/src/opera/template/relationship.py
@@ -10,12 +10,19 @@ class Relationship:
         self.attributes = attributes
         self.interfaces = interfaces
 
+        # This will be set when the relationship is inserted into a topology
+        self.topology = None
+
+        # This will be set at instantiation time.
+        self.instances = None
+
     def is_a(self, typ):
         return typ in self.types
 
     def instantiate(self, source, target):
         relationship_id = "{}--{}".format(source.tosca_id, target.tosca_id)
-        return Instance(self, relationship_id, source, target)
+        self.instances = {relationship_id: Instance(self, relationship_id, source, target)}
+        return self.instances.values()
 
     def run_operation(self, host, interface, operation, instance, verbose,
                       workdir):
@@ -30,18 +37,60 @@ class Relationship:
     def get_property(self, params):
         host, prop, *rest = params
 
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
         if host != "SELF":
             raise DataError(
-                "Accessing non-local stuff is bad. Fix your service template."
+                "Property host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the property is "
+                "referenced locally from something in the node itself."
             )
-        if host == "HOST":
-            raise DataError("HOST is not yet supported in opera.")
 
         # TODO(@tadeboro): Add support for nested property values once we
         # have data type support.
         if prop not in self.properties:
             raise DataError("Template has no '{}' property".format(prop))
+
         return self.properties[prop].eval(self, prop)
 
+    def get_attribute(self, params):
+        host, attr, *rest = params
+
+        if host == "HOST":
+            raise DataError("HOST is not yet supported in opera.")
+        if host != "SELF":
+            raise DataError(
+                "Attribute host should be set to 'SELF' which is the only "
+                "valid value. This is needed to indicate that the attribute "
+                "is referenced locally from something in the node itself."
+            )
+
+        if attr not in self.attributes:
+            raise DataError("Template has no '{}' attribute".format(attr))
+
+        try:
+            return self.attributes[attr].eval(self, attr)
+        except DataError:
+            pass
+
+        if len(self.instances) != 1:
+            raise DataError("Cannot get an attribute from multiple instances")
+
+        return next(iter(self.instances.values())).get_attribute(params)
+
     def get_input(self, params):
         return self.topology.get_input(params)
+
+    def map_attribute(self, params, value):
+        host, prop, *rest = params
+
+        if host not in ("SELF", "SOURCE", "TARGET"):
+            raise DataError(
+                "Attribute host should be set to 'SELF', 'SOURCE' or 'TARGET'."
+            )
+
+        if len(self.instances) != 1:
+            raise DataError(
+                "Mapping an attribute for multiple instances not supported")
+
+        next(iter(self.instances.values())).map_attribute(params, value)
diff --git a/src/opera/template/requirement.py b/src/opera/template/requirement.py
index 5aca3ef..9cb4280 100644
--- a/src/opera/template/requirement.py
+++ b/src/opera/template/requirement.py
@@ -7,3 +7,4 @@ class Requirement:
 
     def resolve(self, topology):
         self.target = topology.get_node(self.target_name)
+        self.relationship.topology = topology
diff --git a/src/opera/template/topology.py b/src/opera/template/topology.py
index 6e9fa22..d3259f4 100644
--- a/src/opera/template/topology.py
+++ b/src/opera/template/topology.py
@@ -5,10 +5,11 @@ from opera.instance.topology import Topology as Instance
 
 
 class Topology:
-    def __init__(self, inputs, outputs, nodes):
+    def __init__(self, inputs, outputs, nodes, relationships):
         self.inputs = inputs
         self.outputs = outputs
         self.nodes = nodes
+        self.relationships = relationships
 
         for node in self.nodes.values():
             node.topology = self
@@ -19,6 +20,14 @@ class Topology:
     def resolve_requirements(self):
         for node in self.nodes.values():
             node.resolve_requirements(self)
+        self.resolve_relationships()
+
+    def resolve_relationships(self):
+        for node in self.nodes.values():
+            for req in node.requirements:
+                relationship = req.relationship
+                if relationship.name in self.relationships.keys():
+                    self.relationships[relationship.name] = relationship
 
     def instantiate(self, storage):
         return Instance(storage, itertools.chain.from_iterable(
@@ -33,14 +42,41 @@ class Topology:
             ) for k, v in self.outputs.items()
         }
 
-    def find_node(self, node_name):
-        if node_name not in self.nodes:
+    def find_node_or_relationship(self, entity_name):
+        node = self.find_node(entity_name)
+        relationship = self.find_relationship(entity_name)
+
+        if not node and not relationship:
             raise DataError(
-                "Node template '{}' does not exist.".format(node_name),
+                "Node template or relationship template "
+                "'{}' does not exist.".format(entity_name),
             )
 
+        if node and relationship:
+            if node.name == relationship.name:
+                raise DataError(
+                    "Node template and relationship template "
+                    "'{}' should not have the same name.".format(entity_name),
+                )
+
+        if node:
+            return node
+
+        if relationship:
+            return relationship
+
+    def find_node(self, node_name):
+        if node_name not in self.nodes:
+            return None
+
         return self.nodes[node_name]
 
+    def find_relationship(self, relationship_name):
+        if relationship_name not in self.relationships:
+            return None
+
+        return self.relationships[relationship_name]
+
     #
     # TOSCA functions
     #
@@ -55,19 +91,18 @@ class Topology:
         return self.inputs[params[0]].eval(self, params[0])
 
     def get_property(self, params):
-        node_name, *rest = params
+        entity_name, *rest = params
 
-        return self.find_node(node_name).get_property(["SELF"] + rest)
+        return self.find_node_or_relationship(entity_name).get_property(["SELF"] + rest)
 
     def get_attribute(self, params):
-        node_name, *rest = params
-
-        return self.find_node(node_name).get_attribute(["SELF"] + rest)
+        entity_name, *rest = params
+        return self.find_node_or_relationship(entity_name).get_attribute(["SELF"] + rest)
 
     def get_artifact(self, params):
-        node_name, *rest = params
+        entity_name, *rest = params
 
-        return self.find_node(node_name).get_artifact(["SELF"] + rest)
+        return self.find_node_or_relationship(entity_name).get_artifact(["SELF"] + rest)
 
     def concat(self, params, node=None):
         if not isinstance(params, list):


Test patch:
diff --git a/tests/integration/misc_tosca_types/modules/relationship_types/test/playbooks/test.yaml b/tests/integration/misc_tosca_types/modules/relationship_types/test/playbooks/test.yaml
index 25596b5..0de3e78 100644
--- a/tests/integration/misc_tosca_types/modules/relationship_types/test/playbooks/test.yaml
+++ b/tests/integration/misc_tosca_types/modules/relationship_types/test/playbooks/test.yaml
@@ -10,4 +10,9 @@
     - name: Print whatever lies in relationship_property
       debug:
         msg: "{{ relationship_property }}"
+
+    - name: Set relationship attribute
+      set_stats:
+        data:
+          relationship_attribute: "This is a relationship attribute."
 ...
diff --git a/tests/integration/misc_tosca_types/modules/relationship_types/test/test.yaml b/tests/integration/misc_tosca_types/modules/relationship_types/test/test.yaml
index 6727939..4485c1f 100644
--- a/tests/integration/misc_tosca_types/modules/relationship_types/test/test.yaml
+++ b/tests/integration/misc_tosca_types/modules/relationship_types/test/test.yaml
@@ -4,6 +4,9 @@ tosca_definitions_version: tosca_simple_yaml_1_3
 relationship_types:
   daily_test.relationships.test:
     derived_from: tosca.relationships.HostedOn
+    attributes:
+      relationship_attribute:
+        type: string
     properties:
       relationship_property:
         type: string
diff --git a/tests/integration/misc_tosca_types/service-template.yaml b/tests/integration/misc_tosca_types/service-template.yaml
index 43995ff..2658a8f 100644
--- a/tests/integration/misc_tosca_types/service-template.yaml
+++ b/tests/integration/misc_tosca_types/service-template.yaml
@@ -92,18 +92,21 @@ topology_template:
           properties:
             capability_property: capability_property_value
       requirements:
-        - host1: my-workstation1
-        - host2: my-workstation2
+        - host:
+            node: my-workstation2
+            relationship: test_rel
       artifacts:
         artifact:
           type: daily_test.artifacts.test
           file: modules/node_types/test/file.test
 
   relationship_templates:
-    test:
+    test_rel:
       type: daily_test.relationships.test
+      attributes:
+        relationship_attribute: rel_attr_test123
       properties:
-        relationship_property: test123
+        relationship_property: rel_prop_test123
 
     interfaces:
       type: daily_test.relationships.interfaces
@@ -121,10 +124,16 @@ topology_template:
         targets: [ hello, setter, workstation_group ]
 
   outputs:
-    output_prop:
+    node_output_prop:
       description: Example of property output
       value: { get_property: [ setter, my_property ] }
-    output_attr:
+    node_output_attr:
       description: Example of attribute output
       value: { get_attribute: [ setter, my_attribute ] }
+    relationship_output_prop:
+      description: Example of attribute output
+      value: { get_property: [ test_rel, relationship_property ] }
+    relationship_output_attr:
+      description: Example of attribute output
+      value: { get_attribute: [ test_rel, relationship_attribute ] }
 ...
diff --git a/tests/unit/opera/instance/test_attribute_mapping.py b/tests/unit/opera/instance/test_attribute_mapping.py
index 05c9ec8..280ebc7 100644
--- a/tests/unit/opera/instance/test_attribute_mapping.py
+++ b/tests/unit/opera/instance/test_attribute_mapping.py
@@ -62,7 +62,8 @@ class TestAttributeMapping:
                               "other"])
     def test_map_attribute_node_bad_host(self, service_template, host):
         node = service_template.find_node("my_node")
-        with pytest.raises(DataError, match="Accessing non-local stuff"):
+        with pytest.raises(DataError, match="Attribute host should be set to "
+                                            "'SELF'"):
           node.map_attribute([host, "colour"], "black")
 
     def test_map_attribute_node_bad_attribute(self, service_template):
@@ -110,7 +111,8 @@ class TestAttributeMapping:
       relationship_instance = next(
         iter(node_source_instance.out_edges["my_target"].values()))
 
-      with pytest.raises(DataError, match="Accessing non-local stuff"):
+      with pytest.raises(DataError, match="Attribute host should be set to "
+                                          "'SELF'"):
         relationship_instance.map_attribute([host, "colour"], "ochre")
 
     @pytest.mark.parametrize("host, pattern", [