Revision 79a1e9bd

b/snf-cyclades-app/synnefo/logic/backend.py
1024 1024
    return None
1025 1025

  
1026 1026

  
1027
def attach_volume(vm, volume, depends=[]):
1028
    log.debug("Attaching volume %s to vm %s", vm, volume)
1029

  
1030
    disk = {"size": volume.size,
1031
            "name": volume.backend_volume_uuid,
1032
            "volume_name": volume.backend_volume_uuid}
1033
    if volume.source_volume_id is not None:
1034
        disk["origin"] = volume.source_volume.backend_volume_uuid
1035
    elif volume.source_snapshot is not None:
1036
        disk["origin"] = volume.source_snapshot["checksum"]
1037
    elif volume.source_image is not None:
1038
        disk["origin"] = volume.source_image["checksum"]
1039

  
1040
    kwargs = {
1041
        "instance": vm.backend_vm_id,
1042
        "disks": [("add", "-1", disk)],
1043
        "depends": depends,
1044
    }
1045
    if vm.backend.use_hotplug():
1046
        kwargs["hotplug"] = True
1047
    if settings.TEST:
1048
        kwargs["dry_run"] = True
1049

  
1050
    with pooled_rapi_client(vm) as client:
1051
        return client.ModifyInstance(**kwargs)
1052

  
1053

  
1054
def detach_volume(vm, volume):
1055
    log.debug("Removing volume %s from vm %s", volume, vm)
1056
    kwargs = {
1057
        "instance": vm.backend_vm_id,
1058
        "disks": [("remove", volume.backend_volume_uuid, {})],
1059
    }
1060
    if vm.backend.use_hotplug():
1061
        kwargs["hotplug"] = True
1062
    if settings.TEST:
1063
        kwargs["dry_run"] = True
1064

  
1065
    with pooled_rapi_client(vm) as client:
1066
        return client.ModifyInstance(**kwargs)
1067

  
1068

  
1027 1069
def get_instances(backend, bulk=True):
1028 1070
    with pooled_rapi_client(backend) as c:
1029 1071
        return c.GetInstances(bulk=bulk)
b/snf-cyclades-app/synnefo/volume/snapshots.py
1
import datetime
2
from django.utils import simplejson as json
3
from django.db import transaction
4
from snf_django.lib.api import faults
5
from snf_django.lib.api.utils import isoformat
6
from synnefo.plankton.utils import image_backend
7
from synnefo.logic import backend
8
from synnefo.volume import util
9

  
10

  
11
SNAPSHOTS_CONTAINER = "snapshots"
12
SNAPSHOTS_DOMAIN = "plankton"
13
SNAPSHOTS_PREFIX = "plankton:"
14
SNAPSHOTS_TYPE = "application/octet-stream"
15
SNAPSHOTS_MAPFILE_PREFIX = "archip:"
16

  
17

  
18
@transaction.commit_on_success
19
def create(user_id, volume, name, description, metadata, force=False):
20

  
21
    if volume.machine is None:
22
        raise faults.BadRequest("Can not snapshot detached volume!")
23

  
24
    volume.snapshot_counter += 1
25
    volume.save()
26

  
27
    snapshot_metadata = {}
28
    snapshot_metadata[SNAPSHOTS_PREFIX + "name"] = description
29
    snapshot_metadata[SNAPSHOTS_PREFIX + "description"] = description
30
    snapshot_metadata[SNAPSHOTS_PREFIX + "metadata"] = json.dumps(metadata)
31
    snapshot_metadata[SNAPSHOTS_PREFIX + "volume_id"] = volume.id
32
    snapshot_metadata[SNAPSHOTS_PREFIX + "status"] = "CREATING"
33
    #XXX: just to work
34
    snapshot_metadata[SNAPSHOTS_PREFIX + "is_snapshot"] = True
35

  
36
    snapshot_name = generate_snapshot_name(volume)
37
    mapfile = SNAPSHOTS_MAPFILE_PREFIX + snapshot_name
38

  
39
    with image_backend(user_id) as pithos_backend:
40
        # move this to plankton backend
41
        snapshot_uuid = pithos_backend.backend.register_object_map(
42
            user=user_id,
43
            account=user_id,
44
            container=SNAPSHOTS_CONTAINER,
45
            name=name,
46
            size=volume.size,
47
            type=SNAPSHOTS_TYPE,
48
            mapfile=mapfile,
49
            meta=snapshot_metadata,
50
            replace_meta=False,
51
            permissions=None)
