Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / volume / views.py @ a868c831

History | View | Annotate | Download (12.7 kB)

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
        "display_name": display_null_field(volume.name),
62
        "links": util.volume_to_dict(volume.id),
63
    }
64
    if detail:
65
        details = {
66
            "status": volume.status.lower(),
67
            "size": volume.size,
68
            "display_description": volume.description,
69
            "created_at": utils.isoformat(volume.created),
70
            "metadata": dict((m.key, m.value) for m in volume.metadata.all()),
71
            "snapshot_id": display_null_field(volume.source_snapshot_id),
72
            "source_volid": display_null_field(volume.source_volume_id),
73
            "image_id": display_null_field(volume.source_image_id),
74
            "attachments": get_volume_attachments(volume),
75
            # TODO:
76
            "volume_type": None,
77
            "delete_on_termination": volume.delete_on_termination,
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
    user_id = request.user_uniq
105

    
106
    new_volume = req.get("volume")
107
    if new_volume is None:
108
        raise faults.BadRequest("Missing 'volume' attribute.")
109

    
110
    # Get and validate 'name' parameter
111
    # TODO: auto generate name
112
    name = new_volume.get("display_name", None)
113
    if name is None:
114
        raise faults.BadRequest("Volume 'name' is needed.")
115
    # Get and validate 'size' parameter
116
    size = new_volume.get("size")
117
    if size is None:
118
        raise faults.BadRequest("Volume 'size' is needed.")
119
    try:
120
        size = int(size)
121
        if size <= 0:
122
            raise ValueError
123
    except ValueError:
124
        raise faults.BadRequest("Volume 'size' needs to be a positive integer"
125
                                " value. '%s' cannot be accepted." % size)
126

    
127
    # TODO: Fix volume type, validate, etc..
128
    volume_type = new_volume.get("volume_type", None)
129

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

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

    
146
    server_id = new_volume.get("server_id")
147
    if server_id is None:
148
        raise faults.BadRequest("Attribute 'server_id' is mandatory")
149

    
150
    # Create the volume
151
    volume = volumes.create(user_id=user_id, size=size, name=name,
152
                            source_volume_id=source_volume_id,
153
                            source_snapshot_id=source_snapshot_id,
154
                            source_image_id=source_image_id,
155
                            volume_type=volume_type, description=description,
156
                            metadata=metadata, server_id=server_id)
157

    
158
    # Render response
159
    data = json.dumps(dict(volume=volume_to_dict(volume, detail=False)))
160
    return HttpResponse(data, status=200)
161

    
162

    
163
@api.api_method(http_method="GET", user_required=True, logger=log)
164
def list_volumes(request, detail=False):
165
    log.debug('list_volumes detail=%s', detail)
166
    volumes = Volume.objects.filter(userid=request.user_uniq).order_by("id")
167

    
168
    volumes = utils.filter_modified_since(request, objects=volumes)
169

    
170
    volumes = [volume_to_dict(v, detail) for v in volumes]
171

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

    
175

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

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

    
183
    return HttpResponse(status=202)
184

    
185

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

    
190
    volume = util.get.volume(request.user_uniq, volume_id)
191

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

    
195

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

    
201
    volume = util.get.volume(request.user_uniq, volume_id, for_update=True)
202

    
203
    new_name = req.get("display_name")
204
    description = req.get("display_description")
205
    delete_on_termination = req.get("delete_on_termination")
206

    
207
    if new_name is None and description is None and\
208
       delete_on_termination is None:
209
        raise faults.BadRequest("Nothing to update.")
210
    else:
211
        volume = volumes.update(volume, new_name, description,
212
                                delete_on_termination)
213

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

    
217

    
218
def snapshot_to_dict(snapshot, detail=True):
219
    owner = snapshot['owner']
220
    status = snapshot['status']
221
    progress = "%s%%" % 100 if status == "ACTIVE" else 0
222

    
223
    data = {
224
        "id": snapshot["uuid"],
225
        "size": int(snapshot["size"]) >> 30,  # gigabytes
226
        "display_name": snapshot["name"],
227
        "display_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": util.snapshot_to_links(snapshot["uuid"])
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
    user_id = request.user_uniq
248

    
249
    new_snapshot = req.get("snapshot")
250
    if new_snapshot is None:
251
        raise faults.BadRequest("Missing 'snapshot' attribute.")
252

    
253
    # Get and validate 'name' parameter
254
    # TODO: auto generate name
255
    metadata = new_snapshot.get("metadata", {})
256
    if not isinstance(metadata, dict):
257
        msg = "Snapshot 'metadata' needs to be a dictionary of key-value"\
258
              " pairs. '%s' can not be accepted." % metadata
259
        raise faults.BadRequest(msg)
260

    
261
    volume_id = new_snapshot.get("volume_id", None)
262
    if volume_id is None:
263
        raise faults.BadRequest("'volume_id' attribute is missing.")
264
    volume = util.get_volume(user_id, volume_id, for_update=True,
265
                             exception=faults.BadRequest)
266

    
267
    name = new_snapshot.get("display_name", None)
268
    if name is None:
269
        name = "snapshot_volume_%s_%s" %\
270
            (volume.id, str(datetime.datetime.now()))
271
    description = new_snapshot.get("display_description", "")
272

    
273
    # TODO: What to do with force ?
274
    force = new_snapshot.get("force", False)
275
    if not isinstance(force, bool):
276
        raise faults.BadRequest("Invalid value for 'force' attribute.")
277

    
278
    snapshot = snapshots.create(user_id=user_id, volume=volume, name=name,
279
                                description=description, metadata=metadata,
280
                                force=force)
281

    
282
    # Render response
283
    data = json.dumps(dict(snapshot=snapshot_to_dict(snapshot, detail=False)))
284
    return HttpResponse(data, status=202)
285

    
286

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

    
300
    snapshots = sorted(snapshots, key=lambda x: x['id'])
301
    snapshots_dict = [snapshot_to_dict(snapshot, detail)
302
                      for snapshot in snapshots]
303

    
304
    data = json.dumps(dict(snapshots=snapshots_dict))
305

    
306
    return HttpResponse(data, status=200)
307

    
308

    
309
@api.api_method(http_method="DELETE", user_required=True, logger=log)
310
def delete_snapshot(request, snapshot_id):
311
    log.debug("delete_snapshot snapshot_id: %s", snapshot_id)
312

    
313
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
314
    snapshots.delete(snapshot)
315

    
316
    return HttpResponse(status=202)
317

    
318

    
319
@api.api_method(http_method="GET", user_required=True, logger=log)
320
def get_snapshot(request, snapshot_id):
321
    log.debug('get_snapshot snapshot_id: %s', snapshot_id)
322

    
323
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
324
    data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)})
325
    return HttpResponse(data, content_type="application/json", status=200)
326

    
327

    
328
@api.api_method(http_method="PUT", user_required=True, logger=log)
329
def update_snapshot(request, snapshot_id):
330
    req = utils.get_request_dict(request)
331
    log.debug('update_snapshot snapshot_id: %s, request: %s', snapshot_id, req)
332
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
333

    
334
    new_name = req.get("display_name")
335
    new_description = req.get("display_description")
336
    if new_name is None and new_description is None:
337
        raise faults.BadRequest("Nothing to update.")
338

    
339
    if new_name is not None:
340
        snapshot = snapshots.rename(snapshot, new_name)
341
    if new_description is not None:
342
        snapshot = snapshots.update_description(snapshot, new_description)
343

    
344
    data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)})
345
    return HttpResponse(data, content_type="application/json", status=200)