commit 5d80553360b232a31cf89fa560f73849cd3ea42c
Author: Christos Stavrakakis <cstavr@grnet.gr>
Date:   Thu Jan 23 23:10:59 2014 +0200

    cyclades: Create servers with volumes
    
    Extend Cyclades POST /servers API endpoint to support specifying volumes
    when creating a server. The call is extended with the
    'block_device_mapping_v2' field that contains a list of dictionaries,
    each of which contains the following information about the devices that
    the server must have:
    
    * source_type: One of'image', 'snapshot', 'volume' or 'blank'
    * uuid: The UUID of the object that is specified by source_type (if
            source_type is not blank)
    * size: The size of the volume
    * delete_on_termination: Whether the volumes will be preserved or
                             automaticaly deleted when the server is deleted
    
    If no volume is specified, then a volume is automatically created with
    the size of the flavor and with source the image that is specified in
    the 'imageRef' field of the API call.
    
    The follow restrictions apply:
    * Source_type can be other than 'blank', only if the disk template is
      ext_ or if the volume is created as that root volume of a server, so
      that it will filled with data by snf-image.
    * The root volume cannot be blank.
    * In order to use a volume, it must be in AVAILABLE status. This will
      never happend until detachable volumes. However the user can "clone"
      a user volume either by creating a new volume from cinder and then
      using this new volume or by taking a snapshot and then use the
      snapshot as a source.
    * The size field is required if the source is image, snapshot or blank.
      Also, the size of the volume must be equal or bigger from the source
      size (if any)
    
    Finally move some code from 'servers' module to 'server_attachments' and
    'commands' to avoid cyclic imports.

diff --git a/snf-cyclades-app/synnefo/api/servers.py b/snf-cyclades-app/synnefo/api/servers.py
index 3094308..6d826ed 100644
--- a/snf-cyclades-app/synnefo/api/servers.py
+++ b/snf-cyclades-app/synnefo/api/servers.py
@@ -66,6 +66,13 @@ urlpatterns = patterns(
     (r'^/(\d+)/os-volume_attachments/(\d+)(?:.json)?$', 'demux_volumes_item'),
 )
 
+VOLUME_SOURCE_TYPES = [
+    "snapshot",
+    "image",
+    "volume",
+    "blank"
+]
+
 
 def demux(request):
     if request.method == 'GET':
@@ -413,10 +420,13 @@ def create_server(request):
     except (KeyError, AssertionError):
         raise faults.BadRequest("Malformed request")
 
+    volumes = None
+    dev_map = server.get("block_device_mapping_v2")
+    if dev_map is not None:
+        volumes = parse_block_device_mapping(dev_map)
+
     # Verify that personalities are well-formed
     util.verify_personality(personality)
-    # Get image information
-    image = util.get_image_dict(image_id, user_id)
     # Get flavor (ensure it is active)
     flavor = util.get_flavor(flavor_id, include_deleted=False)
     if not flavor.allow_create:
@@ -426,9 +436,9 @@ def create_server(request):
     # Generate password
     password = util.random_password()
 
-    vm = servers.create(user_id, name, password, flavor, image,
+    vm = servers.create(user_id, name, password, flavor, image_id,
                         metadata=metadata, personality=personality,
-                        networks=networks)
+                        networks=networks, volumes=volumes)
 
     server = vm_to_dict(vm, detail=True)
     server['status'] = 'BUILD'
@@ -439,6 +449,65 @@ def create_server(request):
     return response
 
 
+def parse_block_device_mapping(dev_map):
+    """Parse 'block_device_mapping_v2' attribute"""
+    if not isinstance(dev_map, list):
+        raise faults.BadRequest("Block Device Mapping is Invalid")
+    return [_parse_block_device(device) for device in dev_map]
+
+
+def _parse_block_device(device):
+    """Parse and validate a block device mapping"""
+    if not isinstance(device, dict):
+        raise faults.BadRequest("Block Device Mapping is Invalid")
+
+    # Validate source type
+    source_type = device.get("source_type")
+    if source_type is None:
+        raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
+                                " source_type field")
+    elif source_type not in VOLUME_SOURCE_TYPES:
+        raise faults.BadRequest("Block Device Mapping is Invalid: source_type"
+                                " must be on of %s"
+                                % ", ".join(VOLUME_SOURCE_TYPES))
+
+    # Validate source UUID
+    uuid = device.get("uuid")
+    if uuid is None and source_type != "blank":
+        raise faults.BadRequest("Block Device Mapping is Invalid: uuid of"
+                                " %s is missing" % source_type)
+
+    # Validate volume size
+    size = device.get("volume_size")
+    if size is not None:
+        try:
+            size = int(size)
+        except (TypeError, ValueError):
+            raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
+                                    " size field")
+
+    # Validate 'delete_on_termination'
+    delete_on_termination = device.get("delete_on_termination")
+    if delete_on_termination is not None:
+        if not isinstance(delete_on_termination, bool):
+            raise faults.BadRequest("Block Device Mapping is Invalid: Invalid"
+                                    " delete_on_termination field")
+    else:
+        if source_type == "volume":
+            delete_on_termination = False
+        else:
+            delete_on_termination = True
+
+    # Unused API Attributes
+    # boot_index = device.get("boot_index")
+    # destination_type = device.get("destination_type")
+
+    return {"source_type": source_type,
+            "source_uuid": uuid,
+            "size": size,
+            "delete_on_termination": delete_on_termination}
+
+
 @api.api_method(http_method='GET', user_required=True, logger=log)
 def get_server_details(request, server_id):
     # Normal Response Codes: 200, 203