52
            #checksum=None,
53

  
54
    backend.snapshot_instance(volume.machine, snapshot_name=snapshot_uuid)
55

  
56
    snapshot = util.get_snapshot(user_id, snapshot_uuid)
57

  
58
    return snapshot
59

  
60

  
61
def generate_snapshot_name(volume):
62
    time = isoformat(datetime.datetime.now())
63
    return "snf-snapshot-of-volume-%s-%s-%s" % (volume.id,
64
                                                volume.snapshot_counter, time)
65

  
66

  
67
@transaction.commit_on_success
68
def delete(snapshot):
69
    user_id = snapshot["owner"]
70
    with image_backend(user_id) as pithos_backend:
71
        pithos_backend.delete_snapshot(snapshot["uuid"])
72
    return snapshot
b/snf-cyclades-app/synnefo/volume/util.py
1
from synnefo.db import models
2
from snf_django.lib.api import faults
3
from synnefo.api.util import get_image_dict, get_vm
4

  
5

  
6
def get_volume(user_id, volume_id, for_update=False,
7
               exception=faults.ItemNotFound):
8
    volumes = models.Volume.objects
9
    if for_update:
10
        volumes = volumes.select_for_update()
11
    try:
12
        return volumes.get(id=volume_id, userid=user_id)
13
    except models.Volume.DoesNotExist:
14
        raise exception("Volume %s not found" % volume_id)
15

  
16

  
17
def get_snapshot(user_id, snapshot_id, exception=faults.ItemNotFound):
18
    try:
19
        return get_image_dict(snapshot_id, user_id)
20
    except faults.ItemNotFound:
21
        raise exception("Snapshot %s not found" % snapshot_id)
22

  
23

  
24
def get_image(user_id, image_id, exception=faults.ItemNotFound):
25
    try:
26
        return get_image_dict(image_id, user_id)
27
    except faults.ItemNotFound:
28
        raise exception("Image %s not found" % image_id)
29

  
30

  
31
def get_server(user_id, server_id, for_update=False,
32
               exception=faults.ItemNotFound):
33
    try:
34
        return get_vm(server_id, user_id, for_update=for_update,
35
                      non_deleted=True, non_suspended=True)
36
    except faults.ItemNotFound:
37
        raise exception("Server %s not found" % server_id)
b/snf-cyclades-app/synnefo/volume/views.py
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from itertools import ifilter
35
from logging import getLogger
36
from django.http import HttpResponse
37
from django.utils import simplejson as json
38

  
39
import datetime
40
from dateutil.parser import parse as date_parse
41

  
42
from snf_django.lib import api
43
from snf_django.lib.api import faults, utils
44

  
45
from synnefo.volume import volumes, snapshots, util
46
from synnefo.db.models import Volume
47
from synnefo.plankton.utils import image_backend
48
log = getLogger('synnefo.volume')
49

  
50

  
51
def display_null_field(field):
52
    if field is None:
53
        return None
54
    else:
55
        str(field)
56

  
57

  
58
def volume_to_dict(volume, detail=True):
59
    data = {
60
        "id": str(volume.id),
61
        "name": display_null_field(volume.name),
62
        # TODO: Links!
63
        "links": "",
64
    }
65
    if detail:
66
        details = {
67
            "status": volume.status.lower(),
68
            "size": volume.size,
69
            "description": volume.description,
70
            "created_at": utils.isoformat(volume.created),
71
            "metadata": dict((m.key, m.value) for m in volume.metadata.all()),
72
            "snapshot_id": display_null_field(volume.source_snapshot_id),
73
            "source_volid": display_null_field(volume.source_volume_id),
74
            "image_id": display_null_field(volume.source_image_id),
75
            "attachments": get_volume_attachments(volume),
76
            # TODO:
77
            "volume_type": None,
78
            #"availabilit_zone": None,
79
            #"bootable": None,
80
            #"os-vol-tenant-attr:tenant_id": None,
81
            #"os-vol-host-attr:host": None,
82
            #"os-vol-mig-status-attr:name_id": None,
83
            #"os-vol-mig-status-attr:migstat": None,
84
        }
85
        data.update(details)
