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/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:
