Statistics
| Branch: | Tag: | Revision:

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

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.utils import image_backend
47
from synnefo.plankton.backend import split_url, InvalidLocation
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(val)
113
        elif key.startswith(META_PREFIX):
114
            name = normalize(key[META_PREFIX_LEN:])
115
            headers[unquote(name)] = unquote(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')
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 InvalidLocation:
164
        raise faults.BadRequest("Invalid location '%s'" % location)
165

    
166
    validate_fields(params)
167

    
168
    if location:
169
        with image_backend(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 image_backend(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 image_backend(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 image_backend(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 image_backend(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 image_backend(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 image_backend(request.user_uniq) as backend:
346
        for image in backend.list_shared_images(member=member):
347
            image_id = image['id']
348
            images.append({'image_id': image_id, 'can_share': False})
349

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

    
353

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

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

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

    
367

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

372
    Described in:
373
    3.6.2. Updating an Image
374

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

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

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

    
387
    validate_fields(meta)
388

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

    
393

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

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

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

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

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

    
418

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

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

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

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