86
    return data
87

  
88

  
89
def get_volume_attachments(volume):
90
    if volume.machine_id is None:
91
        return []
92
    else:
93
        return [{"server_id": volume.machine_id,
94
                 "volume_id": volume.id,
95
                 "device_index": volume.index}]
96

  
97

  
98
@api.api_method(http_method="POST", user_required=True, logger=log)
99
def create_volume(request):
100
    """Create a new Volume."""
101

  
102
    req = utils.get_request_dict(request)
103
    log.debug("create_volume %s", req)
104

  
105
    user_id = request.user_uniq
106
    # Get and validate 'name' parameter
107
    # TODO: auto generate name
108
    name = req.get("name", None)
109
    if name is None:
110
        raise faults.BadRequest("Volume 'name' is needed.")
111
    # Get and validate 'size' parameter
112
    size = req.get("size")
113
    if size is None:
114
        raise faults.BadRequest("Volume 'size' is needed.")
115
    try:
116
        size = int(size)
117
        if size <= 0:
118
            raise ValueError
119
    except ValueError:
120
        raise faults.BadRequest("Volume 'size' needs to be a positive integer"
121
                                " value. '%s' cannot be accepted." % size)
122

  
123
    # TODO: Fix volume type, validate, etc..
124
    volume_type = req.get("volume_type", None)
125

  
126
    # Optional parameters
127
    description = req.get("description", "")
128
    metadata = req.get("metadata", {})
129
    if not isinstance(metadata, dict):
130
        msg = "Volume 'metadata' needs to be a dictionary of key-value pairs."\
131
              " '%s' can not be accepted." % metadata
132
        raise faults.BadRequest(msg)
133

  
134
    # Id of the volume to clone from
135
    source_volume_id = req.get("source_volid")
136
    # Id of the snapshot to create the volume from
137
    source_snapshot_id = req.get("snapshot_id")
138
    # Reference to an Image stored in Glance
139
    source_image_id = req.get("imageRef")
140
    # TODO: Check that not all of them are used
141

  
142
    server_id = req.get("server_id")
143
    if server_id is None:
144
        raise faults.BadRequest("Attribute 'server_id' is mandatory")
145

  
146
    # Create the volume
147
    volume = volumes.create(user_id=user_id, size=size, name=name,
148
                            source_volume_id=source_volume_id,
149
                            source_snapshot_id=source_snapshot_id,
150
                            source_image_id=source_image_id,
151
                            volume_type=volume_type, description=description,
152
                            metadata=metadata, server_id=server_id)
153

  
154
    # Render response
155
    data = json.dumps(dict(volume=volume_to_dict(volume, detail=False)))
156
    return HttpResponse(data, status=200)
157

  
158

  
159
@api.api_method(http_method="GET", user_required=True, logger=log)
160
def list_volumes(request, detail=False):
161
    log.debug('list_volumes detail=%s', detail)
162
    volumes = Volume.objects.filter(userid=request.user_uniq)
163

  
164
    since = utils.isoparse(request.GET.get('changes-since'))
165
    if since:
166
        volumes = volumes.filter(updated__gte=since)
167
        if not volumes:
168
            return HttpResponse(status=304)
169
    else:
170
        volumes = volumes.filter(deleted=False)
171

  
172
    volumes = [volume_to_dict(v, detail) for v in volumes.order_by("id")]
173

  
174
    data = json.dumps({'volumes': volumes})
175
    return HttpResponse(data, content_type="application/json", status=200)
176

  
177

  
178
@api.api_method(http_method="DELETE", user_required=True, logger=log)
179
def delete_volume(request, volume_id):
180
    log.debug("delete_volume volume_id: %s", volume_id)
181

  
182
    volume = util.get.volume(request.user_uniq, volume_id, for_update=True)
183
    volumes.delete(volume)
184

  
185
    return HttpResponse(status=202)
186

  
187

  
188
@api.api_method(http_method="GET", user_required=True, logger=log)
189
def get_volume(request, volume_id):
190
    log.debug('get_volume volume_id: %s', volume_id)
191

  
192
    volume = util.get.volume(request.user_uniq, volume_id)
193

  
194
    data = json.dumps({'volume': volume_to_dict(volume, detail=True)})
195
    return HttpResponse(data, content_type="application/json", status=200)
