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