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