196

  
197

  
198
@api.api_method(http_method="PUT", user_required=True, logger=log)
199
def update_volume(request, volume_id):
200
    req = utils.get_request_dict(request)
201
    log.debug('update_volume volume_id: %s, request: %s', volume_id, req)
202

  
203
    volume = util.get.volume(request.user_uniq, volume_id, for_update=True)
204

  
205
    new_name = req.get("name")
206
    description = req.get("description")
207

  
208
    if new_name is None and description is None:
209
        raise faults.BadRequest("Nothing to update.")
210

  
211
    if new_name is not None:
212
        volume = volumes.rename(volume, new_name)
213
    if description is not None:
214
        volume = volumes.update_description(volume, description)
215
    data = json.dumps({'volume': volume_to_dict(volume, detail=True)})
216
    return HttpResponse(data, content_type="application/json", status=200)
217

  
218

  
219
def snapshot_to_dict(snapshot, detail=True):
220
    owner = snapshot["owner"]
221
    status = snapshot["status"]
222
    progress = snapshot["progress"]
223
    data = {
224
        "id": snapshot["uuid"],
225
        "size": int(snapshot["size"]) >> 30,  # gigabytes
226
        "name": snapshot["name"],
227
        "description": snapshot["description"],
228
        "status": status,
229
        "user_id": owner,
230
        "tenant_id": owner,
231
        "os-extended-snapshot-attribute:progress": progress,
232
        #"os-extended-snapshot-attribute:project_id": project,
233
        "created_at": utils.isoformat(date_parse(snapshot["created_at"])),
234
        "metadata": snapshot.get("metadata", {}),
235
        "volume_id": snapshot.get("volume_id"),
236
        "links": "",  # TODO fix links
237
    }
238
    return data
239

  
240

  
241
@api.api_method(http_method="POST", user_required=True, logger=log)
242
def create_snapshot(request):
243
    """Create a new Snapshot."""
244

  
245
    req = utils.get_request_dict(request)
246
    log.debug("create_snapshot %s", req)
247

  
248
    user_id = request.user_uniq
249
    # Get and validate 'name' parameter
250
    # TODO: auto generate name
251
    metadata = req.get("metadata", {})
252
    if not isinstance(metadata, dict):
253
        msg = "Snapshot 'metadata' needs to be a dictionary of key-value"\
254
              " pairs. '%s' can not be accepted." % metadata
255
        raise faults.BadRequest(msg)
256

  
257
    volume_id = req.get("volume_id", None)
258
    if volume_id is None:
259
        raise faults.BadRequest("'volume_id' attribute is missing.")
260
    volume = util.get_volume(user_id, volume_id, for_update=True,
261
                             exception=faults.BadRequest)
262

  
263
    name = req.get("name", None)
264
    if name is None:
265
        name = "snapshot_volume_%s_%s" %\
266
            (volume.id, str(datetime.datetime.now()))
267
    description = req.get("description", "")
268

  
269
    # TODO: What to do with force ?
270
    force = req.get("force", False)
271
    if not isinstance(force, bool):
272
        raise faults.BadRequest("Invalid value for 'force' attribute.")
273

  
274
    snapshot = snapshots.create(user_id=user_id, volume=volume, name=name,
275
                                description=description, metadata=metadata,
276
                                force=force)
277

  
278
    # Render response
279
    data = json.dumps(dict(snapshot=snapshot_to_dict(snapshot, detail=False)))
280
    return HttpResponse(data, status=200)  # TOO: Maybe 202 ?
281

  
282

  
283
@api.api_method(http_method="GET", user_required=True, logger=log)
284
def list_snapshots(request, detail=False):
285
    log.debug('list_snapshots detail=%s', detail)
286
    since = utils.isoparse(request.GET.get('changes-since'))
287
    with image_backend(request.user_uniq) as backend:
288
        snapshots = backend.list_snapshots()
289
        if since:
290
            updated_since = lambda snap:\
291
                date_parse(snap["updated_at"]) >= since
292
            snapshots = ifilter(updated_since, snapshots)
293
            if not snapshots:
294
                return HttpResponse(status=304)
295

  
296
    snapshots = sorted(snapshots, key=lambda x: x['id'])
297
    snapshots_dict = [snapshot_to_dict(snapshot, detail)
298
                      for snapshot in snapshots]
