Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.4 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
        "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
    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("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("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)
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)
175

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

    
178
    data = json.dumps({'volumes': volumes})
179
    return HttpResponse(data, content_type="application/json", status=200)
180

    
181

    
182
@api.api_method(http_method="DELETE", user_required=True, logger=log)
183
def delete_volume(request, volume_id):
184
    log.debug("delete_volume volume_id: %s", volume_id)
185

    
186
    volume = util.get.volume(request.user_uniq, volume_id, for_update=True)
187
    volumes.delete(volume)
188

    
189
    return HttpResponse(status=202)
190

    
191

    
192
@api.api_method(http_method="GET", user_required=True, logger=log)
193
def get_volume(request, volume_id):
194
    log.debug('get_volume volume_id: %s', volume_id)
195

    
196
    volume = util.get.volume(request.user_uniq, volume_id)
197

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

    
201

    
202
@api.api_method(http_method="PUT", user_required=True, logger=log)
203
def update_volume(request, volume_id):
204
    req = utils.get_request_dict(request)
205
    log.debug('update_volume volume_id: %s, request: %s', volume_id, req)
206

    
207
    volume = util.get.volume(request.user_uniq, volume_id, for_update=True)
208

    
209
    new_name = req.get("name")
210
    description = req.get("description")
211

    
212
    if new_name is None and description is None:
213
        raise faults.BadRequest("Nothing to update.")
214

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

    
222

    
223
def snapshot_to_dict(snapshot, detail=True):
224
    owner = snapshot['owner']
225
    status = snapshot['status']
226
    progress = "%s%%" % 100 if status == "ACTIVE" else 0
227

    
228
    data = {
229
        "id": snapshot["uuid"],
230
        "size": int(snapshot["size"]) >> 30,  # gigabytes
231
        "name": snapshot["name"],
232
        "description": snapshot["description"],
233
        "status": status,
234
        "user_id": owner,
235
        "tenant_id": owner,
236
        "os-extended-snapshot-attribute:progress": progress,
237
        #"os-extended-snapshot-attribute:project_id": project,
238
        "created_at": utils.isoformat(date_parse(snapshot["created_at"])),
239
        "metadata": snapshot.get("metadata", {}),
240
        "volume_id": snapshot.get("volume_id"),
241
        "links": "",  # TODO fix links
242
    }
243
    return data
244

    
245

    
246
@api.api_method(http_method="POST", user_required=True, logger=log)
247
def create_snapshot(request):
248
    """Create a new Snapshot."""
249

    
250
    req = utils.get_request_dict(request)
251
    log.debug("create_snapshot %s", req)
252
    user_id = request.user_uniq
253

    
254
    new_snapshot = req.get("snapshot")
255
    if new_snapshot is None:
256
        raise faults.BadRequest("Missing 'snapshot' attribute.")
257

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

    
266
    volume_id = new_snapshot.get("volume_id", None)
267
    if volume_id is None:
268
        raise faults.BadRequest("'volume_id' attribute is missing.")
269
    volume = util.get_volume(user_id, volume_id, for_update=True,
270
                             exception=faults.BadRequest)
271

    
272
    name = new_snapshot.get("name", None)
273
    if name is None:
274
        name = "snapshot_volume_%s_%s" %\
275
            (volume.id, str(datetime.datetime.now()))
276
    description = new_snapshot.get("description", "")
277

    
278
    # TODO: What to do with force ?
279
    force = new_snapshot.get("force", False)
280
    if not isinstance(force, bool):
281
        raise faults.BadRequest("Invalid value for 'force' attribute.")
282

    
283
    snapshot = snapshots.create(user_id=user_id, volume=volume, name=name,
284
                                description=description, metadata=metadata,
285
                                force=force)
286

    
287
    # Render response
288
    data = json.dumps(dict(snapshot=snapshot_to_dict(snapshot, detail=False)))
289
    return HttpResponse(data, status=202)
290

    
291

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

    
305
    snapshots = sorted(snapshots, key=lambda x: x['id'])
306
    snapshots_dict = [snapshot_to_dict(snapshot, detail)
307
                      for snapshot in snapshots]
308

    
309
    data = json.dumps(dict(snapshots=snapshots_dict))
310

    
311
    return HttpResponse(data, status=200)
312

    
313

    
314
@api.api_method(http_method="DELETE", user_required=True, logger=log)
315
def delete_snapshot(request, snapshot_id):
316
    log.debug("delete_snapshot snapshot_id: %s", snapshot_id)
317

    
318
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
319
    snapshots.delete(snapshot)
320

    
321
    return HttpResponse(status=202)
322

    
323

    
324
@api.api_method(http_method="GET", user_required=True, logger=log)
325
def get_snapshot(request, snapshot_id):
326
    log.debug('get_snapshot snapshot_id: %s', snapshot_id)
327

    
328
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
329
    data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)})
330
    return HttpResponse(data, content_type="application/json", status=200)
331

    
332

    
333
@api.api_method(http_method="PUT", user_required=True, logger=log)
334
def update_snapshot(request, snapshot_id):
335
    req = utils.get_request_dict(request)
336
    log.debug('update_snapshot snapshot_id: %s, request: %s', snapshot_id, req)
337
    snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
338
    # TODO
339
    #snapshot.name = req.get("name", snapshot.name)
340
    #snapshot.description = req.get("description", snapshot.description)
341
    #snapshot.save()
342
    data = json.dumps({'snapshot': snapshot_to_dict(snapshot, detail=True)})
343
    return HttpResponse(data, content_type="application/json", status=200)