Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.4 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.plankton.utils import image_backend
46
from synnefo.plankton.backend import split_url
47

    
48

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

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

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

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

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

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

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

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

    
72
PROPERTY_FIELD_PREFIX = 'property_'
73

    
74

    
75
log = getLogger('synnefo.plankton')
76

    
77

    
78
def _create_image_response(image):
79
    response = HttpResponse()
80

    
81
    for key in DETAIL_FIELDS:
82
        name = 'x-image-meta-' + key.replace('_', '-')
83
        response[name] = image.get(key, '')
84
    for key in [k for k in image.keys() if
85
                k.startswith(PROPERTY_FIELD_PREFIX)]:
86
        name = 'x-image-meta-' + key.replace('_', '-')
87
        response[name] = image.get(key, '')
88

    
89
    return response
90

    
91

    
92
def _get_image_headers(request):
93
    def normalize(s):
94
        return ''.join('_' if c in punctuation else c.lower() for c in s)
95

    
96
    META_PREFIX = 'HTTP_X_IMAGE_META_'
97
    META_PREFIX_LEN = len(META_PREFIX)
98

    
99
    headers = {'properties': {}}
100

    
101
    for key, val in request.META.items():
102
        if key.startswith(META_PREFIX):
103
            name = normalize(key[META_PREFIX_LEN:])
104
            headers[unquote(name)] = unquote(val)
105

    
106
    is_public = headers.get('is_public', None)
107
    if is_public is not None:
108
        headers['is_public'] = True if is_public.lower() == 'true' else False
109

    
110
    if not headers['properties']:
111
        del headers['properties']
112

    
113
    return headers
114

    
115

    
116
def _assert_allowed_keys(d, allowed):
117
    # Filter out property fields
118

    
119
    for k in d:
120
        if k.startswith(PROPERTY_FIELD_PREFIX):
121
            continue
122
        assert k in allowed
123

    
124

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

129
    Described in:
130
    3.6. Adding a New Virtual Machine Image
131

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

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

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

    
144
    params = _get_image_headers(request)
145
    log.debug('add_image %s', params)
146

    
147
    assert 'name' in params
148
    _assert_allowed_keys(params, ADD_FIELDS)
149

    
150
    name = params.pop('name')
151
    location = params.pop('location', None)
152
    try:
153
        split_url(location)
154
    except AssertionError:
155
        raise faults.BadRequest("Invalid location '%s'" % location)
156

    
157
    if location:
158
        with image_backend(request.user_uniq) as backend:
159
            image = backend.register(name, location, params)
160
    else:
161
        #f = StringIO(request.body)
162
        #image = backend.put(name, f, params)
163
        return HttpResponse(status=501)     # Not Implemented
164

    
165
    if not image:
166
        return HttpResponse('Registration failed', status=500)
167

    
168
    return _create_image_response(image)
169

    
170

    
171
@api.api_method(http_method="DELETE", user_required=True, logger=log)
172
def delete_image(request, image_id):
173
    """Delete an Image.
174

175
    This API call is not described in the Openstack Glance API.
176

177
    Implementation notes:
178
      * The implementation does not delete the Image from the storage
179
        backend. Instead it unregisters the image by removing all the
180
        metadata from the plankton metadata domain.
181

182
    """
183
    log.info("delete_image '%s'" % image_id)
184
    userid = request.user_uniq
185
    with image_backend(userid) as backend:
186
        backend.unregister(image_id)
187
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
188
    return HttpResponse(status=204)
189

    
190

    
191
@api.api_method(http_method="PUT", user_required=True, logger=log)
192
def add_image_member(request, image_id, member):
193
    """Add a member to an image
194

195
    Described in:
196
    3.9. Adding a Member to an Image
197

198
    Limitations:
199
      * Passing a body to enable `can_share` is not supported.
200
    """
201

    
202
    log.debug('add_image_member %s %s', image_id, member)
203
    with image_backend(request.user_uniq) as backend:
204
        backend.add_user(image_id, member)
205
    return HttpResponse(status=204)
206

    
207

    
208
@api.api_method(http_method="GET", user_required=True, logger=log)
209
def get_image(request, image_id):
210
    """Retrieve a virtual machine image
211

212
    Described in:
213
    3.5. Retrieving a Virtual Machine Image
214

215
    Implementation notes:
216
      * The implementation is very inefficient as it loads the whole image
217
        in memory.
218
    """
219

    
220
    #image = backend.get_image(image_id)
221
    #if not image:
222
    #    return HttpResponseNotFound()
223
    #
224
    #response = _create_image_response(image)
225
    #data = backend.get_data(image)
226
    #response.content = data
227
    #response['Content-Length'] = len(data)
228
    #response['Content-Type'] = 'application/octet-stream'
229
    #response['ETag'] = image['checksum']
230
    #return response
231
    return HttpResponse(status=501)     # Not Implemented
232

    
233

    
234
@api.api_method(http_method="HEAD", user_required=True, logger=log)
235
def get_image_meta(request, image_id):
236
    """Return detailed metadata on a specific image
237

238
    Described in:
239
    3.4. Requesting Detailed Metadata on a Specific Image
240
    """
241

    
242
    with image_backend(request.user_uniq) as backend:
243
        image = backend.get_image(image_id)