299

  
300
    data = json.dumps(dict(snapshots=snapshots_dict))
301

  
302
    return HttpResponse(data, status=200)
303

  
304

  
305
@api.api_method(http_method="DELETE", user_required=True, logger=log)
306
def delete_snapshot(request, snapshot_id):
307
    log.debug("delete_snapshot snapshot_id: %s", snapshot_id)
308

  
309
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
310
    snapshots.delete(snapshot)
311

  
312
    return HttpResponse(status=202)
313

  
314

  
315
@api.api_method(http_method="GET", user_required=True, logger=log)
316
def get_snapshot(request, snapshot_id):
317
    log.debug('get_snapshot snapshot_id: %s', snapshot_id)
318

  
319
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
320
    data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)})
321
    return HttpResponse(data, content_type="application/json", status=200)
322

  
323

  
324
@api.api_method(http_method="PUT", user_required=True, logger=log)
325
def update_snapshot(request, snapshot_id):
326
    req = utils.get_request_dict(request)
327
    log.debug('update_snapshot snapshot_id: %s, request: %s', snapshot_id, req)
328
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
329
    # TODO
330
    #snapshot.name = req.get("name", snapshot.name)
331
    #snapshot.description = req.get("description", snapshot.description)
332
    #snapshot.save()
333
    data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)})
334
    return HttpResponse(data, content_type="application/json", status=200)
b/snf-cyclades-app/synnefo/volume/volumes.py
1
import logging
2

  
3
from django.db import transaction
4
from synnefo.db.models import Volume
5
from snf_django.lib.api import faults
6
from synnefo.volume import util
7
from synnefo.logic import backend
8

  
9
log = logging.getLogger(__name__)
10

  
11

  
12
@transaction.commit_on_success
13
def create(user_id, size, server_id, name=None, description=None,
14
           source_volume_id=None, source_snapshot_id=None,
15
           source_image_id=None, metadata=None):
16

  
17
    if server_id is None:
18
        raise faults.BadRequest("Volume must be attached to server")
19
    server = util.get_server(user_id, server_id, for_update=True,
20
                             exception=faults.BadRequest)
21

  
22
    # Assert that not more than one source are used
23
    sources = filter(lambda x: x is not None,
24
                     [source_volume_id, source_snapshot_id, source_image_id])
25
    if len(sources) > 1:
26
        raise faults.BadRequest("Volume can not have more than one source!")
27

  
28
    source_volume = None
29
    if source_volume_id is not None:
30
        source_volume = util.get_volume(user_id, source_volume_id,
31
                                        for_update=True,
32
                                        exception=faults.BadRequest)
33
    source_snapshot = None
34
    if source_snapshot_id is not None:
35
        source_snapshot = util.get_snapshot(user_id, source_snapshot_id,
36
                                            exception=faults.BadRequest)
37
    source_image = None
38
    if source_image_id is not None:
39
        source_image = util.get_image(user_id, source_image_id,
40
                                      exception=faults.BadRequest)
41

  
42
    volume = Volume.objects.create(userid=user_id,
43
                                   size=size,
44
                                   name=name,
45
                                   machine=server,
46
                                   description=description,
47
                                   source_volume=source_volume,
48
                                   source_image_id=source_image_id,
49
                                   source_snapshot_id=source_snapshot_id,
50
                                   #volume_type=volume_type,
51
                                   status="CREATING")
52

  
53
    if metadata is not None:
54
        for meta_key, meta_val in metadata.items():
55
            volume.metadata.create(key=meta_key, value=meta_val)
56

  
57
    # Annote volume with snapshot/image information
58
    volume.source_snapshot = source_snapshot
59
    volume.source_image = source_image
60

  
61
    # Create the disk in the backend
62
    volume.backendjobid = backend.attach_volume(server, volume)
63
    volume.save()
64

  
65
    return volume
66

  
67

  
68
@transaction.commit_on_success
69
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()
75

  
76
    log.info("Deleted volume %s", volume)
77

  
78
    return volume
79

  
80

  
81
@transaction.commit_on_success
82
def rename(volume, new_name):
83
    volume.name = new_name
84
    volume.save()
85
    return volume
86

  
87

  
88
@transaction.commit_on_success
89
def update_description(volume, new_description):
90
    volume.description = new_description
91
    volume.save()
92
    return volume

Also available in: Unified diff