@@ -984,7 +1053,7 @@ def volume_to_attachment(volume):
     return {"id": volume.id,
             "volumeId": volume.id,
             "serverId": volume.machine_id,
-            "device": ""}  #  TODO: What device to return?
+            "device": ""}  # TODO: What device to return?
 
 
 @api.api_method(http_method='GET', user_required=True, logger=log)
diff --git a/snf-cyclades-app/synnefo/api/tests/servers.py b/snf-cyclades-app/synnefo/api/tests/servers.py
index d5074fd..cc7e2b8 100644
--- a/snf-cyclades-app/synnefo/api/tests/servers.py
+++ b/snf-cyclades-app/synnefo/api/tests/servers.py
@@ -37,7 +37,7 @@ from copy import deepcopy
 from snf_django.utils.testing import (BaseAPITest, mocked_quotaholder,
                                       override_settings)
 from synnefo.db.models import (VirtualMachine, VirtualMachineMetadata,
-                               IPAddress, NetworkInterface)
+                               IPAddress, NetworkInterface, Volume)
 from synnefo.db import models_factory as mfactory
 from synnefo.logic.utils import get_rsapi_state
 from synnefo.cyclades_settings import cyclades_services
@@ -320,11 +320,13 @@ fixed_image.return_value = {'location': 'pithos://foo',
                             'checksum': '1234',
                             "id": 1,
                             "name": "test_image",
-                            "size": "41242",
+                            "size": 1024,
+                            "is_snapshot": False,
                             'disk_format': 'diskdump'}
 
 
 @patch('synnefo.api.util.get_image', fixed_image)
+@patch('synnefo.volume.util.get_snapshot', fixed_image)
 @patch('synnefo.logic.rapi_pool.GanetiRapiClient')
 class ServerCreateAPITest(ComputeAPITest):
     def setUp(self):
@@ -585,6 +587,89 @@ class ServerCreateAPITest(ComputeAPITest):
                                        json.dumps(request), 'json')
         self.assertEqual(response.status_code, 404)
 
+    def test_create_server_with_volumes(self, mrapi):
+        user = "test_user"
+        mrapi().CreateInstance.return_value = 42
+        # Test creation without any volumes. Server will use flavor+image
+        request = deepcopy(self.request)
+        request["server"]["block_device_mapping_v2"] = []
+        with mocked_quotaholder():
+            response = self.mypost("servers", user,
+                                   json.dumps(request), 'json')
+        self.assertEqual(response.status_code, 202, msg=response.content)
+        vm_id = json.loads(response.content)["server"]["id"]
+        volume = Volume.objects.get(machine_id=vm_id)
+        self.assertEqual(volume.disk_template, self.flavor.disk_template)
+        self.assertEqual(volume.size, self.flavor.disk)
+        self.assertEqual(volume.source, "image:%s" % fixed_image()["id"])
+        self.assertEqual(volume.delete_on_termination, True)
+        self.assertEqual(volume.userid, user)
+
+        # Test using an image
+        request["server"]["block_device_mapping_v2"] = [
+            {"source_type": "image",
+             "uuid": fixed_image()["id"],
+             "volume_size": 10,
+             "delete_on_termination": False}
+        ]
+        with mocked_quotaholder():
+            response = self.mypost("servers", user,
+                                   json.dumps(request), 'json')
+        self.assertEqual(response.status_code, 202, msg=response.content)
+        vm_id = json.loads(response.content)["server"]["id"]
+        volume = Volume.objects.get(machine_id=vm_id)
+        self.assertEqual(volume.disk_template, self.flavor.disk_template)
+        self.assertEqual(volume.size, 10)
+        self.assertEqual(volume.source, "image:%s" % fixed_image()["id"])
+        self.assertEqual(volume.delete_on_termination, False)
+        self.assertEqual(volume.userid, user)
+        self.assertEqual(volume.origin, "pithos:" + fixed_image()["checksum"])
+
+        # Test using a snapshot
+        request["server"]["block_device_mapping_v2"] = [
+            {"source_type": "snapshot",
+             "uuid": fixed_image()["id"],
+             "volume_size": 10,
+             "delete_on_termination": False}
+        ]
+        with mocked_quotaholder():
+            response = self.mypost("servers", user,
+                                   json.dumps(request), 'json')
+        self.assertEqual(response.status_code, 202, msg=response.content)
+        vm_id = json.loads(response.content)["server"]["id"]
+        volume = Volume.objects.get(machine_id=vm_id)
+        self.assertEqual(volume.disk_template, self.flavor.disk_template)
+        self.assertEqual(volume.size, 10)
+        self.assertEqual(volume.source, "snapshot:%s" % fixed_image()["id"])
+        self.assertEqual(volume.origin, fixed_image()["checksum"])
+        self.assertEqual(volume.delete_on_termination, False)
+        self.assertEqual(volume.userid, user)
+
+        source_volume = volume
+        # Test using source volume
+        request["server"]["block_device_mapping_v2"] = [
+            {"source_type": "volume",
+             "uuid": source_volume.id,
+             "volume_size": source_volume.size,
+             "delete_on_termination": False}
+        ]
+        with mocked_quotaholder():
+            response = self.mypost("servers", user,
+                                   json.dumps(request), 'json')
+        # This will fail because the volume is not AVAILABLE.
+        self.assertBadRequest(response)
+
+        # Test using a blank volume
+        request["server"]["block_device_mapping_v2"] = [
+            {"source_type": "blank",
+             "volume_size": 10,
+             "delete_on_termination": True}
+        ]
+        with mocked_quotaholder():
+            response = self.mypost("servers", user,
+                                   json.dumps(request), 'json')
+        self.assertBadRequest(response)
+
 
 @patch('synnefo.logic.rapi_pool.GanetiRapiClient')
 class ServerDestroyAPITest(ComputeAPITest):
