Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / plankton / views.py @ 23808592

History | View | Annotate | Download (13.7 kB)

1
# Copyright 2011-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
import json
35

    
36
from logging import getLogger
37
from string import punctuation
38
from urllib import unquote
39

    
40
from django.conf import settings
41
from django.http import HttpResponse
42

    
43
from snf_django.lib import api
44
from snf_django.lib.api import faults
45
from synnefo.util.text import uenc
46
from synnefo.plankton.backend import PlanktonBackend
47
from synnefo.plankton.backend import split_url
48

    
49

    
50
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
51
           'size_max')
52

    
53
PARAMS = ('sort_key', 'sort_dir')
54

    
55
SORT_KEY_OPTIONS = ('id', 'name', 'status', 'size', 'disk_format',
56
                    'container_format', 'created_at', 'updated_at')
57

    
58
SORT_DIR_OPTIONS = ('asc', 'desc')
59

    
60
LIST_FIELDS = ('status', 'name', 'disk_format', 'container_format', 'size',
61
               'id')
62

    
63
DETAIL_FIELDS = ('name', 'disk_format', 'container_format', 'size', 'checksum',
64
                 'location', 'created_at', 'updated_at', 'deleted_at',
65
                 'status', 'is_public', 'owner', 'properties', 'id')
66

    
67
ADD_FIELDS = ('name', 'id', 'store', 'disk_format', 'container_format', 'size',
68
              'checksum', 'is_public', 'owner', 'properties', 'location')
69

    
70
UPDATE_FIELDS = ('name', 'disk_format', 'container_format', 'is_public',
71
                 'owner', 'properties', 'status')
72

    
73
DISK_FORMATS = ('diskdump', 'extdump', 'ntfsdump')
74

    
75
CONTAINER_FORMATS = ('aki', 'ari', 'ami', 'bare', 'ovf')
76

    
77
STORE_TYPES = ('pithos')
78

    
79

    
80
log = getLogger('synnefo.plankton')
81

    
82

    
83
def _create_image_response(image):
84
    response = HttpResponse()
85

    
86
    for key in DETAIL_FIELDS:
87
        if key == 'properties':
88
            for k, v in image.get('properties', {}).items():
89
                name = 'x-image-meta-property-' + k.replace('_', '-')
90
                response[name] = uenc(v)
91
        else:
92
            name = 'x-image-meta-' + key.replace('_', '-')
93
            response[name] = uenc(image.get(key, ''))
94

    
95
    return response
96

    
97

    
98
def _get_image_headers(request):
99
    def normalize(s):
100
        return ''.join('_' if c in punctuation else c.lower() for c in s)
101

    
102
    META_PREFIX = 'HTTP_X_IMAGE_META_'
103
    META_PREFIX_LEN = len(META_PREFIX)
104
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
105
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
106

    
107
    headers = {'properties': {}}
108

    
109
    for key, val in request.META.items():
110
        if key.startswith(META_PROPERTY_PREFIX):
111
            name = normalize(key[META_PROPERTY_PREFIX_LEN:])
112
            headers['properties'][unquote(name)] = unquote(uenc(val))
113
        elif key.startswith(META_PREFIX):
114
            name = normalize(key[META_PREFIX_LEN:])
115
            headers[unquote(name)] = unquote(uenc(val))
116

    
117
    is_public = headers.get('is_public', None)
118
    if is_public is not None:
119
        headers['is_public'] = True if is_public.lower() == 'true' else False
120

    
121
    if not headers['properties']:
122
        del headers['properties']
123

    
124
    return headers
125

    
126

    
127
@api.api_method(http_method="POST", user_required=True, logger=log)
128
def add_image(request):
129
    """Add a new virtual machine image
130

131
    Described in:
132
    3.6. Adding a New Virtual Machine Image
133

134
    Implementation notes:
135
      * The implementation is very inefficient as it loads the whole image
136
        in memory.
137

138
    Limitations:
139
      * x-image-meta-id is not supported. Will always return 409 Conflict.
140

141
    Extensions:
142
      * An x-image-meta-location header can be passed with a link to file,
143
        instead of uploading the data.
144
    """
145

    
146
    params = _get_image_headers(request)
147
    log.debug('add_image %s', params)
148

    
149
    if not set(params.keys()).issubset(set(ADD_FIELDS)):
150
        raise faults.BadRequest("Invalid parameters")
151

    
152
    name = params.pop('name', None)
153
    if name is None:
154
        raise faults.BadRequest("Image 'name' parameter is required")
155
    elif len(uenc(name)) == 0:
156
        raise faults.BadRequest("Invalid image name")
