Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.2 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
    for key in DETAIL_FIELDS:
88
        if key == 'properties':
89
            for k, v in image.get('properties', {}).items():
90
                name = 'x-image-meta-property-' + k.replace('_', '-')
91
                response[name] = uenc(v)
92
        else:
93
            if key == "status":
94
                img_status = image.get(key, "").upper()
95
                status = API_STATUS_FROM_IMAGE_STATUS.get(img_status,
96
                                                          "UNKNOWN")
97
                response["x-image-meta-status"] = status
98
            else:
99
                name = 'x-image-meta-' + key.replace('_', '-')
100
                response[name] = uenc(image.get(key, ''))
101

    
102
    return response
103

    
104

    
105
def _get_image_headers(request):
106
    def normalize(s):
107
        return ''.join('_' if c in punctuation else c.lower() for c in s)
108

    
109
    META_PREFIX = 'HTTP_X_IMAGE_META_'
110
    META_PREFIX_LEN = len(META_PREFIX)
111
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
112
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
113

    
114
    headers = {'properties': {}}
115

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

    
124
    is_public = headers.get('is_public', None)
125
    if is_public is not None:
126
        headers['is_public'] = True if is_public.lower() == 'true' else False
127

    
128
    if not headers['properties']:
129
        del headers['properties']
130

    
131
    return headers
132

    
133

    
134
@api.api_method(http_method="POST", user_required=True, logger=log)
135
def add_image(request):
136
    """Add a new virtual machine image
137

138
    Described in:
139
    3.6. Adding a New Virtual Machine Image
140

141
    Implementation notes:
142
      * The implementation is very inefficient as it loads the whole image
143
        in memory.
144

145
    Limitations:
146
      * x-image-meta-id is not supported. Will always return 409 Conflict.
147

148
    Extensions:
149
      * An x-image-meta-location header can be passed with a link to file,
150
        instead of uploading the data.
151
    """
152

    
153
    params = _get_image_headers(request)
154
    log.debug('add_image %s', params)
155

    
156
    if not set(params.keys()).issubset(set(ADD_FIELDS)):
157
        raise faults.BadRequest("Invalid parameters")
158

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

    
168
    try:
169
        split_url(location)
170
    except InvalidLocation:
171
        raise faults.BadRequest("Invalid location '%s'" % location)
172

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

    
181
    if not image:
182
        return HttpResponse('Registration failed', status=500)
183

    
184
    return _create_image_response(image)
185

    
186

    
187
@api.api_method(http_method="DELETE", user_required=True, logger=log)
188
def delete_image(request, image_id):
189
    """Delete an Image.
190

191
    This API call is not described in the Openstack Glance API.
192

193
    Implementation notes:
194
      * The implementation does not delete the Image from the storage
195
        backend. Instead it unregisters the image by removing all the
196
        metadata from the plankton metadata domain.
197

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

    
206

    
207
@api.api_method(http_method="PUT", user_required=True, logger=log)
208
def add_image_member(request, image_id, member):
209
    """Add a member to an image
210

211
    Described in:
212
    3.9. Adding a Member to an Image
213

214
    Limitations:
215
      * Passing a body to enable `can_share` is not supported.
216
    """
217

    
218
    log.debug('add_image_member %s %s', image_id, member)
219
    with image_backend(request.user_uniq) as backend:
220
        backend.add_user(image_id, member)
221
    return HttpResponse(status=204)
222

    
223

    
224
@api.api_method(http_method="GET", user_required=True, logger=log)
225
def get_image(request, image_id):
226
    """Retrieve a virtual machine image
227

228
    Described in:
229
    3.5. Retrieving a Virtual Machine Image
230

231
    Implementation notes:
232
      * The implementation is very inefficient as it loads the whole image
233
        in memory.
234
    """
235

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

    
249

    
250
@api.api_method(http_method="HEAD", user_required=True, logger=log)
251
def get_image_meta(request, image_id):
252
    """Return detailed metadata on a specific image
253

254
    Described in:
255
    3.4. Requesting Detailed Metadata on a Specific Image
256
    """
257

    
258
    with image_backend(request.user_uniq) as backend:
259
        image = backend.get_image(image_id)
260
    return _create_image_response(image)
261

    
262

    
263
@api.api_method(http_method="GET", user_required=True, logger=log)
264
def list_image_members(request, image_id):
265
    """List image memberships
266

267
    Described in:
268
    3.7. Requesting Image Memberships
