Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.9 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, quote
39

    
40
from django.conf import settings
41
from django.http import HttpResponse
42
from django.utils.encoding import (smart_unicode, smart_str,
43
                                   DjangoUnicodeDecodeError)
44

    
45
from snf_django.lib import api
46
from snf_django.lib.api import faults
47
from synnefo.plankton.utils import image_backend
48
from synnefo.plankton.backend import split_url, InvalidLocation
49

    
50

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

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

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

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

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

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

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

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

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

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

    
78
STORE_TYPES = ('pithos')
79

    
80

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

    
83

    
84
def _create_image_response(image):
85
    """Encode the image parameters to HTTP Response Headers.
86

87
    Headers are encoded to UTF-8 then quoted.
88

89
    """
90
    response = HttpResponse()
91
    encode_header = lambda x: quote(smart_str(x, encoding='utf-8'))
92

    
93
    for key in DETAIL_FIELDS:
94
        if key == 'properties':
95
            for pkey, pval in image.get('properties', {}).items():
96
                pkey = 'http-x-image-meta-property-' + pkey.replace('_', '-')
97
                pkey = quote(smart_str(pkey, encoding='utf-8'))
98
                pval = quote(smart_str(pval, encoding='utf-8'))
99
                response[encode_header(pkey)] = encode_header(pval)
100
        else:
101
            key = 'http-x-image-meta-' + key.replace('_', '-')
102
            val = image.get('key', '')
103
            response[encode_header(key)] = encode_header(val)
104

    
105
    return response
106

    
107

    
108
def headers_to_image_params(request):
109
    """Decode the HTTP request headers to the acceptable image parameters.
110

111
    Get image properties from the corresponding headers of the HTTP request.
112
    Image headers are unquoted and then decoded to Unicode using UTF-8
113
    encoding. Image headers keys are lowered and all punctation characters
114
    are replaced with underscore.
115

116
    """
117

    
118
    def normalize(s):
119
        return ''.join('_' if c in punctuation else c.lower() for c in s)
120

    
121
    META_PREFIX = 'HTTP_X_IMAGE_META_'
122
    META_PREFIX_LEN = len(META_PREFIX)
123
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
124
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
125

    
126
    decode_header = lambda x: smart_unicode(unquote(x), encoding="utf-8")
127
    is_img_property = lambda x: x.startswith(META_PROPERTY_PREFIX)
128
    is_img_param = lambda x: x.startswith(META_PREFIX)
129

    
130
    params = {}
131
    properties = {}
132
    try:
133
        for key, val in request.META.items():
134
            if isinstance(val, basestring):
135
                val = decode_header(val)
136
            if is_img_property(key):
137
                key = decode_header(key[META_PROPERTY_PREFIX_LEN:])
138
                properties[normalize(key)] = val
139
            elif is_img_param(key):
140
                key = decode_header(key[META_PREFIX_LEN:])
141
                params[normalize(key)] = val
142
    except DjangoUnicodeDecodeError:
143
        raise faults.BadRequest("Could not decode request as UTF-8 string")
144

    
145
    if properties:
146
        params['properties'] = properties
147

    
148
    return params
149

    
150

    
151
@api.api_method(http_method="POST", user_required=True, logger=log)
152
def add_image(request):
153
    """Add a new virtual machine image
154

155
    Described in:
156
    3.6. Adding a New Virtual Machine Image
157

158
    Implementation notes:
159
      * The implementation is very inefficient as it loads the whole image
160
        in memory.
161

162
    Limitations:
163
      * x-image-meta-id is not supported. Will always return 409 Conflict.
164

165
    Extensions:
166
      * An x-image-meta-location header can be passed with a link to file,
167
        instead of uploading the data.
168
    """
169

    
170
    params = headers_to_image_params(request)
171
    log.debug('add_image %s', params)
172

    
173
    if not set(params.keys()).issubset(set(ADD_FIELDS)):
174
        raise faults.BadRequest("Invalid parameters")
175

    
176
    name = params.pop('name')
177
    if name is None:
178
        raise faults.BadRequest("Image 'name' parameter is required")
179
    elif len(smart_unicode(name, encoding="utf-8")) == 0:
180
        raise faults.BadRequest("Invalid image name")
181
    location = params.pop('location', None)
182
    if location is None:
183
        raise faults.BadRequest("'location' parameter is required")
184

    
185
    try:
186
        split_url(location)
187
    except InvalidLocation:
188
        raise faults.BadRequest("Invalid location '%s'" % location)
