diff --git a/circle/occi/occi.py b/circle/occi/occi.py
index 0b7c48a..5d2a6f5 100644
--- a/circle/occi/occi.py
+++ b/circle/occi/occi.py
@@ -1,5 +1,6 @@
 import re
 
+from django.shortcuts import get_object_or_404
 from django.contrib.auth.models import User
 from django.template.loader import render_to_string
 from django.utils import timezone
@@ -43,6 +44,8 @@ occi_os_tpl_regex = re.compile(
     'class="mixin"; ?location=".*"; ?title=".*"$'
 )
 
+occi_attribute_link_regex = '^/%s/(?P<id>\d+)/?'
+
 
 class Category():
     """Represents a Category object
@@ -395,7 +398,7 @@ class StorageLink(Link):
 
     def init_attrs(self, instance, disk):
         self.attrs = {}
-        self.attrs['occi.core.id'] = "%d_at_%d" % (disk.pk, instance.pk)
+        self.attrs['occi.core.id'] = "vm%d_disk%d" % (instance.pk, disk.pk)
         self.attrs['occi.core.target'] = Storage(disk).render_location()
         self.attrs['occi.core.source'] = Compute(instance).render_location()
         # deviceid? mountpoint?
@@ -404,10 +407,40 @@ class StorageLink(Link):
         self.instance = instance
         self.disk = disk
 
+    @classmethod
+    def create_object(cls, data):
+        attributes = {}
+
+        for d in data:
+            attr = occi_attribute_regex.match(d)
+            if attr:
+                attributes[attr.group("attribute")] = attr.group("value")
+
+        source = attributes.get("occi.core.source")
+        target = attributes.get("occi.core.target")
+        if not (source and target):
+            return None
+
+        # TODO user
+        user = User.objects.get(username="test")
+        g = re.match(occi_attribute_link_regex % "storage", target)
+        disk_pk = g.group("id")
+        g = re.match(occi_attribute_link_regex % "vm", source)
+        vm_pk = g.group("id")
+
+        disk = get_object_or_404(Disk, pk=disk_pk)
+        vm = get_object_or_404(Instance, pk=vm_pk)
+
+
+        vm.attach_disk(user=user, disk=disk)
+        cls.location = "%sstoragelink/%svm_%sdisk" % (OCCI_ADDR, vm_pk,
+                                                      disk_pk)
+        return cls
+
     def render_location(self):
-        return "/link/storagelink/%d_at_%d" % (self.disk.pk, self.instance.pk)
+        return "/link/storagelink/vm%d_disk%d" % (instance.pk, disk.pk)
 
-    def render_body(self):
+    def render_as_link(self):
         kind = STORAGE_LINK_KIND
 
         return render_to_string("occi/link.html", {
@@ -417,6 +450,14 @@ class StorageLink(Link):
             'attrs': self.attrs,
         })
 
+    def render_as_category(self):
+        kind = STORAGE_LINK_KIND
+
+        return render_to_string("occi/storagelink.html", {
+            'kind': kind,
+            'attrs': self.attrs,
+        })
+
 
 """predefined stuffs
 
diff --git a/circle/occi/templates/occi/compute.html b/circle/occi/templates/occi/compute.html
index ea7bd15..36c3870 100644
--- a/circle/occi/templates/occi/compute.html
+++ b/circle/occi/templates/occi/compute.html
@@ -9,5 +9,5 @@ Category: compute; scheme="{{ kind.scheme }}"; class="{{ kind.class }}";
 {% endfor %}
 
 {% for l in links %}
-  {{ l.render_body }}
+  {{ l.render_as_link }}
 {% endfor %}
