Statistics
| Branch: | Tag: | Revision:

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

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
        "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

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

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

    
216

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

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

    
239

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

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

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

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

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

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

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

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

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

    
285

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

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

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

    
305
    return HttpResponse(data, status=200)
306

    
307

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

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

    
315
    return HttpResponse(status=202)
316

    
317

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

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

    
326

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