diff --git a/snf-cyclades-app/synnefo/logic/backend.py b/snf-cyclades-app/synnefo/logic/backend.py
index 2862006..6411e28 100644
--- a/snf-cyclades-app/synnefo/logic/backend.py
+++ b/snf-cyclades-app/synnefo/logic/backend.py
@@ -711,7 +711,7 @@ def create_instance(vm, nics, volumes, flavor, image):
     kw['name'] = vm.backend_vm_id
     # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
 
-    kw['disk_template'] = flavor.disk_template
+    kw['disk_template'] = volumes[0].template
     disks = []
     for volume in volumes:
         disk = {"name": volume.backend_volume_uuid,
diff --git a/snf-cyclades-app/synnefo/logic/commands.py b/snf-cyclades-app/synnefo/logic/commands.py
new file mode 100644
index 0000000..20b5841
--- /dev/null
+++ b/snf-cyclades-app/synnefo/logic/commands.py
@@ -0,0 +1,156 @@
+# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#   1. Redistributions of source code must retain the above copyright
+#      notice, this list of conditions and the following disclaimer.
+#
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and documentation are
+# those of the authors and should not be interpreted as representing official
+# policies, either expressed or implied, of GRNET S.A.
+
+import logging
+
+from functools import wraps
+from django.db import transaction
+
+from django.conf import settings
+from snf_django.lib.api import faults
+from synnefo import quotas
+from synnefo.db.models import VirtualMachine
+
+
+log = logging.getLogger(__name__)
+
+
+def validate_server_action(vm, action):
+    if vm.deleted:
+        raise faults.BadRequest("Server '%s' has been deleted." % vm.id)
+
+    # Destroyin a server should always be permitted
+    if action == "DESTROY":
+        return
+
+    # Check that there is no pending action
+    pending_action = vm.task
+    if pending_action:
+        if pending_action == "BUILD":
+            raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
+        raise faults.BadRequest("Cannot perform '%s' action while there is a"
+                                " pending '%s'." % (action, pending_action))
+
+    # Check if action can be performed to VM's operstate
+    operstate = vm.operstate
+    if operstate == "ERROR":
+        raise faults.BadRequest("Cannot perform '%s' action while server is"
+                                " in 'ERROR' state." % action)
+    elif operstate == "BUILD" and action != "BUILD":
+        raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
+    elif (action == "START" and operstate != "STOPPED") or\
+         (action == "STOP" and operstate != "STARTED") or\
+         (action == "RESIZE" and operstate != "STOPPED") or\
+         (action in ["CONNECT", "DISCONNECT"]
+          and operstate != "STOPPED"
+          and not settings.GANETI_USE_HOTPLUG) or \
+         (action in ["ATTACH_VOLUME", "DETACH_VOLUME"]
+          and operstate != "STOPPED"
+          and not settings.GANETI_USE_HOTPLUG):
+        raise faults.BadRequest("Cannot perform '%s' action while server is"
+                                " in '%s' state." % (action, operstate))
+    return
+
+
+def server_command(action, action_fields=None):
+    """Handle execution of a server action.
+
+    Helper function to validate and execute a server action, handle quota
+    commission and update the 'task' of the VM in the DB.
+
+    1) Check if action can be performed. If it can, then there must be no
+       pending task (with the exception of DESTROY).
+    2) Handle previous commission if unresolved:
+       * If it is not pending and it to accept, then accept
+       * If it is not pending and to reject or is pending then reject it. Since
+       the action can be performed only if there is no pending task, then there
+       can be no pending commission. The exception is DESTROY, but in this case
+       the commission can safely be rejected, and the dispatcher will generate
+       the correct ones!
+    3) Issue new commission and associate it with the VM. Also clear the task.
+    4) Send job to ganeti
+    5) Update task and commit
+    """
+    def decorator(func):
+        @wraps(func)
+        @transaction.commit_on_success
+        def wrapper(vm, *args, **kwargs):
+            user_id = vm.userid
+            validate_server_action(vm, action)
+            vm.action = action
+
+            commission_name = "client: api, resource: %s" % vm
+            quotas.handle_resource_commission(vm, action=action,
+                                              action_fields=action_fields,
+                                              commission_name=commission_name)
+            vm.save()
+
+            # XXX: Special case for server creation!
+            if action == "BUILD":
+                # Perform a commit, because the VirtualMachine must be saved to
+                # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti.
+                # Otherwise, messages will arrive from snf-dispatcher about
+                # this instance, before the VM is stored in DB.
+                transaction.commit()
+                # After committing the locks are released. Refetch the instance
+                # to guarantee x-lock.
+                vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
+
+            # Send the job to Ganeti and get the associated jobID
+            try:
+                job_id = func(vm, *args, **kwargs)
+            except Exception as e:
+                if vm.serial is not None:
+                    # Since the job never reached Ganeti, reject the commission
+                    log.debug("Rejecting commission: '%s', could not perform"
+                              " action '%s': %s" % (vm.serial,  action, e))
+                    transaction.rollback()
+                    quotas.reject_resource_serial(vm)
+                    transaction.commit()
+                raise
+
+            if action == "BUILD" and vm.serial is not None:
+                # XXX: Special case for server creation: we must accept the
+                # commission because the VM has been stored in DB. Also, if
+                # communication with Ganeti fails, the job will never reach
+                # Ganeti, and the commission will never be resolved.
+                quotas.accept_resource_serial(vm)
+
+            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
+                     user_id, vm.id, action, job_id, vm.serial)
+
+            # store the new task in the VM
+            if job_id is not None:
+                vm.task = action
+                vm.task_job_id = job_id
+            vm.save()
+
+            return vm
+        return wrapper
+    return decorator
diff --git a/snf-cyclades-app/synnefo/logic/management/commands/server-create.py b/snf-cyclades-app/synnefo/logic/management/commands/server-create.py
index 7c60ed7..82b1196 100644
--- a/snf-cyclades-app/synnefo/logic/management/commands/server-create.py
+++ b/snf-cyclades-app/synnefo/logic/management/commands/server-create.py
@@ -116,7 +116,7 @@ class Command(BaseCommand):
             backend = None
 
         connection_list = parse_connections(options["connections"])