189

    
190
    validate_fields(params)
191

    
192
    if location:
193
        with image_backend(request.user_uniq) as backend:
194
            image = backend.register(name, location, params)
195
    else:
196
        #f = StringIO(request.body)
197
        #image = backend.put(name, f, params)
198
        return HttpResponse(status=501)     # Not Implemented
199

    
200
    if not image:
201
        return HttpResponse('Registration failed', status=500)
202

    
203
    return _create_image_response(image)
204

    
205

    
206
@api.api_method(http_method="DELETE", user_required=True, logger=log)
207
def delete_image(request, image_id):
208
    """Delete an Image.
209

210
    This API call is not described in the Openstack Glance API.
211

212
    Implementation notes:
213
      * The implementation does not delete the Image from the storage
214
        backend. Instead it unregisters the image by removing all the
215
        metadata from the plankton metadata domain.
216

217
    """
218
    log.info("delete_image '%s'" % image_id)
219
    userid = request.user_uniq
220
    with image_backend(userid) as backend:
221
        backend.unregister(image_id)
222
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
223
    return HttpResponse(status=204)
224

    
225

    
226
@api.api_method(http_method="PUT", user_required=True, logger=log)
227
def add_image_member(request, image_id, member):
228
    """Add a member to an image
229

230
    Described in:
231
    3.9. Adding a Member to an Image
232

233
    Limitations:
234
      * Passing a body to enable `can_share` is not supported.
235
    """
236

    
237
    log.debug('add_image_member %s %s', image_id, member)
238
    with image_backend(request.user_uniq) as backend:
239
        backend.add_user(image_id, member)
240
    return HttpResponse(status=204)
241

    
242

    
243
@api.api_method(http_method="GET", user_required=True, logger=log)
244
def get_image(request, image_id):
245
    """Retrieve a virtual machine image
246

247
    Described in:
248
    3.5. Retrieving a Virtual Machine Image
249

250
    Implementation notes:
251
      * The implementation is very inefficient as it loads the whole image
252
        in memory.
253
    """
254

    
255
    #image = backend.get_image(image_id)
256
    #if not image:
257
    #    return HttpResponseNotFound()
258
    #
259
    #response = _create_image_response(image)
260
    #data = backend.get_data(image)
261
    #response.content = data
262
    #response['Content-Length'] = len(data)
263
    #response['Content-Type'] = 'application/octet-stream'
264
    #response['ETag'] = image['checksum']
265
    #return response
266
    return HttpResponse(status=501)     # Not Implemented
267

    
268

    
269
@api.api_method(http_method="HEAD", user_required=True, logger=log)
270
def get_image_meta(request, image_id):
271
    """Return detailed metadata on a specific image
272

273
    Described in:
274
    3.4. Requesting Detailed Metadata on a Specific Image
275
    """
276

    
277
    with image_backend(request.user_uniq) as backend:
278
        image = backend.get_image(image_id)
279
    return _create_image_response(image)
280

    
281

    
282
@api.api_method(http_method="GET", user_required=True, logger=log)
283
def list_image_members(request, image_id):
284
    """List image memberships
285

286
    Described in:
287
    3.7. Requesting Image Memberships
288
    """
289

    
290
    with image_backend(request.user_uniq) as backend:
291
        users = backend.list_users(image_id)
292

    
293
    members = [{'member_id': u, 'can_share': False} for u in users]
294
    data = json.dumps({'members': members}, indent=settings.DEBUG)
295
    return HttpResponse(data)
296

    
297

    
298
@api.api_method(http_method="GET", user_required=True, logger=log)
299
def list_images(request, detail=False):
300
    """Return a list of available images.
301

302
    This includes images owned by the user, images shared with the user and
303
    public images.
304

305
    """
306

    
307
    def get_request_params(keys):
308
        params = {}
309
        for key in keys:
310
            val = request.GET.get(key, None)
311
            if val is not None:
312
                params[key] = val
313
        return params
314

    
315
    log.debug('list_public_images detail=%s', detail)
316

    
317
    filters = get_request_params(FILTERS)
318
    params = get_request_params(PARAMS)
319

    
320
    params.setdefault('sort_key', 'created_at')
321
    params.setdefault('sort_dir', 'desc')
322

    
323
    if not params['sort_key'] in SORT_KEY_OPTIONS:
324
        raise faults.BadRequest("Invalid 'sort_key'")
325
    if not params['sort_dir'] in SORT_DIR_OPTIONS:
326
        raise faults.BadRequest("Invalid 'sort_dir'")
