Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.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.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
                 "is_snapshot")
67

    
68
ADD_FIELDS = ('name', 'id', 'store', 'disk_format', 'container_format', 'size',
69
              'checksum', 'is_public', 'owner', 'properties', 'location')
70

    
71
UPDATE_FIELDS = ('name', 'disk_format', 'container_format', 'is_public',
72
                 'owner', 'properties', 'status')
73

    
74

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

    
77

    
78
API_STATUS_FROM_IMAGE_STATUS = {
79
    "CREATING": "SAVING",
80
    "AVAILABLE": "ACTIVE",
81
    "DELETED": "DELETED"}
82

    
83

    
84
def _create_image_response(image):
85
    response = HttpResponse()
86

    
87
    def normalize(s):
88
        return ''.join('_' if c in punctuation else c.lower() for c in s)\
89
                 .replace("\n", "_")
90

    
91
    for key in DETAIL_FIELDS:
92
        if key == 'properties':
93
            for k, v in image.get('properties', {}).items():
94
                name = 'x-image-meta-property-' + k.replace('_', '-')
95
                response[name] = normalize(str(v))
96
        else:
97
            if key == "status":
98
                img_status = image.get(key, "").upper()
99
                status = API_STATUS_FROM_IMAGE_STATUS.get(img_status,
100
                                                          "UNKNOWN")
101
                response["x-image-meta-status"] = status
102
            else:
103
                name = 'x-image-meta-' + key.replace('_', '-')
104
                response[name] = normalize(str(image.get(key, '')))
105

    
106
    return response
107

    
108

    
109
def _get_image_headers(request):
110
    def normalize(s):
111
        return ''.join('_' if c in punctuation else c.lower() for c in s)
112

    
113
    META_PREFIX = 'HTTP_X_IMAGE_META_'
114
    META_PREFIX_LEN = len(META_PREFIX)
115
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
116
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
117

    
118
    headers = {'properties': {}}
119

    
120
    for key, val in request.META.items():
121
        if key.startswith(META_PROPERTY_PREFIX):
122
            name = normalize(key[META_PROPERTY_PREFIX_LEN:])
123
            headers['properties'][unquote(name)] = unquote(val)
124
        elif key.startswith(META_PREFIX):
125
            name = normalize(key[META_PREFIX_LEN:])
126
            headers[unquote(name)] = unquote(val)
127

    
128
    is_public = headers.get('is_public', None)
129
    if is_public is not None:
130
        headers['is_public'] = True if is_public.lower() == 'true' else False
131

    
132
    if not headers['properties']:
133
        del headers['properties']
134

    
135
    return headers
136

    
137

    
138
@api.api_method(http_method="POST", user_required=True, logger=log)
139
def add_image(request):
140
    """Add a new virtual machine image
141

142
    Described in:
143
    3.6. Adding a New Virtual Machine Image
144

145
    Implementation notes:
146
      * The implementation is very inefficient as it loads the whole image
147
        in memory.
148

149
    Limitations:
150
      * x-image-meta-id is not supported. Will always return 409 Conflict.
151

152
    Extensions:
153
      * An x-image-meta-location header can be passed with a link to file,
154
        instead of uploading the data.
155
    """
156

    
157
    params = _get_image_headers(request)
158
    log.debug('add_image %s', params)
159

    
160
    if not set(params.keys()).issubset(set(ADD_FIELDS)):
161
        raise faults.BadRequest("Invalid parameters")
162

    
163
    name = params.pop('name')
164
    if name is None:
165
        raise faults.BadRequest("Image 'name' parameter is required")
166
    elif len(uenc(name)) == 0:
167
        raise faults.BadRequest("Invalid image name")
168
    location = params.pop('location', None)
169
    if location is None:
170
        raise faults.BadRequest("'location' parameter is required")
171

    
172
    try:
173
        split_url(location)
174
    except InvalidLocation:
175
        raise faults.BadRequest("Invalid location '%s'" % location)
176

    
177
    if location:
178
        with image_backend(request.user_uniq) as backend:
179
            image = backend.register(name, location, params)
180
    else:
181
        #f = StringIO(request.body)
182
        #image = backend.put(name, f, params)
183
        return HttpResponse(status=501)     # Not Implemented
184

    
185
    if not image:
186
        return HttpResponse('Registration failed', status=500)
187

    
188
    return _create_image_response(image)
189

    
190

    
191
@api.api_method(http_method="DELETE", user_required=True, logger=log)
192
def delete_image(request, image_id):
193
    """Delete an Image.
194

195
    This API call is not described in the Openstack Glance API.
196

197
    Implementation notes:
198
      * The implementation does not delete the Image from the storage
199
        backend. Instead it unregisters the image by removing all the
200
        metadata from the plankton metadata domain.
201

202
    """
203
    log.info("delete_image '%s'" % image_id)
204
    userid = request.user_uniq
205
    with image_backend(userid) as backend:
206
        backend.unregister(image_id)
207
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
208
    return HttpResponse(status=204)
209

    
210

    
211
@api.api_method(http_method="PUT", user_required=True, logger=log)
212
def add_image_member(request, image_id, member):
213
    """Add a member to an image
214

215
    Described in:
216
    3.9. Adding a Member to an Image
217

218
    Limitations:
219
      * Passing a body to enable `can_share` is not supported.
220
    """
221

    
222
    log.debug('add_image_member %s %s', image_id, member)
223
    with image_backend(request.user_uniq) as backend:
224
        backend.add_user(image_id, member)
225
    return HttpResponse(status=204)
226

    
227

    
228
@api.api_method(http_method="GET", user_required=True, logger=log)
229
def get_image(request, image_id):
230
    """Retrieve a virtual machine image
231

232
    Described in:
233
    3.5. Retrieving a Virtual Machine Image
234

235
    Implementation notes:
236
      * The implementation is very inefficient as it loads the whole image
237
        in memory.
238
    """
239

    
240
    #image = backend.get_image(image_id)
241
    #if not image:
242
    #    return HttpResponseNotFound()
243
    #
244
    #response = _create_image_response(image)
245
    #data = backend.get_data(image)
246
    #response.content = data
247
    #response['Content-Length'] = len(data)
248
    #response['Content-Type'] = 'application/octet-stream'
249
    #response['ETag'] = image['checksum']
250
    #return response
251
    return HttpResponse(status=501)     # Not Implemented
252

    
253

    
254
@api.api_method(http_method="HEAD", user_required=True, logger=log)
255
def get_image_meta(request, image_id):
256
    """Return detailed metadata on a specific image
257

258
    Described in:
259
    3.4. Requesting Detailed Metadata on a Specific Image
260
    """
261

    
262
    with image_backend(request.user_uniq) as backend:
263
        image = backend.get_image(image_id)
264
    return _create_image_response(image)
265

    
266

    
267
@api.api_method(http_method="GET", user_required=True, logger=log)
268
def list_image_members(request, image_id):
269
    """List image memberships
270

271
    Described in:
272
    3.7. Requesting Image Memberships
273
    """
274

    
275
    with image_backend(request.user_uniq) as backend:
276
        users = backend.list_users(image_id)
277

    
278
    members = [{'member_id': u, 'can_share': False} for u in users]
279
    data = json.dumps({'members': members}, indent=settings.DEBUG)
280
    return HttpResponse(data)
281

    
282

    
283
@api.api_method(http_method="GET", user_required=True, logger=log)
284
def list_images(request, detail=False):
285
    """Return a list of available images.
286

287
    This includes images owned by the user, images shared with the user and
288
    public images.
289

290
    """
291

    
292
    def get_request_params(keys):
293
        params = {}
294
        for key in keys:
295
            val = request.GET.get(key, None)
296
            if val is not None:
297
                params[key] = val
298
        return params
299

    
300
    log.debug('list_public_images detail=%s', detail)
301

    
302
    filters = get_request_params(FILTERS)
303
    params = get_request_params(PARAMS)
304

    
305
    params.setdefault('sort_key', 'created_at')
306
    params.setdefault('sort_dir', 'desc')
307

    
308
    if not params['sort_key'] in SORT_KEY_OPTIONS:
309
        raise faults.BadRequest("Invalid 'sort_key'")
310
    if not params['sort_dir'] in SORT_DIR_OPTIONS:
311
        raise faults.BadRequest("Invalid 'sort_dir'")
312

    
313
    if 'size_max' in filters:
314
        try:
315
            filters['size_max'] = int(filters['size_max'])
316
        except ValueError:
317
            raise faults.BadRequest("Malformed request.")
318

    
319
    if 'size_min' in filters:
320
        try:
321
            filters['size_min'] = int(filters['size_min'])
322
        except ValueError:
323
            raise faults.BadRequest("Malformed request.")
324

    
325
    with image_backend(request.user_uniq) as backend:
326
        images = backend.list_images(filters, params)
327

    
328
    # Remove keys that should not be returned
329
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
330
    for image in images:
331
        for key in image.keys():
332
            if key not in fields:
333
                del image[key]
334

    
335
    data = json.dumps(images, indent=settings.DEBUG)
336
    return HttpResponse(data)
337

    
338

    
339
@api.api_method(http_method="GET", user_required=True, logger=log)
340
def list_shared_images(request, member):
341
    """Request shared images
342

343
    Described in:
344
    3.8. Requesting Shared Images
345

346
    Implementation notes:
347
      * It is not clear what this method should do. We return the IDs of
348
        the users's images that are accessible by `member`.
349
    """
350

    
351
    log.debug('list_shared_images %s', member)
352

    
353
    images = []
354
    with image_backend(request.user_uniq) as backend:
355
        for image in backend.list_shared_images(member=member):
356
            image_id = image['id']
357
            images.append({'image_id': image_id, 'can_share': False})
358

    
359
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
360
    return HttpResponse(data)
361

    
362

    
363
@api.api_method(http_method="DELETE", user_required=True, logger=log)
364
def remove_image_member(request, image_id, member):
365
    """Remove a member from an image
366

367
    Described in:
368
    3.10. Removing a Member from an Image
369
    """
370

    
371
    log.debug('remove_image_member %s %s', image_id, member)
372
    with image_backend(request.user_uniq) as backend:
373
        backend.remove_user(image_id, member)
374
    return HttpResponse(status=204)
375

    
376

    
377
@api.api_method(http_method="PUT", user_required=True, logger=log)
378
def update_image(request, image_id):
379
    """Update an image
380

381
    Described in:
382
    3.6.2. Updating an Image
383

384
    Implementation notes:
385
      * It is not clear which metadata are allowed to be updated. We support:
386
        name, disk_format, container_format, is_public, owner, properties
387
        and status.
388
    """
389

    
390
    meta = _get_image_headers(request)
391
    log.debug('update_image %s', meta)
392

    
393
    if not set(meta.keys()).issubset(set(UPDATE_FIELDS)):
394
        raise faults.BadRequest("Invalid metadata")
395

    
396
    with image_backend(request.user_uniq) as backend:
397
        image = backend.update_metadata(image_id, meta)
398
    return _create_image_response(image)
399

    
400

    
401
@api.api_method(http_method="PUT", user_required=True, logger=log)
402
def update_image_members(request, image_id):
403
    """Replace a membership list for an image
404

405
    Described in:
406
    3.11. Replacing a Membership List for an Image
407

408
    Limitations:
409
      * can_share value is ignored
410
    """
411

    
412
    log.debug('update_image_members %s', image_id)
413
    members = []
414
    try:
415
        data = json.loads(request.body)
416
        for member in data['memberships']:
417
            members.append(member['member_id'])
418
    except (ValueError, KeyError, TypeError):
419
        return HttpResponse(status=400)
420

    
421
    with image_backend(request.user_uniq) as backend:
422
        backend.replace_users(image_id, members)
423
    return HttpResponse(status=204)