Commit 6487b4f5 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

cyclades: Various fixes relative for volumes

* Do not store source_{volume_id, image_id, snapshot_id} as seperate
  fields. Instead store them in the 'source' field, with a special prefix
  to indicate the source type.
* Add 'origin' field to Volume model to contain the origin that will be
  passed to Ganeti disk
* Check that 'source' option is used only for ext_* volumes
* Check that if a volume is used as source, the volume is in 'AVAILABLE'
* Implement deleting a volume by detaching it from the corresponding
  server. Also, forbid detaching the root volume(index=0) of a server
* Rename 'hotplug' to 'hotplug_if_possible' in Ganeti methods.
* Rename 'name' and 'description' API attributes to 'display_name' and
parent a38bc041
......@@ -825,8 +825,8 @@ class NetworkInterface(models.Model):
device_owner = models.CharField('Device owner', max_length=128, null=True)
def __unicode__(self):
return "<%s:vm:%s network:%s>" % (, self.machine_id,
return "<NIC %s:vm:%s network:%s>" % (, self.machine_id,
def backend_uuid(self):
......@@ -1003,19 +1003,21 @@ class Volume(models.Model):
" volume")
name = models.CharField("Name", max_length=255, null=True)
description = models.CharField("Description", max_length=256, null=True)
userid = models.CharField("Owner's UUID", max_length=100, null=False,
size = models.IntegerField("Volume size in GB", null=False)
source_image_id = models.CharField(max_length=100, null=True)
source_snapshot_id = models.CharField(max_length=100, null=True)
source_volume = models.ForeignKey("Volume",
delete_on_termination = models.BooleanField("Delete on Server Termination",
default=True, null=False)
source = models.CharField(max_length=128, null=True)
origin = models.CharField(max_length=128, null=True)
# TODO: volume_type should be foreign key to VolumeType model
volume_type = None
deleted = models.BooleanField("Deleted", default=False, null=False)
......@@ -1042,6 +1044,63 @@ class Volume(models.Model):
def backend_disk_uuid(self):
return u"%sdisk-%d" % (settings.BACKEND_PREFIX_ID,
def source_image_id(self):
src = self.source
if src and src.startswith(self.SOURCE_IMAGE_PREFIX):
return src[len(self.SOURCE_IMAGE_PREFIX):]
return None
def source_snapshot_id(self):
src = self.source
if src and src.startswith(self.SOURCE_SNAPSHOT_PREFIX):
return src[len(self.SOURCE_SNAPSHOT_PREFIX):]
return None
def source_volume_id(self):
src = self.source
if src and src.startswith(self.SOURCE_VOLUME_PREFIX):
return src[len(self.SOURCE_VOLUME_PREFIX):]
return None
def disk_template(self):
if self.machine is None:
return None
disk_template = self.machine.flavor.disk_template
return disk_template.split("_")[0]
def disk_provider(self):
if self.machine is None:
return None
disk_template = self.machine.flavor.disk_template
if "_" in disk_template:
return disk_template.split("_")[1]
return None
def prefix_source(source_id, source_type):
if source_type == "volume":
return Volume.SOURCE_VOLUME_PREFIX + str(source_id)
if source_type == "snapshot":
return Volume.SOURCE_SNAPSHOT_PREFIX + str(source_id)
if source_type == "image":
return Volume.SOURCE_IMAGE_PREFIX + str(source_id)
elif source_type == "blank":
return None
def __unicode__(self):
return "<Volume %s:vm:%s>" % (, self.machine_id)
class Metadata(models.Model):
key = models.CharField("Metadata Key", max_length=64)
......@@ -34,7 +34,7 @@ from django.conf import settings
from django.db import transaction
from datetime import datetime, timedelta
from synnefo.db.models import (Backend, VirtualMachine, Network,
from synnefo.db.models import (VirtualMachine, Network,
pooled_rapi_client, VirtualMachineDiagnostic,
Flavor, IPAddress, IPAddressLog)
......@@ -637,7 +637,7 @@ def create_instance(vm, nics, volumes, flavor, image):
provider = flavor.disk_provider
if provider is not None:
disk["provider"] = provider
disk["origin"] = volume.source_image["checksum"]
disk["origin"] = volume.origin
extra_disk_params = settings.GANETI_DISK_PROVIDER_KWARGS\
if extra_disk_params is not None:
......@@ -1014,15 +1014,16 @@ def set_firewall_profile(vm, profile, nic):
def attach_volume(vm, volume, depends=[]):
log.debug("Attaching volume %s to vm %s", vm, volume)
disk = {"size": volume.size,
disk = {"size": int(volume.size) << 10,
"name": volume.backend_volume_uuid,
"volume_name": volume.backend_volume_uuid}
if volume.source_volume_id is not None:
disk["origin"] = volume.source_volume.backend_volume_uuid
elif volume.source_snapshot is not None:
disk["origin"] = volume.source_snapshot["checksum"]
elif volume.source_image is not None:
disk["origin"] = volume.source_image["checksum"]
disk_provider = volume.disk_provider
if disk_provider is not None:
disk["provider"] = disk_provider
if volume.origin is not None:
disk["origin"] = volume.origin
kwargs = {
"instance": vm.backend_vm_id,
......@@ -1030,7 +1031,7 @@ def attach_volume(vm, volume, depends=[]):
"depends": depends,
if vm.backend.use_hotplug():
kwargs["hotplug"] = True
kwargs["hotplug_if_possible"] = True
if settings.TEST:
kwargs["dry_run"] = True
......@@ -1038,14 +1039,15 @@ def attach_volume(vm, volume, depends=[]):
return client.ModifyInstance(**kwargs)
def detach_volume(vm, volume):
def detach_volume(vm, volume, depends=[]):
log.debug("Removing volume %s from vm %s", volume, vm)
kwargs = {
"instance": vm.backend_vm_id,
"disks": [("remove", volume.backend_volume_uuid, {})],
"depends": depends,
if vm.backend.use_hotplug():
kwargs["hotplug"] = True
kwargs["hotplug_if_possible"] = True
if settings.TEST:
kwargs["dry_run"] = True
......@@ -290,10 +290,10 @@ def create_instance_volumes(vm, flavor, image):
volume.source_image = image
return [volume]
from synnefo.db import models
from snf_django.lib.api import faults
from synnefo.api.util import get_image_dict, get_vm, image_backend
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
from synnefo.lib import join_urls
from import get_service_path
def get_volume(user_id, volume_id, for_update=False,
......@@ -36,3 +39,21 @@ def get_server(user_id, server_id, for_update=False,
non_deleted=True, non_suspended=True)
except faults.ItemNotFound:
raise exception("Server %s not found" % server_id)
get_service_path(cyclades_services, "volume", version="v2.0"))
VOLUMES_URL = join_urls(VOLUME_URL, "volumes/")
SNAPSHOTS_URL = join_urls(VOLUME_URL, "snapshots/")
def volume_to_links(volume_id):
href = join_urls(VOLUMES_URL, str(volume_id))
return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
def snapshot_to_links(snapshot_id):
href = join_urls(SNAPSHOTS_URL, str(snapshot_id))
return [{"rel": rel, "href": href} for rel in ("self", "bookmark")]
......@@ -58,15 +58,14 @@ def display_null_field(field):
def volume_to_dict(volume, detail=True):
data = {
"id": str(,
"name": display_null_field(,
# TODO: Links!
"links": "",
"display_name": display_null_field(,
"links": util.volume_to_dict(,
if detail:
details = {
"status": volume.status.lower(),
"size": volume.size,
"description": volume.description,
"display_description": volume.description,
"created_at": utils.isoformat(volume.created),
"metadata": dict((m.key, m.value) for m in volume.metadata.all()),
"snapshot_id": display_null_field(volume.source_snapshot_id),
......@@ -75,6 +74,7 @@ def volume_to_dict(volume, detail=True):
"attachments": get_volume_attachments(volume),
"volume_type": None,
"delete_on_termination": volume.delete_on_termination,
#"availabilit_zone": None,
#"bootable": None,
#"os-vol-tenant-attr:tenant_id": None,
......@@ -109,7 +109,7 @@ def create_volume(request):
# Get and validate 'name' parameter
# TODO: auto generate name
name = new_volume.get("name", None)
name = new_volume.get("display_name", None)
if name is None:
raise faults.BadRequest("Volume 'name' is needed.")
# Get and validate 'size' parameter
......@@ -128,7 +128,7 @@ def create_volume(request):
volume_type = new_volume.get("volume_type", None)
# Optional parameters
description = new_volume.get("description", "")
description = new_volume.get("display_description", "")
metadata = new_volume.get("metadata", {})
if not isinstance(metadata, dict):
msg = "Volume 'metadata' needs to be a dictionary of key-value pairs."\
......@@ -163,17 +163,11 @@ def create_volume(request):
@api.api_method(http_method="GET", user_required=True, logger=log)
def list_volumes(request, detail=False):
log.debug('list_volumes detail=%s', detail)
volumes = Volume.objects.filter(userid=request.user_uniq)
volumes = Volume.objects.filter(userid=request.user_uniq).order_by("id")
since = utils.isoparse(request.GET.get('changes-since'))
if since:
volumes = volumes.filter(updated__gte=since)
if not volumes:
return HttpResponse(status=304)
volumes = volumes.filter(deleted=False)
volumes = utils.filter_modified_since(request, objects=volumes)
volumes = [volume_to_dict(v, detail) for v in volumes.order_by("id")]
volumes = [volume_to_dict(v, detail) for v in volumes]
data = json.dumps({'volumes': volumes})
return HttpResponse(data, content_type="application/json", status=200)
......@@ -206,8 +200,8 @@ def update_volume(request, volume_id):
volume = util.get.volume(request.user_uniq, volume_id, for_update=True)
new_name = req.get("name")
description = req.get("description")
new_name = req.get("display_name")
description = req.get("display_description")
if new_name is None and description is None:
raise faults.BadRequest("Nothing to update.")
......@@ -25,28 +25,45 @@ 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!")
source_volume = None
# Only ext_ disk template supports cloning from another source
disk_template = server.flavor.disk_template
if not disk_template.startswith("ext_") and sources:
msg = ("Volumes of '%s' disk template cannot have a source" %
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,
source_snapshot = None
if source_snapshot_id is not None:
# 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.SOURCE_VOLUME_PREFIX + str(source_volume_id)
origin = source_volume.backend_volume_uuid
elif source_snapshot_id is not None:
source_snapshot = util.get_snapshot(user_id, source_snapshot_id,
source_image = None
if source_image_id is not None:
# TODO: Check the state of the snapshot!!
origin = source_snapshot["checksum"]
source = Volume.SOURCE_SNAPSHOT_PREFIX + str(source_snapshot_id)
elif source_image_id is not None:
source_image = util.get_image(user_id, source_image_id,
origin = source_image["checksum"]
source = Volume.SOURCE_IMAGE_PREFIX + str(source_image_id)
volume = Volume.objects.create(userid=user_id,
......@@ -54,10 +71,6 @@ def create(user_id, size, server_id, name=None, description=None,
for meta_key, meta_val in metadata.items():
volume.metadata.create(key=meta_key, value=meta_val)
# Annote volume with snapshot/image information
volume.source_snapshot = source_snapshot
volume.source_image = source_image
# Create the disk in the backend
volume.backendjobid = backend.attach_volume(server, volume)
......@@ -67,13 +80,18 @@ def create(user_id, size, server_id, name=None, description=None,
def delete(volume):
if volume.machine_id is not None:
raise faults.BadRequest("Volume %s is still in use by server %s"
% (, volume.machine_id))
volume.deleted = True
"""Delete a Volume"""
# A volume is deleted by detaching it from the server that is attached.
# Deleting a detached volume is not implemented.
if volume.index == 0:
raise faults.BadRequest("Cannot detach the root volume of a server")"Deleted volume %s", volume)
if volume.machine_id is not None:
volume.backendjobid = backend.detach_volume(volume.machine, volume)"Detach volume '%s' from server '%s', job: %s",, volume.machine_id, volume.backendjobid)
raise faults.BadRequest("Cannot delete a detached volume")
return volume
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment