Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.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
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', "is_snapshot")
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
    def normalize(s):
80
        return ''.join('_' if c in punctuation else c.lower() for c in s)\
81
                .replace("\n", "_")
82

    
83
    for key in DETAIL_FIELDS:
84
        if key == 'properties':
85
            for k, v in image.get('properties', {}).items():
86
                name = 'x-image-meta-property-' + k.replace('_', '-')
87
                response[name] = normalize(str(v))
88
        else:
89
            name = 'x-image-meta-' + key.replace('_', '-')
90
            response[name] = normalize(str(image.get(key, '')))
91

    
92
    return response
93

    
94

    
95
def _get_image_headers(request):
96
    def normalize(s):
97
        return ''.join('_' if c in punctuation else c.lower() for c in s)
98

    
99
    META_PREFIX = 'HTTP_X_IMAGE_META_'
100
    META_PREFIX_LEN = len(META_PREFIX)
101
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
102
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
103

    
104
    headers = {'properties': {}}
105

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

    
114
    is_public = headers.get('is_public', None)
115
    if is_public is not None:
116
        headers['is_public'] = True if is_public.lower() == 'true' else False
117

    
118
    if not headers['properties']:
119
        del headers['properties']
120

    
121
    return headers
122

    
123

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

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

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

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

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

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

    
146
    if not set(params.keys()).issubset(set(ADD_FIELDS)):
147
        raise faults.BadRequest("Invalid parameters")
148

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

    
158
    try:
159
        split_url(location)
160
    except InvalidLocation:
161
        raise faults.BadRequest("Invalid location '%s'" % location)
162

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

    
171
    if not image:
172
        return HttpResponse('Registration failed', status=500)
173

    
174
    return _create_image_response(image)
175

    
176

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

181
    This API call is not described in the Openstack Glance API.
182

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

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

    
196

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

201
    Described in:
202
    3.9. Adding a Member to an Image
203

204
    Limitations:
205
      * Passing a body to enable `can_share` is not supported.
206
    """
207

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

    
213

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

218
    Described in:
219
    3.5. Retrieving a Virtual Machine Image
220

221
    Implementation notes:
222
      * The implementation is very inefficient as it loads the whole image
223
        in memory.
224
    """
225

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

    
239

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

244
    Described in:
245
    3.4. Requesting Detailed Metadata on a Specific Image
246
    """
247

    
248
    with image_backend(request.user_uniq) as backend:
249
        image = backend.get_image(image_id)
250
    return _create_image_response(image)
251

    
252

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

257
    Described in:
258
    3.7. Requesting Image Memberships
259
    """
260

    
261
    with image_backend(request.user_uniq) as backend:
262
        users = backend.list_users(image_id)
263

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

    
268

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

273
    This includes images owned by the user, images shared with the user and
274
    public images.
275

276
    """
277

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

    
286
    log.debug('list_public_images detail=%s', detail)
287

    
288
    filters = get_request_params(FILTERS)
289
    params = get_request_params(PARAMS)
290

    
291
    params.setdefault('sort_key', 'created_at')
292
    params.setdefault('sort_dir', 'desc')
293

    
294
    if not params['sort_key'] in SORT_KEY_OPTIONS:
295
        raise faults.BadRequest("Invalid 'sort_key'")
296
    if not params['sort_dir'] in SORT_DIR_OPTIONS:
297
        raise faults.BadRequest("Invalid 'sort_dir'")
298

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

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

    
311
    with image_backend(request.user_uniq) as backend:
312
        images = backend.list_images(filters, params)
313

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

    
321
    data = json.dumps(images, indent=settings.DEBUG)
322
    return HttpResponse(data)
323

    
324

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

329
    Described in:
330
    3.8. Requesting Shared Images
331

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

    
337
    log.debug('list_shared_images %s', member)
338

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

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

    
348

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

353
    Described in:
354
    3.10. Removing a Member from an Image
355
    """
356

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

    
362

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

367
    Described in:
368
    3.6.2. Updating an Image
369

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

    
376
    meta = _get_image_headers(request)
377
    log.debug('update_image %s', meta)
378

    
379
    if not set(meta.keys()).issubset(set(UPDATE_FIELDS)):
380
        raise faults.BadRequest("Invalid metadata")
381

    
382
    with image_backend(request.user_uniq) as backend:
383
        image = backend.update_metadata(image_id, meta)
384
    return _create_image_response(image)
385

    
386

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

391
    Described in:
392
    3.11. Replacing a Membership List for an Image
393

394
    Limitations:
395
      * can_share value is ignored
396
    """
397

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

    
407
    with image_backend(request.user_uniq) as backend:
408
        backend.replace_users(image_id, members)
409
    return HttpResponse(status=204)