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)