-        server = servers.create(user_id, name, password, flavor, image,
+        server = servers.create(user_id, name, password, flavor, image["id"],
                                 networks=connection_list,
                                 use_backend=backend)
         pprint.pprint_server(server, stdout=self.stdout)
diff --git a/snf-cyclades-app/synnefo/logic/server_attachments.py b/snf-cyclades-app/synnefo/logic/server_attachments.py
new file mode 100644
index 0000000..a2c41bf
--- /dev/null
+++ b/snf-cyclades-app/synnefo/logic/server_attachments.py
@@ -0,0 +1,109 @@
+# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#   1. Redistributions of source code must retain the above copyright
+#      notice, this list of conditions and the following disclaimer.
+#
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and documentation are
+# those of the authors and should not be interpreted as representing official
+# policies, either expressed or implied, of GRNET S.A.
+
+import logging
+
+from snf_django.lib.api import faults
+from django.conf import settings
+from synnefo.logic import backend, commands
+
+log = logging.getLogger(__name__)
+
+
+@commands.server_command("ATTACH_VOLUME")
+def attach_volume(vm, volume):
+    """Attach a volume to a server.
+
+    The volume must be in 'AVAILABLE' status in order to be attached. Also,
+    number of the volumes that are attached to the server must remain less
+    than 'GANETI_MAX_DISKS_PER_INSTANCE' setting. This function will send
+    the corresponding job to Ganeti backend and update the status of the
+    volume to 'ATTACHING'.
+
+    """
+    # Check volume state
+    if volume.status not in ["AVAILABLE", "CREATING"]:
+        raise faults.BadRequest("Cannot attach volume while volume is in"
+                                " '%s' status." % volume.status)
+
+    # Check that disk templates are the same
+    if volume.disk_template != vm.flavor.disk_template:
+        msg = ("Volume and server must have the same disk template. Volume has"
+               " disk template '%s' while server has '%s'"
+               % (volume.disk_template, vm.flavor.disk_template))
+        raise faults.BadRequest(msg)
+
+    # Check maximum disk per instance hard limit
+    if vm.volumes.count() == settings.GANETI_MAX_DISKS_PER_INSTANCE:
+        raise faults.BadRequest("Maximum volumes per server limit reached")
+
+    jobid = backend.attach_volume(vm, volume)
+
+    log.info("Attached volume '%s' to server '%s'. JobID: '%s'", volume.id,
+             volume.machine_id, jobid)
+
+    volume.backendjobid = jobid
+    volume.machine = vm
+    volume.status = "ATTACHING"
+    volume.save()
+    return jobid
+
+
+@commands.server_command("DETACH_VOLUME")
+def detach_volume(vm, volume):
+    """Detach a volume to a server.
+
+    The volume must be in 'IN_USE' status in order to be detached. Also,
+    the root volume of the instance (index=0) can not be detached. This
+    function will send the corresponding job to Ganeti backend and update the
+    status of the volume to 'DETACHING'.
+
+    """
+
+    _check_attachment(vm, volume)
+    if volume.status != "IN_USE":
+        #TODO: Maybe allow other statuses as well ?
+        raise faults.BadRequest("Cannot detach volume while volume is in"
+                                " '%s' status." % volume.status)
+    if volume.index == 0:
+        raise faults.BadRequest("Cannot detach the root volume of a server")
+    jobid = backend.detach_volume(vm, volume)
+    log.info("Detached volume '%s' from server '%s'. JobID: '%s'", volume.id,
+             volume.machine_id, jobid)
+    volume.backendjobid = jobid
+    volume.status = "DETACHING"
+    volume.save()
+    return jobid
+
+
+def _check_attachment(vm, volume):
+    """Check that volume is attached to vm."""
+    if volume.machine_id != vm.id:
+        raise faults.BadRequest("Volume '%s' is not attached to server '%s'"
+                                % volume.id, vm.id)
diff --git a/snf-cyclades-app/synnefo/logic/servers.py b/snf-cyclades-app/synnefo/logic/servers.py
index 9ec38a2..6e16d6f 100644
--- a/snf-cyclades-app/synnefo/logic/servers.py
+++ b/snf-cyclades-app/synnefo/logic/servers.py
@@ -30,22 +30,22 @@
 import logging
 
 from socket import getfqdn
-from functools import wraps
 from django import dispatch
 from django.db import transaction
 from django.utils import simplejson as json
 
 from snf_django.lib.api import faults
 from django.conf import settings
-from synnefo import quotas
 from synnefo.api import util
 from synnefo.logic import backend, ips, utils
 from synnefo.logic.backend_allocator import BackendAllocator
 from synnefo.db.models import (NetworkInterface, VirtualMachine,
-                               VirtualMachineMetadata, IPAddressLog, Network,
-                               Volume)
+                               VirtualMachineMetadata, IPAddressLog, Network)
 from vncauthproxy.client import request_forwarding as request_vnc_forwarding
 from synnefo.logic import rapi