157
    location = params.pop('location', None)
158
    if location is None:
159
        raise faults.BadRequest("'location' parameter is required")
160

    
161
    try:
162
        split_url(location)
163
    except AssertionError:
164
        raise faults.BadRequest("Invalid location '%s'" % location)
165

    
166
    validate_fields(params)
167

    
168
    if location:
169
        with PlanktonBackend(request.user_uniq) as backend:
170
            image = backend.register(name, location, params)
171
    else:
172
        #f = StringIO(request.body)
173
        #image = backend.put(name, f, params)
174
        return HttpResponse(status=501)     # Not Implemented
175

    
176
    if not image:
177
        return HttpResponse('Registration failed', status=500)
178

    
179
    return _create_image_response(image)
180

    
181

    
182
@api.api_method(http_method="DELETE", user_required=True, logger=log)
183
def delete_image(request, image_id):
184
    """Delete an Image.
185

186
    This API call is not described in the Openstack Glance API.
187

188
    Implementation notes:
189
      * The implementation does not delete the Image from the storage
190
        backend. Instead it unregisters the image by removing all the
191
        metadata from the plankton metadata domain.
192

193
    """
194
    log.info("delete_image '%s'" % image_id)
195
    userid = request.user_uniq
196
    with PlanktonBackend(userid) as backend:
197
        backend.unregister(image_id)
198
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
199
    return HttpResponse(status=204)
200

    
201

    
202
@api.api_method(http_method="PUT", user_required=True, logger=log)
203
def add_image_member(request, image_id, member):
204
    """Add a member to an image
205

206
    Described in:
207
    3.9. Adding a Member to an Image
208

209
    Limitations:
210
      * Passing a body to enable `can_share` is not supported.
211
    """
212

    
213
    log.debug('add_image_member %s %s', image_id, member)
214
    with PlanktonBackend(request.user_uniq) as backend:
215
        backend.add_user(image_id, member)
216
    return HttpResponse(status=204)
217

    
218

    
219
@api.api_method(http_method="GET", user_required=True, logger=log)
220
def get_image(request, image_id):
221
    """Retrieve a virtual machine image
222

223
    Described in:
224
    3.5. Retrieving a Virtual Machine Image
225

226
    Implementation notes:
227
      * The implementation is very inefficient as it loads the whole image
228
        in memory.
229
    """
230

    
231
    #image = backend.get_image(image_id)
232
    #if not image:
233
    #    return HttpResponseNotFound()
234
    #
235
    #response = _create_image_response(image)
236
    #data = backend.get_data(image)
237
    #response.content = data
238
    #response['Content-Length'] = len(data)
239
    #response['Content-Type'] = 'application/octet-stream'
240
    #response['ETag'] = image['checksum']
241
    #return response
242
    return HttpResponse(status=501)     # Not Implemented
243

    
244

    
245
@api.api_method(http_method="HEAD", user_required=True, logger=log)
246
def get_image_meta(request, image_id):
247
    """Return detailed metadata on a specific image
248

249
    Described in:
250
    3.4. Requesting Detailed Metadata on a Specific Image
251
    """
252

    
253
    with PlanktonBackend(request.user_uniq) as backend:
254
        image = backend.get_image(image_id)
255
    return _create_image_response(image)
256

    
257

    
258
@api.api_method(http_method="GET", user_required=True, logger=log)
259
def list_image_members(request, image_id):
260
    """List image memberships
261

262
    Described in:
263
    3.7. Requesting Image Memberships
264
    """
265

    
266
    with PlanktonBackend(request.user_uniq) as backend:
267
        users = backend.list_users(image_id)
268

    
269
    members = [{'member_id': u, 'can_share': False} for u in users]
270
    data = json.dumps({'members': members}, indent=settings.DEBUG)
271
    return HttpResponse(data)
272

    
273

    
274
@api.api_method(http_method="GET", user_required=True, logger=log)
275
def list_images(request, detail=False):
276
    """Return a list of available images.
277

278
    This includes images owned by the user, images shared with the user and
279
    public images.
280

281
    """
282

    
283
    def get_request_params(keys):
284
        params = {}
285
        for key in keys:
286
            val = request.GET.get(key, None)
287
            if val is not None:
288
                params[key] = val
289
        return params
290

    
291
    log.debug('list_public_images detail=%s', detail)
292

    
293
    filters = get_request_params(FILTERS)
294
    params = get_request_params(PARAMS)
295

    
296
    params.setdefault('sort_key', 'created_at')
297
    params.setdefault('sort_dir', 'desc')
298

    
299
    if not params['sort_key'] in SORT_KEY_OPTIONS:
300
        raise faults.BadRequest("Invalid 'sort_key'")
301
    if not params['sort_dir'] in SORT_DIR_OPTIONS:
302
        raise faults.BadRequest("Invalid 'sort_dir'")
303

    
304
    if 'size_max' in filters:
305
        try:
306
            filters['size_max'] = int(filters['size_max'])
307
        except ValueError:
308
            raise faults.BadRequest("Malformed request.")
309

    
310
    if 'size_min' in filters:
311
        try:
312
            filters['size_min'] = int(filters['size_min'])
313
        except ValueError:
314
            raise faults.BadRequest("Malformed request.")
315

    
316
    with PlanktonBackend(request.user_uniq) as backend:
317
        images = backend.list_images(filters, params)
318

    
319
    # Remove keys that should not be returned
320
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
321
    for image in images:
322
        for key in image.keys():
323
            if key not in fields:
324
                del image[key]
325

    
326
    data = json.dumps(images, indent=settings.DEBUG)
327
    return HttpResponse(data)
328

    
329

    
330
@api.api_method(http_method="GET", user_required=True, logger=log)
331
def list_shared_images(request, member):
332
    """Request shared images
333

334
    Described in:
335
    3.8. Requesting Shared Images
336

337
    Implementation notes:
338
      * It is not clear what this method should do. We return the IDs of
339
        the users's images that are accessible by `member`.
340
    """
341

    
342
    log.debug('list_shared_images %s', member)
343

    
344
    images = []
345
    with PlanktonBackend(request.user_uniq) as backend:
346
        for image in backend.list_shared_images(member=member):
347
            images.append({'image_id': image["id"], 'can_share': False})
348

    
349
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
350
    return HttpResponse(data)
351

    
352

    
353
@api.api_method(http_method="DELETE", user_required=True, logger=log)
354
def remove_image_member(request, image_id, member):
355
    """Remove a member from an image
356

357
    Described in:
358
    3.10. Removing a Member from an Image
359
    """
360

    
361
    log.debug('remove_image_member %s %s', image_id, member)
362
    with PlanktonBackend(request.user_uniq) as backend:
363
        backend.remove_user(image_id, member)
364
    return HttpResponse(status=204)
365

    
366

    
367
@api.api_method(http_method="PUT", user_required=True, logger=log)
368
def update_image(request, image_id):
369
    """Update an image
370

371
    Described in:
372
    3.6.2. Updating an Image
373

374
    Implementation notes:
375
      * It is not clear which metadata are allowed to be updated. We support:
376
        name, disk_format, container_format, is_public, owner, properties
377
        and status.
378
    """
379

    
380
    meta = _get_image_headers(request)
381
    log.debug('update_image %s', meta)
382

    
383
    if not set(meta.keys()).issubset(set(UPDATE_FIELDS)):
384
        raise faults.BadRequest("Invalid metadata")
385

    
386
    validate_fields(meta)
387

    
388
    with PlanktonBackend(request.user_uniq) as backend:
389
        image = backend.update_metadata(image_id, meta)
390
    return _create_image_response(image)
391

    
392

    
393
@api.api_method(http_method="PUT", user_required=True, logger=log)
394
def update_image_members(request, image_id):
395
    """Replace a membership list for an image
396

397
    Described in:
398
    3.11. Replacing a Membership List for an Image
399

400
    Limitations:
401
      * can_share value is ignored
402
    """
403

    
404
    log.debug('update_image_members %s', image_id)
405
    members = []
406
    try:
407
        data = json.loads(request.body)
408
        for member in data['memberships']:
409
            members.append(member['member_id'])
410
    except (ValueError, KeyError, TypeError):
411
        return HttpResponse(status=400)
412

    
413
    with PlanktonBackend(request.user_uniq) as backend:
414
        backend.replace_users(image_id, members)
415
    return HttpResponse(status=204)
416

    
417

    
418
def validate_fields(params):
419
    if "id" in params:
420
        raise faults.BadRequest("Setting the image ID is not supported")
421

    
422
    if "store" in params:
423
        if params["store"] not in STORE_TYPES:
424
            raise faults.BadRequest("Invalid store type '%s'" %
425
                                    params["store"])
426

    
427
    if "disk_format" in params:
428
        if params["disk_format"] not in DISK_FORMATS:
429
            raise faults.BadRequest("Invalid disk format '%s'" %
430
                                    params['disk_format'])
431

    
432
    if "container_format" in params:
433
        if params["container_format"] not in CONTAINER_FORMATS:
434
            raise faults.BadRequest("Invalid container format '%s'" %
435
                                    params['container_format'])