diff --git a/circle/occi/templates/occi/storagelink.html b/circle/occi/templates/occi/storagelink.html
new file mode 100644
index 0000000..dbda0fd
--- /dev/null
+++ b/circle/occi/templates/occi/storagelink.html
@@ -0,0 +1,4 @@
+Category: compute; scheme="{{ kind.scheme }}"; class="{{ kind.class }}";
+{% for k, v in attrs.items %}
+  X-OCCI-Attribute: {{ k }}={% if v.isdigit == False or k == "occi.core.id" %}"{{ v }}"{% else %}{{ v }}{% endif %}
+{% endfor %}
diff --git a/circle/occi/urls.py b/circle/occi/urls.py
index 4caa315..a434bd3 100644
--- a/circle/occi/urls.py
+++ b/circle/occi/urls.py
@@ -20,7 +20,7 @@ from django.conf.urls import url, patterns
 
 from occi.views import (
     QueryInterface, ComputeInterface, VmInterface, OsTplInterface,
-    StorageInterface, DiskInterface,
+    StorageInterface, DiskInterface, StorageLinkInterface
 )
 
 urlpatterns = patterns(
@@ -31,4 +31,8 @@ urlpatterns = patterns(
     url(r'^vm/(?P<pk>\d+)/$', VmInterface.as_view(), name="occi.vm"),
     url(r'^storage/$', StorageInterface.as_view(), name="occi.storage"),
     url(r'^disk/(?P<pk>\d+)/$', DiskInterface.as_view(), name="occi.disk"),
+
+    url(r'^link/storagelink/$', StorageLinkInterface.as_view()),
+    url(r'^link/storagelink/vm(?P<vm_pk>\d+)_disk(?P<disk_pk>\d+)/$',
+        StorageLinkInterface.as_view(), name="occi.storagelink"),
 )
diff --git a/circle/occi/views.py b/circle/occi/views.py
index d071e05..538b269 100644
--- a/circle/occi/views.py
+++ b/circle/occi/views.py
@@ -1,4 +1,5 @@
 from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
 from django.utils.decorators import method_decorator
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import View, DetailView
@@ -10,6 +11,7 @@ from .occi import (
     Compute,
     Storage,
     OsTemplate,
+    StorageLink,
     COMPUTE_KIND,
     STORAGE_KIND,
     LINK_KIND,
@@ -184,3 +186,40 @@ class DiskInterface(DetailView):
     @method_decorator(csrf_exempt)
     def dispatch(self, *args, **kwargs):
         return super(DiskInterface, self).dispatch(*args, **kwargs)
+
+
+class StorageLinkInterface(View):
+
+    def get_vm_and_disk(self):
+        vm = get_object_or_404(Instance, pk=self.kwargs['vm_pk'])
+        disk = get_object_or_404(Disk, pk=self.kwargs['disk_pk'])
+        return vm, disk
+
+    def get(self, request, *args, **kwargs):
+        vm, disk = self.get_vm_and_disk()
+        sl = StorageLink(instance=vm, disk=disk)
+        return HttpResponse(
+            sl.render_as_category(),
+            content_type="text/plain",
+        )
+
+    def post(self, request, *args, **kwargs):
+        # we don't support actions for storagelinks
+        # (they don't even exist in the model)
+        if request.GET.get("action"):
+            return HttpResponse("", status=500)
+        else:
+            data = get_post_data_from_request(request)
+            sl = StorageLink.create_object(data=data)
+            response = HttpResponse(
+                "X-OCCI-Location: %s" % sl.location,
+                status=201,
+                content_type="text/plain",
+            )
+            return response
+
+        return HttpResponse()
+
+    @method_decorator(csrf_exempt)
+    def dispatch(self, *args, **kwargs):
+        return super(StorageLinkInterface, self).dispatch(*args, **kwargs)
diff --git a/circle/vm/models/instance.py b/circle/vm/models/instance.py
index 541b7c1..a2d4672 100644
--- a/circle/vm/models/instance.py
+++ b/circle/vm/models/instance.py
@@ -817,7 +817,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
         return acts
 
     def get_merged_activities(self, user=None):
-        whitelist = ("create_disk", "download_disk")
+        whitelist = ("create_disk", "download_disk", "attach_disk")
         acts = self.get_activities(user)
         merged_acts = []
         latest = None
diff --git a/circle/vm/operations.py b/circle/vm/operations.py
index 9555341..9dc2789 100644
--- a/circle/vm/operations.py
+++ b/circle/vm/operations.py
@@ -1284,3 +1284,39 @@ class DetachNetwork(DetachMixin, AbstractNetworkOperation):
     id = "_detach_network"
     name = _("detach network")
     task = vm_tasks.detach_network
+
+
+@register_operation
+class AttachDiskOperation(InstanceOperation):
+    id = 'attach_disk'
+    name = _("attach disk")
+    description = _("Attach an already created disk to the virtual machine.")
+    required_perms = ()
+    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
+
+    def _operation(self, user, activity, disk):
+        devnums = list(ascii_lowercase)
+        for d in self.instance.disks.all():
+            devnums.remove(d.dev_num)
+        disk.dev_num = devnums.pop(0)
+        disk.save()
+
+        self.instance.disks.add(disk)
+
+        if self.instance.is_running:
+            with activity.sub_activity(
+                'deploying_disk',
+                readable_name=ugettext_noop("deploying disk")
+            ):
+                disk.deploy()
+            self.instance._attach_disk(parent_activity=activity, disk=disk)
+
+        activity.result = create_readable(
+            ugettext_noop("%(name)s (#%(pk)s), dev num: %(dev_num)s"),
+            name=disk.name, pk=disk.pk, dev_num=disk.dev_num
+        )
+
+    def get_activity_name(self, kwargs):
+        return create_readable(
+            ugettext_noop("attach disk %(name)s"),
+            name=kwargs['disk'].name)