244
    return _create_image_response(image)
245

    
246

    
247
@api.api_method(http_method="GET", user_required=True, logger=log)
248
def list_image_members(request, image_id):
249
    """List image memberships
250

251
    Described in:
252
    3.7. Requesting Image Memberships
253
    """
254

    
255
    with image_backend(request.user_uniq) as backend:
256
        users = backend.list_users(image_id)
257

    
258
    members = [{'member_id': u, 'can_share': False} for u in users]
259
    data = json.dumps({'members': members}, indent=settings.DEBUG)
260
    return HttpResponse(data)
261

    
262

    
263
@api.api_method(http_method="GET", user_required=True, logger=log)
264
def list_images(request, detail=False):
265
    """Return a list of available images.
266

267
    This includes images owned by the user, images shared with the user and
268
    public images.
269

270
    """
271

    
272
    def get_request_params(keys):
273
        params = {}
274
        for key in keys:
275
            val = request.GET.get(key, None)
276
            if val is not None:
277
                params[key] = val
278
        return params
279

    
280
    log.debug('list_public_images detail=%s', detail)
281

    
282
    filters = get_request_params(FILTERS)
283
    params = get_request_params(PARAMS)
284

    
285
    params.setdefault('sort_key', 'created_at')
286
    params.setdefault('sort_dir', 'desc')
287

    
288
    assert params['sort_key'] in SORT_KEY_OPTIONS
289
    assert params['sort_dir'] in SORT_DIR_OPTIONS
290

    
291
    if 'size_max' in filters:
292
        try:
293
            filters['size_max'] = int(filters['size_max'])
294
        except ValueError:
295
            raise faults.BadRequest("Malformed request.")
296

    
297
    if 'size_min' in filters:
298
        try:
299
            filters['size_min'] = int(filters['size_min'])
300
        except ValueError:
301
            raise faults.BadRequest("Malformed request.")
302

    
303
    with image_backend(request.user_uniq) as backend:
304
        images = backend.list_images(filters, params)
305

    
306
    # Remove keys that should not be returned
307
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
308
    for image in images:
309
        properties = {}
310
        for key in image.keys():
311
            if key.startswith(PROPERTY_FIELD_PREFIX):
312
                properties[key.replace(PROPERTY_FIELD_PREFIX, '', 1)] =\
313
                    image.pop(key)
314
            elif key not in fields:
315
                del image[key]
316
        if detail and properties:
317
            image['properties'] = properties
318

    
319
    data = json.dumps(images, indent=settings.DEBUG)
320
    return HttpResponse(data)
321

    
322

    
323
@api.api_method(http_method="GET", user_required=True, logger=log)
324
def list_shared_images(request, member):
325
    """Request shared images
326

327
    Described in:
328
    3.8. Requesting Shared Images
329

330
    Implementation notes:
331
      * It is not clear what this method should do. We return the IDs of
332
        the users's images that are accessible by `member`.
333
    """
334

    
335
    log.debug('list_shared_images %s', member)
336

    
337
    images = []
338
    with image_backend(request.user_uniq) as backend:
339
        for image in backend.list_shared_images(member=member):
340
            image_id = image['id']
341
            images.append({'image_id': image_id, 'can_share': False})
342

    
343
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
344
    return HttpResponse(data)
345

    
346

    
347
@api.api_method(http_method="DELETE", user_required=True, logger=log)
348
def remove_image_member(request, image_id, member):
349
    """Remove a member from an image
350

351
    Described in:
352
    3.10. Removing a Member from an Image
353
    """
354

    
355
    log.debug('remove_image_member %s %s', image_id, member)
356
    with image_backend(request.user_uniq) as backend:
357
        backend.remove_user(image_id, member)
358
    return HttpResponse(status=204)
359

    
360

    
361
@api.api_method(http_method="PUT", user_required=True, logger=log)
362
def update_image(request, image_id):
363
    """Update an image
364

365
    Described in:
366
    3.6.2. Updating an Image
367

368
    Implementation notes:
369
      * It is not clear which metadata are allowed to be updated. We support:
370
        name, disk_format, container_format, is_public, owner, properties
371
        and status.
372
    """
373

    
374
    meta = _get_image_headers(request)
375
    log.debug('update_image %s', meta)
376

    
377
    _assert_allowed_keys(meta, UPDATE_FIELDS)
378

    
379
    with image_backend(request.user_uniq) as backend:
380
        image = backend.update_metadata(image_id, meta)
381
    return _create_image_response(image)
382

    
383

    
384
@api.api_method(http_method="PUT", user_required=True, logger=log)
385
def update_image_members(request, image_id):
386
    """Replace a membership list for an image
387

388
    Described in:
389
    3.11. Replacing a Membership List for an Image
390

391
    Limitations:
392
      * can_share value is ignored
393
    """
394

    
395
    log.debug('update_image_members %s', image_id)
396
    members = []
397
    try:
398
        data = json.loads(request.body)
399
        for member in data['memberships']:
400
            members.append(member['member_id'])
401
    except (ValueError, KeyError, TypeError):
402
        return HttpResponse(status=400)
403

    
404
    with image_backend(request.user_uniq) as backend:
405
        backend.replace_users(image_id, members)
406
    return HttpResponse(status=204)