+from synnefo.volume.volumes import _create_volume
+from synnefo.volume.util import get_volume
+from synnefo.logic import commands
 
 log = logging.getLogger(__name__)
 
@@ -53,129 +53,20 @@ log = logging.getLogger(__name__)
 server_created = dispatch.Signal(providing_args=["created_vm_params"])
 
 
-def validate_server_action(vm, action):
-    if vm.deleted:
-        raise faults.BadRequest("Server '%s' has been deleted." % vm.id)
-
-    # Destroyin a server should always be permitted
-    if action == "DESTROY":
-        return
-
-    # Check that there is no pending action
-    pending_action = vm.task
-    if pending_action:
-        if pending_action == "BUILD":
-            raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
-        raise faults.BadRequest("Cannot perform '%s' action while there is a"
-                                " pending '%s'." % (action, pending_action))
-
-    # Check if action can be performed to VM's operstate
-    operstate = vm.operstate
-    if operstate == "ERROR":
-        raise faults.BadRequest("Cannot perform '%s' action while server is"
-                                " in 'ERROR' state." % action)
-    elif operstate == "BUILD" and action != "BUILD":
-        raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
-    elif (action == "START" and operstate != "STOPPED") or\
-         (action == "STOP" and operstate != "STARTED") or\
-         (action == "RESIZE" and operstate != "STOPPED") or\
-         (action in ["CONNECT", "DISCONNECT"]
-          and operstate != "STOPPED"
-          and not settings.GANETI_USE_HOTPLUG) or \
-         (action in ["ATTACH_VOLUME", "DETACH_VOLUME"]
-          and operstate != "STOPPED"
-          and not settings.GANETI_USE_HOTPLUG):
-        raise faults.BadRequest("Cannot perform '%s' action while server is"
-                                " in '%s' state." % (action, operstate))
-    return
-
-
-def server_command(action, action_fields=None):
-    """Handle execution of a server action.
-
-    Helper function to validate and execute a server action, handle quota
-    commission and update the 'task' of the VM in the DB.
-
-    1) Check if action can be performed. If it can, then there must be no
-       pending task (with the exception of DESTROY).
-    2) Handle previous commission if unresolved:
-       * If it is not pending and it to accept, then accept
-       * If it is not pending and to reject or is pending then reject it. Since
-       the action can be performed only if there is no pending task, then there
-       can be no pending commission. The exception is DESTROY, but in this case
-       the commission can safely be rejected, and the dispatcher will generate
-       the correct ones!
-    3) Issue new commission and associate it with the VM. Also clear the task.
-    4) Send job to ganeti
-    5) Update task and commit
-    """
-    def decorator(func):
-        @wraps(func)
-        @transaction.commit_on_success
-        def wrapper(vm, *args, **kwargs):
-            user_id = vm.userid
-            validate_server_action(vm, action)
-            vm.action = action
-
-            commission_name = "client: api, resource: %s" % vm
-            quotas.handle_resource_commission(vm, action=action,
-                                              action_fields=action_fields,
-                                              commission_name=commission_name)
-            vm.save()
-
-            # XXX: Special case for server creation!
-            if action == "BUILD":
-                # Perform a commit, because the VirtualMachine must be saved to
-                # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti.
-                # Otherwise, messages will arrive from snf-dispatcher about
-                # this instance, before the VM is stored in DB.
-                transaction.commit()
-                # After committing the locks are released. Refetch the instance
-                # to guarantee x-lock.
-                vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
-
-            # Send the job to Ganeti and get the associated jobID
-            try:
-                job_id = func(vm, *args, **kwargs)
-            except Exception as e:
-                if vm.serial is not None:
-                    # Since the job never reached Ganeti, reject the commission
-                    log.debug("Rejecting commission: '%s', could not perform"
-                              " action '%s': %s" % (vm.serial,  action, e))
-                    transaction.rollback()
-                    quotas.reject_resource_serial(vm)
-                    transaction.commit()
-                raise
-
-            if action == "BUILD" and vm.serial is not None:
-                # XXX: Special case for server creation: we must accept the
-                # commission because the VM has been stored in DB. Also, if
-                # communication with Ganeti fails, the job will never reach
-                # Ganeti, and the commission will never be resolved.
-                quotas.accept_resource_serial(vm)
-
-            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
-                     user_id, vm.id, action, job_id, vm.serial)
-
-            # store the new task in the VM
-            if job_id is not None:
-                vm.task = action
-                vm.task_job_id = job_id
-            vm.save()
-
-            return vm
-        return wrapper
-    return decorator
-
-
 @transaction.commit_on_success
-def create(userid, name, password, flavor, image, metadata={},
-           personality=[], networks=None, use_backend=None):
+def create(userid, name, password, flavor, image_id, metadata={},
+           personality=[], networks=None, volumes=None,
+           use_backend=None):
+
+    # Get image information
+    # TODO: Image is not mandatory if disks are specified
+    image = util.get_image_dict(image_id, userid)
 
     # Check that image fits into the disk
