Revision 5f90e24c
b/snf-cyclades-app/synnefo/db/models.py | ||
---|---|---|
824 | 824 |
device_owner = models.CharField('Device owner', max_length=128, null=True) |
825 | 825 |
|
826 | 826 |
def __unicode__(self): |
827 |
return "<%s:vm:%s network:%s>" % (self.id, self.machine_id, |
|
828 |
self.network_id) |
|
827 |
return "<NIC %s:vm:%s network:%s>" % (self.id, self.machine_id,
|
|
828 |
self.network_id)
|
|
829 | 829 |
|
830 | 830 |
@property |
831 | 831 |
def backend_uuid(self): |
... | ... | |
1002 | 1002 |
" volume") |
1003 | 1003 |
) |
1004 | 1004 |
|
1005 |
SOURCE_IMAGE_PREFIX = "image:" |
|
1006 |
SOURCE_SNAPSHOT_PREFIX = "snapshot:" |
|
1007 |
SOURCE_VOLUME_PREFIX = "volume:" |
|
1008 |
|
|
1005 | 1009 |
name = models.CharField("Name", max_length=255, null=True) |
1006 | 1010 |
description = models.CharField("Description", max_length=256, null=True) |
1007 | 1011 |
userid = models.CharField("Owner's UUID", max_length=100, null=False, |
1008 | 1012 |
db_index=True) |
1009 | 1013 |
size = models.IntegerField("Volume size in GB", null=False) |
1010 |
source_image_id = models.CharField(max_length=100, null=True) |
|
1011 |
source_snapshot_id = models.CharField(max_length=100, null=True) |
|
1012 |
source_volume = models.ForeignKey("Volume", |
|
1013 |
related_name="cloned_volumes", |
|
1014 |
null=True) |
|
1015 | 1014 |
delete_on_termination = models.BooleanField("Delete on Server Termination", |
1016 | 1015 |
default=True, null=False) |
1017 | 1016 |
|
1017 |
source = models.CharField(max_length=128, null=True) |
|
1018 |
origin = models.CharField(max_length=128, null=True) |
|
1019 |
|
|
1018 | 1020 |
# TODO: volume_type should be foreign key to VolumeType model |
1019 | 1021 |
volume_type = None |
1020 | 1022 |
deleted = models.BooleanField("Deleted", default=False, null=False) |
... | ... | |
1041 | 1043 |
def backend_disk_uuid(self): |
1042 | 1044 |
return u"%sdisk-%d" % (settings.BACKEND_PREFIX_ID, self.id) |
1043 | 1045 |
|
1046 |
@property |
|
1047 |
def source_image_id(self): |
|
1048 |
src = self.source |
|
1049 |
if src and src.startswith(self.SOURCE_IMAGE_PREFIX): |
|
1050 |
return src[len(self.SOURCE_IMAGE_PREFIX):] |
|
1051 |
else: |
|
1052 |
return None |
|
1053 |
|
|
1054 |
@property |
|
1055 |
def source_snapshot_id(self): |
|
1056 |
src = self.source |
|
1057 |
if src and src.startswith(self.SOURCE_SNAPSHOT_PREFIX): |
|
1058 |
return src[len(self.SOURCE_SNAPSHOT_PREFIX):] |
|
1059 |
else: |
|
1060 |
return None |
|
1061 |
|
|
1062 |
@property |
|
1063 |
def source_volume_id(self): |
|
1064 |
src = self.source |
|
1065 |
if src and src.startswith(self.SOURCE_VOLUME_PREFIX): |
|
1066 |
return src[len(self.SOURCE_VOLUME_PREFIX):] |
|
1067 |
else: |
|
1068 |
return None |
|
1069 |
|
|
1070 |
@property |
|
1071 |
def disk_template(self): |
|
1072 |
if self.machine is None: |
|
1073 |
return None |
|
1074 |
else: |
|
1075 |
disk_template = self.machine.flavor.disk_template |
|
1076 |
return disk_template.split("_")[0] |
|
1077 |
|
|
1078 |
@property |
|
1079 |
def disk_provider(self): |
|
1080 |
if self.machine is None: |
|
1081 |
return None |
|
1082 |
else: |
|
1083 |
disk_template = self.machine.flavor.disk_template |
|
1084 |
if "_" in disk_template: |
|
1085 |
return disk_template.split("_")[1] |
|
1086 |
else: |
|
1087 |
return None |
|
1088 |
|
|
1089 |
@staticmethod |
|
1090 |
def prefix_source(source_id, source_type): |
|
1091 |
if source_type == "volume": |
|
1092 |
return Volume.SOURCE_VOLUME_PREFIX + str(source_id) |
|
1093 |
if source_type == "snapshot": |
|
1094 |
return Volume.SOURCE_SNAPSHOT_PREFIX + str(source_id) |
|
1095 |
if source_type == "image": |
|
1096 |
return Volume.SOURCE_IMAGE_PREFIX + str(source_id) |
|
1097 |
elif source_type == "blank": |
|
1098 |
return None |
|
1099 |
|
|
1100 |
def __unicode__(self): |
|
1101 |
return "<Volume %s:vm:%s>" % (self.id, self.machine_id) |
|
1102 |
|
|
1044 | 1103 |
|
1045 | 1104 |
class Metadata(models.Model): |
1046 | 1105 |
key = models.CharField("Metadata Key", max_length=64) |
b/snf-cyclades-app/synnefo/logic/backend.py | ||
---|---|---|
34 | 34 |
from django.db import transaction |
35 | 35 |
from datetime import datetime, timedelta |
36 | 36 |
|
37 |
from synnefo.db.models import (Backend, VirtualMachine, Network,
|
|
37 |
from synnefo.db.models import (VirtualMachine, Network, |
|
38 | 38 |
BackendNetwork, BACKEND_STATUSES, |
39 | 39 |
pooled_rapi_client, VirtualMachineDiagnostic, |
40 | 40 |
Flavor, IPAddress, IPAddressLog) |
... | ... | |
637 | 637 |
provider = flavor.disk_provider |
638 | 638 |
if provider is not None: |
639 | 639 |
disk["provider"] = provider |
640 |
disk["origin"] = volume.source_image["checksum"]
|
|
640 |
disk["origin"] = volume.origin
|
|
641 | 641 |
extra_disk_params = settings.GANETI_DISK_PROVIDER_KWARGS\ |
642 | 642 |
.get(provider) |
643 | 643 |
if extra_disk_params is not None: |
... | ... | |
1042 | 1042 |
def attach_volume(vm, volume, depends=[]): |
1043 | 1043 |
log.debug("Attaching volume %s to vm %s", vm, volume) |
1044 | 1044 |
|
1045 |
disk = {"size": volume.size,
|
|
1045 |
disk = {"size": int(volume.size) << 10,
|
|
1046 | 1046 |
"name": volume.backend_volume_uuid, |
1047 | 1047 |
"volume_name": volume.backend_volume_uuid} |
1048 |
if volume.source_volume_id is not None: |
|
1049 |
disk["origin"] = volume.source_volume.backend_volume_uuid |
|
1050 |
elif volume.source_snapshot is not None: |
|
1051 |
disk["origin"] = volume.source_snapshot["checksum"] |
|
1052 |
elif volume.source_image is not None: |
|
1053 |
disk["origin"] = volume.source_image["checksum"] |
|
1048 |
|
|
1049 |
disk_provider = volume.disk_provider |
|
1050 |
if disk_provider is not None: |
|
1051 |
disk["provider"] = disk_provider |
|
1052 |
|
|
1053 |
if volume.origin is not None: |
|
1054 |
disk["origin"] = volume.origin |
|
1054 | 1055 |
|
1055 | 1056 |
kwargs = { |
1056 | 1057 |
"instance": vm.backend_vm_id, |
... | ... | |
1058 | 1059 |
"depends": depends, |
1059 | 1060 |
} |
1060 | 1061 |
if vm.backend.use_hotplug(): |
1061 |
kwargs["hotplug"] = True |
|
1062 |
kwargs["hotplug_if_possible"] = True
|
|
1062 | 1063 |
if settings.TEST: |
1063 | 1064 |
kwargs["dry_run"] = True |
1064 | 1065 |
|
... | ... | |
1066 | 1067 |
return client.ModifyInstance(**kwargs) |
1067 | 1068 |
|
1068 | 1069 |
|
1069 |
def detach_volume(vm, volume): |
|
1070 |
def detach_volume(vm, volume, depends=[]):
|
|
1070 | 1071 |
log.debug("Removing volume %s from vm %s", volume, vm) |
1071 | 1072 |
kwargs = { |
1072 | 1073 |
"instance": vm.backend_vm_id, |
1073 | 1074 |
"disks": [("remove", volume.backend_volume_uuid, {})], |
1075 |
"depends": depends, |
|
1074 | 1076 |
} |
1075 | 1077 |
if vm.backend.use_hotplug(): |
1076 |
kwargs["hotplug"] = True |
|
1078 |
kwargs["hotplug_if_possible"] = True
|
|
1077 | 1079 |
if settings.TEST: |
1078 | 1080 |
kwargs["dry_run"] = True |
1079 | 1081 |
|
b/snf-cyclades-app/synnefo/logic/servers.py | ||
---|---|---|
285 | 285 |
machine=vm, |
286 | 286 |
name=name, |
287 | 287 |
size=flavor.disk, |
288 |
source_image_id=image["id"], |
|
288 |
source=Volume.SOURCE_IMAGE_PREFIX+image["id"], |
|
289 |
origin=image["checksum"], |
|
289 | 290 |
status="CREATING") |
290 | 291 |
|
291 |
volume.source_image = image |
|
292 | 292 |
volume.save() |
293 | 293 |
|
294 | 294 |
return [volume] |
b/snf-cyclades-app/synnefo/volume/util.py | ||
---|---|---|
1 | 1 |
from synnefo.db import models |
2 | 2 |
from snf_django.lib.api import faults |
3 | 3 |
from synnefo.api.util import get_image_dict, get_vm, image_backend |
4 |
from synnefo.cyclades_settings import cyclades_services, BASE_HOST |
|
5 |
from synnefo.lib import join_urls |
|
6 |
from synnefo.lib.services import get_service_path |
|
4 | 7 |
|
5 | 8 |
|
6 | 9 |
def get_volume(user_id, volume_id, for_update=False, |
... | ... | |
36 | 39 |
non_deleted=True, non_suspended=True) |
37 | 40 |
except faults.ItemNotFound: |
38 | 41 |
raise exception("Server %s not found" % server_id) |
42 |
|
|
43 |
|
|
44 |
VOLUME_URL = \ |
|
45 |
join_urls(BASE_HOST, |
|
46 |
get_service_path(cyclades_services, "volume", version="v2.0")) |
|
47 |
|
|
48 |
VOLUMES_URL = join_urls(VOLUME_URL, "volumes/") |
|
49 |
SNAPSHOTS_URL = join_urls(VOLUME_URL, "snapshots/") |
|
50 |
|
|
51 |
|
|
52 |
def volume_to_links(volume_id): |
|
53 |
href = join_urls(VOLUMES_URL, str(volume_id)) |
|
54 |
return [{"rel": rel, "href": href} for rel in ("self", "bookmark")] |
|
55 |
|
|
56 |
|
|
57 |
def snapshot_to_links(snapshot_id): |
|
58 |
href = join_urls(SNAPSHOTS_URL, str(snapshot_id)) |
|
59 |
return [{"rel": rel, "href": href} for rel in ("self", "bookmark")] |
b/snf-cyclades-app/synnefo/volume/views.py | ||
---|---|---|
58 | 58 |
def volume_to_dict(volume, detail=True): |
59 | 59 |
data = { |
60 | 60 |
"id": str(volume.id), |
61 |
"name": display_null_field(volume.name), |
|
62 |
# TODO: Links! |
|
63 |
"links": "", |
|
61 |
"display_name": display_null_field(volume.name), |
|
62 |
"links": util.volume_to_dict(volume.id), |
|
64 | 63 |
} |
65 | 64 |
if detail: |
66 | 65 |
details = { |
67 | 66 |
"status": volume.status.lower(), |
68 | 67 |
"size": volume.size, |
69 |
"description": volume.description, |
|
68 |
"display_description": volume.description,
|
|
70 | 69 |
"created_at": utils.isoformat(volume.created), |
71 | 70 |
"metadata": dict((m.key, m.value) for m in volume.metadata.all()), |
72 | 71 |
"snapshot_id": display_null_field(volume.source_snapshot_id), |
... | ... | |
75 | 74 |
"attachments": get_volume_attachments(volume), |
76 | 75 |
# TODO: |
77 | 76 |
"volume_type": None, |
77 |
"delete_on_termination": volume.delete_on_termination, |
|
78 | 78 |
#"availabilit_zone": None, |
79 | 79 |
#"bootable": None, |
80 | 80 |
#"os-vol-tenant-attr:tenant_id": None, |
... | ... | |
109 | 109 |
|
110 | 110 |
# Get and validate 'name' parameter |
111 | 111 |
# TODO: auto generate name |
112 |
name = new_volume.get("name", None) |
|
112 |
name = new_volume.get("display_name", None)
|
|
113 | 113 |
if name is None: |
114 | 114 |
raise faults.BadRequest("Volume 'name' is needed.") |
115 | 115 |
# Get and validate 'size' parameter |
... | ... | |
128 | 128 |
volume_type = new_volume.get("volume_type", None) |
129 | 129 |
|
130 | 130 |
# Optional parameters |
131 |
description = new_volume.get("description", "") |
|
131 |
description = new_volume.get("display_description", "")
|
|
132 | 132 |
metadata = new_volume.get("metadata", {}) |
133 | 133 |
if not isinstance(metadata, dict): |
134 | 134 |
msg = "Volume 'metadata' needs to be a dictionary of key-value pairs."\ |
... | ... | |
163 | 163 |
@api.api_method(http_method="GET", user_required=True, logger=log) |
164 | 164 |
def list_volumes(request, detail=False): |
165 | 165 |
log.debug('list_volumes detail=%s', detail) |
166 |
volumes = Volume.objects.filter(userid=request.user_uniq) |
|
166 |
volumes = Volume.objects.filter(userid=request.user_uniq).order_by("id")
|
|
167 | 167 |
|
168 |
since = utils.isoparse(request.GET.get('changes-since')) |
|
169 |
if since: |
|
170 |
volumes = volumes.filter(updated__gte=since) |
|
171 |
if not volumes: |
|
172 |
return HttpResponse(status=304) |
|
173 |
else: |
|
174 |
volumes = volumes.filter(deleted=False) |
|
168 |
volumes = utils.filter_modified_since(request, objects=volumes) |
|
175 | 169 |
|
176 |
volumes = [volume_to_dict(v, detail) for v in volumes.order_by("id")]
|
|
170 |
volumes = [volume_to_dict(v, detail) for v in volumes] |
|
177 | 171 |
|
178 | 172 |
data = json.dumps({'volumes': volumes}) |
179 | 173 |
return HttpResponse(data, content_type="application/json", status=200) |
... | ... | |
206 | 200 |
|
207 | 201 |
volume = util.get.volume(request.user_uniq, volume_id, for_update=True) |
208 | 202 |
|
209 |
new_name = req.get("name") |
|
210 |
description = req.get("description") |
|
203 |
new_name = req.get("display_name")
|
|
204 |
description = req.get("display_description")
|
|
211 | 205 |
|
212 | 206 |
if new_name is None and description is None: |
213 | 207 |
raise faults.BadRequest("Nothing to update.") |
b/snf-cyclades-app/synnefo/volume/volumes.py | ||
---|---|---|
25 | 25 |
if len(sources) > 1: |
26 | 26 |
raise faults.BadRequest("Volume can not have more than one source!") |
27 | 27 |
|
28 |
source_volume = None |
|
28 |
# Only ext_ disk template supports cloning from another source |
|
29 |
disk_template = server.flavor.disk_template |
|
30 |
if not disk_template.startswith("ext_") and sources: |
|
31 |
msg = ("Volumes of '%s' disk template cannot have a source" % |
|
32 |
disk_template) |
|
33 |
raise faults.BadRequest(msg) |
|
34 |
|
|
35 |
origin = None |
|
36 |
source = None |
|
29 | 37 |
if source_volume_id is not None: |
30 | 38 |
source_volume = util.get_volume(user_id, source_volume_id, |
31 | 39 |
for_update=True, |
32 | 40 |
exception=faults.BadRequest) |
33 |
source_snapshot = None |
|
34 |
if source_snapshot_id is not None: |
|
41 |
# Check that volume is ready to be snapshotted |
|
42 |
if source_volume.status != "AVAILABLE": |
|
43 |
msg = ("Cannot take a snapshot while snapshot is in '%s' state" |
|
44 |
% source_volume.status) |
|
45 |
raise faults.BadRequest(msg) |
|
46 |
source = Volume.SOURCE_VOLUME_PREFIX + str(source_volume_id) |
|
47 |
origin = source_volume.backend_volume_uuid |
|
48 |
elif source_snapshot_id is not None: |
|
35 | 49 |
source_snapshot = util.get_snapshot(user_id, source_snapshot_id, |
36 | 50 |
exception=faults.BadRequest) |
37 |
source_image = None |
|
38 |
if source_image_id is not None: |
|
51 |
# TODO: Check the state of the snapshot!! |
|
52 |
origin = source_snapshot["checksum"] |
|
53 |
source = Volume.SOURCE_SNAPSHOT_PREFIX + str(source_snapshot_id) |
|
54 |
elif source_image_id is not None: |
|
39 | 55 |
source_image = util.get_image(user_id, source_image_id, |
40 | 56 |
exception=faults.BadRequest) |
57 |
origin = source_image["checksum"] |
|
58 |
source = Volume.SOURCE_IMAGE_PREFIX + str(source_image_id) |
|
41 | 59 |
|
42 | 60 |
volume = Volume.objects.create(userid=user_id, |
43 | 61 |
size=size, |
44 | 62 |
name=name, |
45 | 63 |
machine=server, |
46 | 64 |
description=description, |
47 |
source_volume=source_volume, |
|
48 |
source_image_id=source_image_id, |
|
49 |
source_snapshot_id=source_snapshot_id, |
|
65 |
source=source, |
|
66 |
origin=origin, |
|
50 | 67 |
#volume_type=volume_type, |
51 | 68 |
status="CREATING") |
52 | 69 |
|
... | ... | |
54 | 71 |
for meta_key, meta_val in metadata.items(): |
55 | 72 |
volume.metadata.create(key=meta_key, value=meta_val) |
56 | 73 |
|
57 |
# Annote volume with snapshot/image information |
|
58 |
volume.source_snapshot = source_snapshot |
|
59 |
volume.source_image = source_image |
|
60 |
|
|
61 | 74 |
# Create the disk in the backend |
62 | 75 |
volume.backendjobid = backend.attach_volume(server, volume) |
63 | 76 |
volume.save() |
... | ... | |
67 | 80 |
|
68 | 81 |
@transaction.commit_on_success |
69 | 82 |
def delete(volume): |
70 |
if volume.machine_id is not None:
|
|
71 |
raise faults.BadRequest("Volume %s is still in use by server %s"
|
|
72 |
% (volume.id, volume.machine_id))
|
|
73 |
volume.deleted = True
|
|
74 |
volume.save()
|
|
83 |
"""Delete a Volume"""
|
|
84 |
# A volume is deleted by detaching it from the server that is attached.
|
|
85 |
# Deleting a detached volume is not implemented.
|
|
86 |
if volume.index == 0:
|
|
87 |
raise faults.BadRequest("Cannot detach the root volume of a server")
|
|
75 | 88 |
|
76 |
log.info("Deleted volume %s", volume) |
|
89 |
if volume.machine_id is not None: |
|
90 |
volume.backendjobid = backend.detach_volume(volume.machine, volume) |
|
91 |
log.info("Detach volume '%s' from server '%s', job: %s", |
|
92 |
volume.id, volume.machine_id, volume.backendjobid) |
|
93 |
else: |
|
94 |
raise faults.BadRequest("Cannot delete a detached volume") |
|
77 | 95 |
|
78 | 96 |
return volume |
79 | 97 |
|
Also available in: Unified diff