Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (11.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
39

    
40
from django.conf import settings
41
from django.http import (HttpResponse, HttpResponseNotFound,
42
                         HttpResponseBadRequest)
43

    
44
from snf_django.lib import api
45
from synnefo.plankton.utils import plankton_method
46

    
47

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

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

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

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

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

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

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

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

    
71

    
72
log = getLogger('synnefo.plankton')
73

    
74

    
75
def _create_image_response(image):
76
    response = HttpResponse()
77

    
78
    for key in DETAIL_FIELDS:
79
        if key == 'properties':
80
            for k, v in image.get('properties', {}).items():
81
                name = 'x-image-meta-property-' + k.replace('_', '-')
82
                response[name] = v
83
        else:
84
            name = 'x-image-meta-' + key.replace('_', '-')
85
            response[name] = image.get(key, '')
86

    
87
    return response
88

    
89

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

    
94
    META_PREFIX = 'HTTP_X_IMAGE_META_'
95
    META_PREFIX_LEN = len(META_PREFIX)
96
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
97
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
98

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

    
101
    for key, val in request.META.items():
102
        if key.startswith(META_PROPERTY_PREFIX):
103
            name = normalize(key[META_PROPERTY_PREFIX_LEN:])
104
            headers['properties'][unquote(name)] = unquote(val)
105
        elif key.startswith(META_PREFIX):
106
            name = normalize(key[META_PREFIX_LEN:])
107
            headers[unquote(name)] = unquote(val)
108

    
109
    is_public = headers.get('is_public', None)
110
    if is_public is not None:
111
        headers['is_public'] = True if is_public.lower() == 'true' else False
112

    
113
    if not headers['properties']:
114
        del headers['properties']
115

    
116
    return headers
117

    
118

    
119
@api.api_method(http_method="POST", user_required=True, logger=log)
120
@plankton_method
121
def add_image(request):
122
    """Add a new virtual machine image
123

124
    Described in:
125
    3.6. Adding a New Virtual Machine Image
126

127
    Implementation notes:
128
      * The implementation is very inefficient as it loads the whole image
129
        in memory.
130

131
    Limitations:
132
      * x-image-meta-id is not supported. Will always return 409 Conflict.
133

134
    Extensions:
135
      * An x-image-meta-location header can be passed with a link to file,
136
        instead of uploading the data.
137
    """
138

    
139
    params = _get_image_headers(request)
140
    log.debug('add_image %s', params)
141

    
142
    assert 'name' in params
143
    assert set(params.keys()).issubset(set(ADD_FIELDS))
144

    
145
    name = params.pop('name')
146
    location = params.pop('location', None)
147

    
148
    if location:
149
        image = request.backend.register(name, location, params)
150
    else:
151
        #f = StringIO(request.raw_post_data)
152
        #image = request.backend.put(name, f, params)
153
        return HttpResponse(status=501)     # Not Implemented
154

    
155
    if not image:
156
        return HttpResponse('Registration failed', status=500)
157

    
158
    return _create_image_response(image)
159

    
160

    
161
@api.api_method(http_method="DELETE", user_required=True, logger=log)
162
@plankton_method
163
def delete_image(request, image_id):
164
    """Delete an Image.
165

166
    This API call is not described in the Openstack Glance API.
167

168
    Implementation notes:
169
      * The implementation does not delete the Image from the storage
170
        backend. Instead it unregisters the image by removing all the
171
        metadata from the plankton metadata domain.
172

173
    """
174
    log.info("delete_image '%s'" % image_id)
175
    userid = request.user_uniq
176
    request.backend.unregister(image_id)
177
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
178
    return HttpResponse(status=204)
179

    
180

    
181
@api.api_method(http_method="PUT", user_required=True, logger=log)
182
@plankton_method
183
def add_image_member(request, image_id, member):
184
    """Add a member to an image
185

186
    Described in:
187
    3.9. Adding a Member to an Image
188

189
    Limitations:
190
      * Passing a body to enable `can_share` is not supported.
191
    """
192

    
193
    log.debug('add_image_member %s %s', image_id, member)
194
    request.backend.add_user(image_id, member)
195
    return HttpResponse(status=204)
196

    
197

    
198
@api.api_method(http_method="GET", user_required=True, logger=log)
199
@plankton_method
200
def get_image(request, image_id):
201
    """Retrieve a virtual machine image
202

203
    Described in:
204
    3.5. Retrieving a Virtual Machine Image
205

206
    Implementation notes:
207
      * The implementation is very inefficient as it loads the whole image
208
        in memory.
209
    """
210

    
211
    #image = request.backend.get_image(image_id)
212
    #if not image:
213
    #    return HttpResponseNotFound()
214
    #
215
    #response = _create_image_response(image)
216
    #data = request.backend.get_data(image)
217
    #response.content = data
218
    #response['Content-Length'] = len(data)
219
    #response['Content-Type'] = 'application/octet-stream'
220
    #response['ETag'] = image['checksum']
221
    #return response
222
    return HttpResponse(status=501)     # Not Implemented
223

    
224

    
225
@api.api_method(http_method="HEAD", user_required=True, logger=log)
226
@plankton_method
227
def get_image_meta(request, image_id):
228
    """Return detailed metadata on a specific image
229

230
    Described in:
231
    3.4. Requesting Detailed Metadata on a Specific Image
232
    """
233

    
234
    image = request.backend.get_image(image_id)
235
    if not image:
236
        return HttpResponseNotFound()
237
    return _create_image_response(image)
238

    
239

    
240
@api.api_method(http_method="GET", user_required=True, logger=log)
241
@plankton_method
242
def list_image_members(request, image_id):
243
    """List image memberships
244

245
    Described in:
246
    3.7. Requesting Image Memberships
247
    """
248

    
249
    members = [{'member_id': user, 'can_share': False}
250
               for user in request.backend.list_users(image_id)]
251
    data = json.dumps({'members': members}, indent=settings.DEBUG)
252
    return HttpResponse(data)
253

    
254

    
255
@api.api_method(http_method="GET", user_required=True, logger=log)
256
@plankton_method
257
def list_images(request, detail=False):
258
    """Return a list of available images.
259

260
    This includes images owned by the user, images shared with the user and
261
    public images.
262

263
    """
264

    
265
    def get_request_params(keys):
266
        params = {}
267
        for key in keys:
268
            val = request.GET.get(key, None)
269
            if val is not None:
270
                params[key] = val
271
        return params
272

    
273
    log.debug('list_public_images detail=%s', detail)
274

    
275
    filters = get_request_params(FILTERS)
276
    params = get_request_params(PARAMS)
277

    
278
    params.setdefault('sort_key', 'created_at')
279
    params.setdefault('sort_dir', 'desc')
280

    
281
    assert params['sort_key'] in SORT_KEY_OPTIONS
282
    assert params['sort_dir'] in SORT_DIR_OPTIONS
283

    
284
    if 'size_max' in filters:
285
        try:
286
            filters['size_max'] = int(filters['size_max'])
287
        except ValueError:
288
            return HttpResponseBadRequest('400 Bad Request')
289

    
290
    if 'size_min' in filters:
291
        try:
292
            filters['size_min'] = int(filters['size_min'])
293
        except ValueError:
294
            return HttpResponseBadRequest('400 Bad Request')
295

    
296
    images = request.backend.list(filters, params)
297

    
298
    # Remove keys that should not be returned
299
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
300
    for image in images:
301
        for key in image.keys():
302
            if key not in fields:
303
                del image[key]
304

    
305
    data = json.dumps(images, indent=settings.DEBUG)
306
    return HttpResponse(data)
307

    
308

    
309
@api.api_method(http_method="GET", user_required=True, logger=log)
310
@plankton_method
311
def list_shared_images(request, member):
312
    """Request shared images
313

314
    Described in:
315
    3.8. Requesting Shared Images
316

317
    Implementation notes:
318
      * It is not clear what this method should do. We return the IDs of
319
        the users's images that are accessible by `member`.
320
    """
321

    
322
    log.debug('list_shared_images %s', member)
323

    
324
    images = []
325
    for image in request.backend.iter_shared(member=member):
326
        image_id = image['id']
327
        images.append({'image_id': image_id, 'can_share': False})
328

    
329
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
330
    return HttpResponse(data)
331

    
332

    
333
@api.api_method(http_method="DELETE", user_required=True, logger=log)
334
@plankton_method
335
def remove_image_member(request, image_id, member):
336
    """Remove a member from an image
337

338
    Described in:
339
    3.10. Removing a Member from an Image
340
    """
341

    
342
    log.debug('remove_image_member %s %s', image_id, member)
343
    request.backend.remove_user(image_id, member)
344
    return HttpResponse(status=204)
345

    
346

    
347
@api.api_method(http_method="PUT", user_required=True, logger=log)
348
@plankton_method
349
def update_image(request, image_id):
350
    """Update an image
351

352
    Described in:
353
    3.6.2. Updating an Image
354

355
    Implementation notes:
356
      * It is not clear which metadata are allowed to be updated. We support:
357
        name, disk_format, container_format, is_public, owner, properties
358
        and status.
359
    """
360

    
361
    meta = _get_image_headers(request)
362
    log.debug('update_image %s', meta)
363

    
364
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
365

    
366
    image = request.backend.update(image_id, meta)
367
    return _create_image_response(image)
368

    
369

    
370
@api.api_method(http_method="PUT", user_required=True, logger=log)
371
@plankton_method
372
def update_image_members(request, image_id):
373
    """Replace a membership list for an image
374

375
    Described in:
376
    3.11. Replacing a Membership List for an Image
377

378
    Limitations:
379
      * can_share value is ignored
380
    """
381

    
382
    log.debug('update_image_members %s', image_id)
383
    members = []
384
    try:
385
        data = json.loads(request.raw_post_data)
386
        for member in data['memberships']:
387
            members.append(member['member_id'])
388
    except (ValueError, KeyError, TypeError):
389
        return HttpResponse(status=400)
390

    
391
    request.backend.replace_users(image_id, members)
392
    return HttpResponse(status=204)