269
    """
270

    
271
    with image_backend(request.user_uniq) as backend:
272
        users = backend.list_users(image_id)
273

    
274
    members = [{'member_id': u, 'can_share': False} for u in users]
275
    data = json.dumps({'members': members}, indent=settings.DEBUG)
276
    return HttpResponse(data)
277

    
278

    
279
@api.api_method(http_method="GET", user_required=True, logger=log)
280
def list_images(request, detail=False):
281
    """Return a list of available images.
282

283
    This includes images owned by the user, images shared with the user and
284
    public images.
285

286
    """
287

    
288
    def get_request_params(keys):
289
        params = {}
290
        for key in keys:
291
            val = request.GET.get(key, None)
292
            if val is not None:
293
                params[key] = val
294
        return params
295

    
296
    log.debug('list_public_images detail=%s', detail)
297

    
298
    filters = get_request_params(FILTERS)
299
    params = get_request_params(PARAMS)
300

    
301
    params.setdefault('sort_key', 'created_at')
302
    params.setdefault('sort_dir', 'desc')
303

    
304
    if not params['sort_key'] in SORT_KEY_OPTIONS:
305
        raise faults.BadRequest("Invalid 'sort_key'")
306
    if not params['sort_dir'] in SORT_DIR_OPTIONS:
307
        raise faults.BadRequest("Invalid 'sort_dir'")
308

    
309
    if 'size_max' in filters:
310
        try:
311
            filters['size_max'] = int(filters['size_max'])
312
        except ValueError:
313
            raise faults.BadRequest("Malformed request.")
314

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

    
321
    with image_backend(request.user_uniq) as backend:
322
        images = backend.list_images(filters, params)
323

    
324
    # Remove keys that should not be returned
325
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
326
    for image in images:
327
        for key in image.keys():
328
            if key not in fields:
329
                del image[key]
330

    
331
    data = json.dumps(images, indent=settings.DEBUG)
332
    return HttpResponse(data)
333

    
334

    
335
@api.api_method(http_method="GET", user_required=True, logger=log)
336
def list_shared_images(request, member):
337
    """Request shared images
338

339
    Described in:
340
    3.8. Requesting Shared Images
341

342
    Implementation notes:
343
      * It is not clear what this method should do. We return the IDs of
344
        the users's images that are accessible by `member`.
345
    """
346

    
347
    log.debug('list_shared_images %s', member)
348

    
349
    images = []
350
    with image_backend(request.user_uniq) as backend:
351
        for image in backend.list_shared_images(member=member):
352
            image_id = image['id']
353
            images.append({'image_id': image_id, 'can_share': False})
354

    
355
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
356
    return HttpResponse(data)
357

    
358

    
359
@api.api_method(http_method="DELETE", user_required=True, logger=log)
360
def remove_image_member(request, image_id, member):
361
    """Remove a member from an image
362

363
    Described in:
364
    3.10. Removing a Member from an Image
365
    """
366

    
367
    log.debug('remove_image_member %s %s', image_id, member)
368
    with image_backend(request.user_uniq) as backend:
369
        backend.remove_user(image_id, member)
370
    return HttpResponse(status=204)
371

    
372

    
373
@api.api_method(http_method="PUT", user_required=True, logger=log)
374
def update_image(request, image_id):
375
    """Update an image
376

377
    Described in:
378
    3.6.2. Updating an Image
379

380
    Implementation notes:
381
      * It is not clear which metadata are allowed to be updated. We support:
382
        name, disk_format, container_format, is_public, owner, properties
383
        and status.
384
    """
385

    
386
    meta = _get_image_headers(request)
387
    log.debug('update_image %s', meta)
388

    
389
    if not set(meta.keys()).issubset(set(UPDATE_FIELDS)):
390
        raise faults.BadRequest("Invalid metadata")
391

    
392
    with image_backend(request.user_uniq) as backend:
393
        image = backend.update_metadata(image_id, meta)
394
    return _create_image_response(image)
395

    
396

    
397
@api.api_method(http_method="PUT", user_required=True, logger=log)
398
def update_image_members(request, image_id):
399
    """Replace a membership list for an image
400

401
    Described in:
402
    3.11. Replacing a Membership List for an Image
403

404
    Limitations:
405
      * can_share value is ignored
406
    """
407

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

    
417
    with image_backend(request.user_uniq) as backend:
418
        backend.replace_users(image_id, members)
419
    return HttpResponse(status=204)