327

    
328
    if 'size_max' in filters:
329
        try:
330
            filters['size_max'] = int(filters['size_max'])
331
        except ValueError:
332
            raise faults.BadRequest("Malformed request.")
333

    
334
    if 'size_min' in filters:
335
        try:
336
            filters['size_min'] = int(filters['size_min'])
337
        except ValueError:
338
            raise faults.BadRequest("Malformed request.")
339

    
340
    with image_backend(request.user_uniq) as backend:
341
        images = backend.list_images(filters, params)
342

    
343
    # Remove keys that should not be returned
344
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
345
    for image in images:
346
        for key in image.keys():
347
            if key not in fields:
348
                del image[key]
349

    
350
    data = json.dumps(images, indent=settings.DEBUG)
351
    return HttpResponse(data)
352

    
353

    
354
@api.api_method(http_method="GET", user_required=True, logger=log)
355
def list_shared_images(request, member):
356
    """Request shared images
357

358
    Described in:
359
    3.8. Requesting Shared Images
360

361
    Implementation notes:
362
      * It is not clear what this method should do. We return the IDs of
363
        the users's images that are accessible by `member`.
364
    """
365

    
366
    log.debug('list_shared_images %s', member)
367

    
368
    images = []
369
    with image_backend(request.user_uniq) as backend:
370
        for image in backend.list_shared_images(member=member):
371
            image_id = image['id']
372
            images.append({'image_id': image_id, 'can_share': False})
373

    
374
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
375
    return HttpResponse(data)
376

    
377

    
378
@api.api_method(http_method="DELETE", user_required=True, logger=log)
379
def remove_image_member(request, image_id, member):
380
    """Remove a member from an image
381

382
    Described in:
383
    3.10. Removing a Member from an Image
384
    """
385

    
386
    log.debug('remove_image_member %s %s', image_id, member)
387
    with image_backend(request.user_uniq) as backend:
388
        backend.remove_user(image_id, member)
389
    return HttpResponse(status=204)
390

    
391

    
392
@api.api_method(http_method="PUT", user_required=True, logger=log)
393
def update_image(request, image_id):
394
    """Update an image
395

396
    Described in:
397
    3.6.2. Updating an Image
398

399
    Implementation notes:
400
      * It is not clear which metadata are allowed to be updated. We support:
401
        name, disk_format, container_format, is_public, owner, properties
402
        and status.
403
    """
404

    
405
    meta = headers_to_image_params(request)
406
    log.debug('update_image %s', meta)
407

    
408
    if not set(meta.keys()).issubset(set(UPDATE_FIELDS)):
409
        raise faults.BadRequest("Invalid metadata")
410

    
411
    validate_fields(meta)
412

    
413
    with image_backend(request.user_uniq) as backend:
414
        image = backend.update_metadata(image_id, meta)
415
    return _create_image_response(image)
416

    
417

    
418
@api.api_method(http_method="PUT", user_required=True, logger=log)
419
def update_image_members(request, image_id):
420
    """Replace a membership list for an image
421

422
    Described in:
423
    3.11. Replacing a Membership List for an Image
424

425
    Limitations:
426
      * can_share value is ignored
427
    """
428

    
429
    log.debug('update_image_members %s', image_id)
430
    data = api.utils.get_json_body(request)
431
    members = []
432

    
433
    memberships = api.utils.get_attribute(data, "memberships", attr_type=list)
434
    for member in memberships:
435
        if not isinstance(member, dict):
436
            raise faults.BadRequest("Invalid 'memberships' field")
437
        member = api.utils.get_attribute(member, "member_id")
438
        members.append(member)
439

    
440
    with image_backend(request.user_uniq) as backend:
441
        backend.replace_users(image_id, members)
442
    return HttpResponse(status=204)
443

    
444

    
445
def validate_fields(params):
446
    if "id" in params:
447
        raise faults.BadRequest("Setting the image ID is not supported")
448

    
449
    if "store" in params:
450
        if params["store"] not in STORE_TYPES:
451
            raise faults.BadRequest("Invalid store type '%s'" %
452
                                    params["store"])
453

    
454
    if "disk_format" in params:
455
        if params["disk_format"] not in DISK_FORMATS:
456
            raise faults.BadRequest("Invalid disk format '%s'" %
457
                                    params['disk_format'])
458

    
459
    if "container_format" in params:
460
        if params["container_format"] not in CONTAINER_FORMATS:
461
            raise faults.BadRequest("Invalid container format '%s'" %
462
                                    params['container_format'])