-    if image["size"] > (flavor.disk << 30):
-        msg = "Flavor's disk size '%s' is smaller than the image's size '%s'"
-        raise faults.BadRequest(msg % (flavor.disk << 30, image["size"]))
+    if int(image["size"]) > (flavor.disk << 30):
+        msg = ("Flavor's disk size '%s' is smaller than the image's"
+               "size '%s'" % (flavor.disk << 30, image["size"]))
+        raise faults.BadRequest(msg)
 
     if use_backend is None:
         # Allocate server to a Ganeti backend
@@ -187,18 +78,6 @@ def create(userid, name, password, flavor, image, metadata={},
     # Create the ports for the server
     ports = create_instance_ports(userid, networks)
 
-    # Fix flavor for archipelago
-    disk_template, provider = util.get_flavor_provider(flavor)
-    if provider:
-        flavor.disk_template = disk_template
-        flavor.disk_provider = provider
-        flavor.disk_origin = None
-        if provider in settings.GANETI_CLONE_PROVIDERS:
-            flavor.disk_origin = image['checksum']
-            image['backend_id'] = 'null'
-    else:
-        flavor.disk_provider = None
-
     # We must save the VM instance now, so that it gets a valid
     # vm.backend_vm_id.
     vm = VirtualMachine.objects.create(name=name,
@@ -215,7 +94,35 @@ def create(userid, name, password, flavor, image, metadata={},
         port.index = index
         port.save()
 
-    volumes = create_instance_volumes(vm, flavor, image)
+    # If no volumes are specified, we automatically create a volume with the
+    # size of the flavor and filled with the specified image.
+    if not volumes:
+        volumes = [{"source_type": "image",
+                    "source_uuid": image["id"],
+                    "size": flavor.disk,
+                    "delete_on_termination": True}]
+
+    assert(len(volumes) > 0), "Cannot create server without volumes"
+
+    if volumes[0]["source_type"] == "blank":
+        raise faults.BadRequest("Root volume cannot be blank")
+
+    server_volumes = []
+    for index, vol_info in enumerate(volumes):
+        if vol_info["source_type"] == "volume":
+            uuid = vol_info["source_uuid"]
+            v = get_volume(userid, uuid, for_update=True,
+                           exception=faults.BadRequest)
+            if v.status != "AVAILABLE":
+                raise faults.BadRequest("Cannot use volume while it is in %s"
+                                        " status" % v.status)
+            v.delete_on_termination = vol_info["delete_on_termination"]
+            v.index = index
+            v.save()
+        else:
+            v = _create_volume(server=vm, user_id=userid,
+                               index=index, **vol_info)
+        server_volumes.append(v)
 
     for key, val in metadata.items():
         VirtualMachineMetadata.objects.create(
@@ -224,7 +131,7 @@ def create(userid, name, password, flavor, image, metadata={},
             vm=vm)
 
     # Create the server in Ganeti.
-    vm = create_server(vm, ports, volumes, flavor, image, personality,
+    vm = create_server(vm, ports, server_volumes, flavor, image, personality,
                        password)
 
     return vm
@@ -250,13 +157,21 @@ def allocate_new_server(userid, flavor):
     return use_backend
 
 
-@server_command("BUILD")
+@commands.server_command("BUILD")
 def create_server(vm, nics, volumes, flavor, image, personality, password):
     # dispatch server created signal needed to trigger the 'vmapi', which
     # enriches the vm object with the 'config_url' attribute which must be
     # passed to the Ganeti job.
+
+    # If the root volume has a provider, then inform snf-image to not fill
+    # the volume with data
+    image_id = image["backend_id"]
+    root_volume = volumes[0]
+    if root_volume.origin is not None:
+        image_id = "null"
+
     server_created.send(sender=vm, created_vm_params={
-        'img_id': image['backend_id'],
+        'img_id': image_id,
         'img_passwd': password,
         'img_format': str(image['format']),
         'img_personality': json.dumps(personality),
@@ -283,22 +198,7 @@ def create_server(vm, nics, volumes, flavor, image, personality, password):
     return jobID
 
 
-def create_instance_volumes(vm, flavor, image):
-    name = "Root volume of server: %s" % vm.id
-    volume = Volume.objects.create(userid=vm.userid,
-                                   machine=vm,
-                                   name=name,
-                                   size=flavor.disk,
-                                   source=Volume.SOURCE_IMAGE_PREFIX+image["id"],
-                                   origin=image["checksum"],
-                                   status="CREATING")
-
-    volume.save()
-
-    return [volume]
-
-
-@server_command("DESTROY")
+@commands.server_command("DESTROY")
 def destroy(vm, shutdown_timeout=None):
     # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on
     # Ganeti before OP_INSTANCE_CREATE. This will be fixed when
@@ -312,19 +212,19 @@ def destroy(vm, shutdown_timeout=None):
     return backend.delete_instance(vm, shutdown_timeout=shutdown_timeout)
 
 
-@server_command("START")
+@commands.server_command("START")
 def start(vm):
     log.info("Starting VM %s", vm)
     return backend.startup_instance(vm)
 
 
-@server_command("STOP")
+@commands.server_command("STOP")
 def stop(vm, shutdown_timeout=None):
     log.info("Stopping VM %s", vm)
     return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
 
 
-@server_command("REBOOT")
+@commands.server_command("REBOOT")
 def reboot(vm, reboot_type, shutdown_timeout=None):
     if reboot_type not in ("SOFT", "HARD"):
         raise faults.BadRequest("Malformed request. Invalid reboot"
@@ -338,7 +238,7 @@ def reboot(vm, reboot_type, shutdown_timeout=None):
 def resize(vm, flavor):
     action_fields = {"beparams": {"vcpus": flavor.cpu,
                                   "maxmem": flavor.ram}}
-    comm = server_command("RESIZE", action_fields=action_fields)
+    comm = commands.server_command("RESIZE", action_fields=action_fields)
     return comm(_resize)(vm, flavor)
 
 
@@ -358,7 +258,7 @@ def _resize(vm, flavor):
     return backend.resize_instance(vm, vcpus=flavor.cpu, memory=flavor.ram)
 
 
-@server_command("SET_FIREWALL_PROFILE")
+@commands.server_command("SET_FIREWALL_PROFILE")
 def set_firewall_profile(vm, profile, nic):
     log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
 
@@ -368,7 +268,7 @@ def set_firewall_profile(vm, profile, nic):
     return None
 
 
-@server_command("CONNECT")
+@commands.server_command("CONNECT")
 def connect(vm, network, port=None):
     if port is None:
         port = _create_port(vm.userid, network)
@@ -379,7 +279,7 @@ def connect(vm, network, port=None):
     return backend.connect_to_network(vm, port)
 
 
-@server_command("DISCONNECT")
+@commands.server_command("DISCONNECT")
 def disconnect(vm, nic):
     log.info("Removing NIC %s from VM %s", nic, vm)
     return backend.disconnect_from_network(vm, nic)
@@ -760,76 +660,3 @@ def _port_for_request(user_id, network_dict):
     else:
         raise faults.BadRequest("Network 'uuid' or 'port' attribute"
                                 " is required.")
-
-
-@server_command("ATTACH_VOLUME")
-def attach_volume(vm, volume):
-    """Attach a volume to a server.
-
-    The volume must be in 'AVAILABLE' status in order to be attached. Also,
-    number of the volumes that are attached to the server must remain less
-    than 'GANETI_MAX_DISKS_PER_INSTANCE' setting. This function will send
-    the corresponding job to Ganeti backend and update the status of the
-    volume to 'ATTACHING'.
-
-    """
-    # Check volume state
-    if volume.status not in ["AVAILABLE", "CREATING"]:
-        raise faults.BadRequest("Cannot attach volume while volume is in"
-                                " '%s' status." % volume.status)
-
-    # Check that disk templates are the same
-    if volume.disk_template != vm.flavor.disk_template:
-        msg = ("Volume and server must have the same disk template. Volume has"
-               " disk template '%s' while server has '%s'"
-               % (volume.disk_template, vm.flavor.disk_template))
-        raise faults.BadRequest(msg)
-
-    # Check maximum disk per instance hard limit
-    if vm.volumes.count() == settings.GANETI_MAX_DISKS_PER_INSTANCE:
-        raise faults.BadRequest("Maximum volumes per server limit reached")
-
-    jobid = backend.attach_volume(vm, volume)
-
-    log.info("Attached volume '%s' to server '%s'. JobID: '%s'", volume.id,
-             volume.machine_id, jobid)
-
-    volume.backendjobid = jobid
-    volume.machine = vm
-    volume.status = "ATTACHING"
-    volume.save()
-    return jobid
-
-
-@server_command("DETACH_VOLUME")
-def detach_volume(vm, volume):
-    """Detach a volume to a server.
-
-    The volume must be in 'IN_USE' status in order to be detached. Also,
-    the root volume of the instance (index=0) can not be detached. This
-    function will send the corresponding job to Ganeti backend and update the
-    status of the volume to 'DETACHING'.
-
-    """
-
-    _check_attachment(vm, volume)
-    if volume.status != "IN_USE":
-        #TODO: Maybe allow other statuses as well ?
-        raise faults.BadRequest("Cannot detach volume while volume is in"
-                                " '%s' status." % volume.status)
-    if volume.index == 0:
-        raise faults.BadRequest("Cannot detach the root volume of a server")
-    jobid = backend.detach_volume(vm, volume)
-    log.info("Detached volume '%s' from server '%s'. JobID: '%s'", volume.id,
-             volume.machine_id, jobid)
-    volume.backendjobid = jobid
-    volume.status = "DETACHING"
-    volume.save()
-    return jobid
-
-
-def _check_attachment(vm, volume):
-    """Check that volume is attached to vm."""
-    if volume.machine_id != vm.id:
-        raise faults.BadRequest("Volume '%s' is not attached to server '%s'"
-                                % volume.id, vm.id)
diff --git a/snf-cyclades-app/synnefo/volume/volumes.py b/snf-cyclades-app/synnefo/volume/volumes.py
index 5dd70dd..83d29ba 100644
--- a/snf-cyclades-app/synnefo/volume/volumes.py
+++ b/snf-cyclades-app/synnefo/volume/volumes.py
@@ -1,10 +1,10 @@
 import logging
 
 from django.db import transaction
-from synnefo.db.models import Volume
 from snf_django.lib.api import faults
+from synnefo.db.models import Volume
 from synnefo.volume import util
-from synnefo.logic import backend, servers
+from synnefo.logic import server_attachments
 
 log = logging.getLogger(__name__)
 
@@ -14,6 +14,7 @@ def create(user_id, size, server_id, name=None, description=None,
            source_volume_id=None, source_snapshot_id=None,
            source_image_id=None, metadata=None):
 
+    # Currently we cannot create volumes without being attached to a server
     if server_id is None:
         raise faults.BadRequest("Volume must be attached to server")
     server = util.get_server(user_id, server_id, for_update=True,
@@ -25,55 +26,99 @@ def create(user_id, size, server_id, name=None, description=None,
     if len(sources) > 1:
         raise faults.BadRequest("Volume can not have more than one source!")
 
-    # Only ext_ disk template supports cloning from another source
+    if source_volume_id is not None:
+        source_type = "volume"
+        source_uuid = source_volume_id
+    elif source_snapshot_id is not None:
+        source_type = "snapshot"
+        source_uuid = source_snapshot_id
+    elif source_image_id is not None:
+        source_type = "image"
+        source_uuid = source_image_id
+    else:
+        source_type = source_uuid = None
+
+    volume = _create_volume(server, user_id, size, source_type, source_uuid,
+                            name, description, index=None)
+
+    if metadata is not None:
+        for meta_key, meta_val in metadata.items():
+            volume.metadata.create(key=meta_key, value=meta_val)
+
+    server_attachments.attach_volume(server, volume)
+
+    return volume
+
+
+def _create_volume(server, user_id, size, source_type, source_uuid,
+                   name=None, description=None, index=None,
+                   delete_on_termination=True):
+
+    # Only ext_ disk template supports cloning from another source. Otherwise
+    # is must be the root volume so that 'snf-image' fill the volume
     disk_template = server.flavor.disk_template
-    if not disk_template.startswith("ext_") and sources:
+    can_have_source = (index == 0 or disk_template.startswith("ext_"))
+    if not can_have_source and source_type != "blank":
         msg = ("Volumes of '%s' disk template cannot have a source" %
                disk_template)
         raise faults.BadRequest(msg)
 
-    origin = None
-    source = None
-    if source_volume_id is not None:
-        source_volume = util.get_volume(user_id, source_volume_id,
+    # TODO: Check Volume/Snapshot Status
+    if source_type == "volume":
+        source_volume = util.get_volume(user_id, source_uuid,
                                         for_update=True,
                                         exception=faults.BadRequest)
-        # Check that volume is ready to be snapshotted
-        if source_volume.status != "AVAILABLE":
-            msg = ("Cannot take a snapshot while snapshot is in '%s' state"
-                   % source_volume.status)
-            raise faults.BadRequest(msg)
-        source = Volume.prefix_source(source_volume_id, source_type="volume")
+        if source_volume.status != "IN_USE":
+            raise faults.BadRequest("Cannot clone volume while it is in '%s'"
+                                    " status" % source_volume.status)
+        # If no size is specified, use the size of the volume
+        if size is None:
+            size = source_volume.size
+        elif size < source_volume.size:
+            raise faults.BadRequest("Volume size cannot be smaller than the"
+                                    " source volume")
+        source = Volume.prefix_source(source_uuid, source_type="volume")
         origin = source_volume.backend_volume_uuid
-    elif source_snapshot_id is not None:
-        source_snapshot = util.get_snapshot(user_id, source_snapshot_id,
+    elif source_type == "snapshot":
+        source_snapshot = util.get_snapshot(user_id, source_uuid,
                                             exception=faults.BadRequest)
-        # TODO: Check the state of the snapshot!!
-        source = Volume.prefix_source(source_snapshot_id,
+        source = Volume.prefix_source(source_uuid,
                                       source_type="snapshot")
+        if size is None:
+            raise faults.BadRequest("Volume size is required")
+        elif (size << 30) < int(source_snapshot["size"]):
+            raise faults.BadRequest("Volume size '%s' is smaller than"
+                                    " snapshot's size '%s'"
+                                    % (size << 30, source_snapshot["size"]))
         origin = source_snapshot["checksum"]
-    elif source_image_id is not None:
-        source_image = util.get_image(user_id, source_image_id,
+    elif source_type == "image":
+        source_image = util.get_image(user_id, source_uuid,
                                       exception=faults.BadRequest)
-        source = Volume.prefix_source(source_image_id, source_type="image")
+        if size is None:
+            raise faults.BadRequest("Volume size is required")
+        elif (size << 30) < int(source_image["size"]):
+            raise faults.BadRequest("Volume size '%s' is smaller than"
+                                    " image's size '%s'"
+                                    % (size << 30, source_image["size"]))
+        source = Volume.prefix_source(source_uuid, source_type="image")
         origin = source_image["checksum"]
+    elif source_type == "blank":
+        if size is None:
+            raise faults.BadRequest("Volume size is required")
+        source = origin = None
+    else:
+        raise faults.BadRequest("Unknwon source type")
 
     volume = Volume.objects.create(userid=user_id,
                                    size=size,
                                    name=name,
                                    machine=server,
                                    description=description,
+                                   delete_on_termination=delete_on_termination,
                                    source=source,
                                    origin=origin,
                                    #volume_type=volume_type,
                                    status="CREATING")
-
-    if metadata is not None:
-        for meta_key, meta_val in metadata.items():
-            volume.metadata.create(key=meta_key, value=meta_val)
-
-    servers.attach_volume(server, volume)
-
     return volume
 
 
@@ -83,7 +128,7 @@ def delete(volume):
     # A volume is deleted by detaching it from the server that is attached.
     # Deleting a detached volume is not implemented.
     if volume.machine_id is not None:
-        servers.detach_volume(volume.machine, volume)
+        server_attachments.detach_volume(volume.machine, volume)
         log.info("Detach volume '%s' from server '%s', job: %s",
                  volume.id, volume.machine_id, volume.backendjobid)
     else:
