Statistics
| Branch: | Tag: | Revision:

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

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

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

    
76

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

    
80
    for key in DETAIL_FIELDS:
81
        if key == 'properties':
82
            for k, v in image.get('properties', {}).items():
83
                name = 'x-image-meta-property-' + k.replace('_', '-')
84
                response[name] = v
85
        else:
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
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
99
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
100

    
101
    headers = {'properties': {}}
102

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

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

    
115
    if not headers['properties']:
116
        del headers['properties']
117

    
118
    return headers
119

    
120

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

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

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

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

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

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

    
143
    if not set(params.keys()).issubset(set(ADD_FIELDS)):
144
        raise faults.BadRequest("Invalid parameters")
145

    
146
    name = params.pop('name')
147
    if name is None:
148
        raise faults.BadRequest("Image 'name' parameter is required")
149
    elif len(uenc(name)) == 0:
150
        raise faults.BadRequest("Invalid image name")
151
    location = params.pop('location', None)
152
    if location is None:
153
        raise faults.BadRequest("'location' parameter is required")
154

    
155
    try:
156
        split_url(location)
157
    except InvalidLocation:
158
        raise faults.BadRequest("Invalid location '%s'" % location)
159

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

    
168
    if not image:
169
        return HttpResponse('Registration failed', status=500)
170

    
171
    return _create_image_response(image)
172

    
173

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

178
    This API call is not described in the Openstack Glance API.
179

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

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

    
193

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

198
    Described in:
199
    3.9. Adding a Member to an Image
200

201
    Limitations:
202
      * Passing a body to enable `can_share` is not supported.
203
    """
204

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

    
210

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

215
    Described in:
216
    3.5. Retrieving a Virtual Machine Image
217

218
    Implementation notes:
219
      * The implementation is very inefficient as it loads the whole image
220
        in memory.
221
    """
222

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

    
236

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

241
    Described in:
242
    3.4. Requesting Detailed Metadata on a Specific Image
243
    """
244

    
245
    with image_backend(request.user_uniq) as backend:
246
        image = backend.get_image(image_id)
247
    return _create_image_response(image)
248

    
249

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

254
    Described in:
255
    3.7. Requesting Image Memberships
256
    """
257

    
258
    with image_backend(request.user_uniq) as backend:
259
        users = backend.list_users(image_id)
260

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

    
265

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

270
    This includes images owned by the user, images shared with the user and
271
    public images.
272

273
    """
274

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

    
283
    log.debug('list_public_images detail=%s', detail)
284

    
285
    filters = get_request_params(FILTERS)
286
    params = get_request_params(PARAMS)
287

    
288
    params.setdefault('sort_key', 'created_at')
289
    params.setdefault('sort_dir', 'desc')
290

    
291
    if not params['sort_key'] in SORT_KEY_OPTIONS:
292
        raise faults.BadRequest("Invalid 'sort_key'")
293
    if not params['sort_dir'] in SORT_DIR_OPTIONS:
294
        raise faults.BadRequest("Invalid 'sort_dir'")
295

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

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

    
308
    with image_backend(request.user_uniq) as backend:
309
        images = backend.list_images(filters, params)
310

    
311
    # Remove keys that should not be returned
312
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
313
    for image in images:
314
        for key in image.keys():
315
            if key not in fields:
316
                del image[key]
317

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

    
321

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

326
    Described in:
327
    3.8. Requesting Shared Images
328

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

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

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

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

    
345

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

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

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

    
359

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

364
    Described in:
365
    3.6.2. Updating an Image
366

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

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

    
376
    if not set(meta.keys()).issubset(set(UPDATE_FIELDS)):
377
        raise faults.BadRequest("Invalid metadata")
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)