Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (11.4 kB)

1
# Copyright 2011 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

    
43
from synnefo.plankton.util import plankton_method
44

    
45

    
46
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
47
           'size_max')
48

    
49
PARAMS = ('sort_key', 'sort_dir')
50

    
51
SORT_KEY_OPTIONS = ('id', 'name', 'status', 'size', 'disk_format',
52
                    'container_format', 'created_at', 'updated_at')
53

    
54
SORT_DIR_OPTIONS = ('asc', 'desc')
55

    
56
LIST_FIELDS = ('status', 'name', 'disk_format', 'container_format', 'size',
57
               'id')
58

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

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

    
66
UPDATE_FIELDS = ('name', 'disk_format', 'container_format', 'is_public',
67
                 'owner', 'properties', 'status')
68

    
69

    
70
log = getLogger('synnefo.plankton')
71

    
72

    
73
def _create_image_response(image):
74
    response = HttpResponse()
75

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

    
85
    return response
86

    
87

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

    
92
    META_PREFIX = 'HTTP_X_IMAGE_META_'
93
    META_PREFIX_LEN = len(META_PREFIX)
94
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
95
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
96

    
97
    headers = {'properties': {}}
98

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

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

    
111
    if not headers['properties']:
112
        del headers['properties']
113

    
114
    return headers
115

    
116

    
117
@plankton_method('POST')
118
def add_image(request):
119
    """Add a new virtual machine image
120

121
    Described in:
122
    3.6. Adding a New Virtual Machine Image
123

124
    Implementation notes:
125
      * The implementation is very inefficient as it loads the whole image
126
        in memory.
127

128
    Limitations:
129
      * x-image-meta-id is not supported. Will always return 409 Conflict.
130

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

    
136
    params = _get_image_headers(request)
137
    log.debug('add_image %s', params)
138

    
139
    assert 'name' in params
140
    assert set(params.keys()).issubset(set(ADD_FIELDS))
141

    
142
    name = params.pop('name')
143
    location = params.pop('location', None)
144

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

    
152
    if not image:
153
        return HttpResponse('Registration failed', status=500)
154

    
155
    return _create_image_response(image)
156

    
157

    
158
@plankton_method('PUT')
159
def add_image_member(request, image_id, member):
160
    """Add a member to an image
161

162
    Described in:
163
    3.9. Adding a Member to an Image
164

165
    Limitations:
166
      * Passing a body to enable `can_share` is not supported.
167
    """
168

    
169
    log.debug('add_image_member %s %s', image_id, member)
170
    request.backend.add_user(image_id, member)
171
    return HttpResponse(status=204)
172

    
173

    
174
@plankton_method('GET')
175
def get_image(request, image_id):
176
    """Retrieve a virtual machine image
177

178
    Described in:
179
    3.5. Retrieving a Virtual Machine Image
180

181
    Implementation notes:
182
      * The implementation is very inefficient as it loads the whole image
183
        in memory.
184
    """
185

    
186
    #image = request.backend.get_image(image_id)
187
    #if not image:
188
    #    return HttpResponseNotFound()
189
    #
190
    #response = _create_image_response(image)
191
    #data = request.backend.get_data(image)
192
    #response.content = data
193
    #response['Content-Length'] = len(data)
194
    #response['Content-Type'] = 'application/octet-stream'
195
    #response['ETag'] = image['checksum']
196
    #return response
197
    return HttpResponse(status=501)     # Not Implemented
198

    
199

    
200
@plankton_method('HEAD')
201
def get_image_meta(request, image_id):
202
    """Return detailed metadata on a specific image
203

204
    Described in:
205
    3.4. Requesting Detailed Metadata on a Specific Image
206
    """
207

    
208
    image = request.backend.get_image(image_id)
209
    if not image:
210
        return HttpResponseNotFound()
211
    return _create_image_response(image)
212

    
213

    
214
@plankton_method('GET')
215
def list_image_members(request, image_id):
216
    """List image memberships
217

218
    Described in:
219
    3.7. Requesting Image Memberships
220
    """
221

    
222
    members = [{'member_id': user, 'can_share': False}
223
                for user in request.backend.list_users(image_id)]
224
    data = json.dumps({'members': members}, indent=settings.DEBUG)
225
    return HttpResponse(data)
226

    
227

    
228
@plankton_method('GET')
229
def list_public_images(request, detail=False):
230
    """Return a list of public VM images.
231

232
    Described in:
233
    3.1. Requesting a List of Public VM Images
234
    3.2. Requesting Detailed Metadata on Public VM Images
235
    3.3. Filtering Images Returned via GET /images andGET /images/detail
236

237
    Extensions:
238
      * Image ID is returned in both compact and detail listings
239
    """
240

    
241
    def get_request_params(keys):
242
        params = {}
243
        for key in keys:
244
            val = request.GET.get(key, None)
245
            if val is not None:
246
                params[key] = val
247
        return params
248

    
249
    log.debug('list_public_images detail=%s', detail)
250

    
251
    filters = get_request_params(FILTERS)
252
    params = get_request_params(PARAMS)
253

    
254
    params.setdefault('sort_key', 'created_at')
255
    params.setdefault('sort_dir', 'desc')
256

    
257
    assert params['sort_key'] in SORT_KEY_OPTIONS
258
    assert params['sort_dir'] in SORT_DIR_OPTIONS
259

    
260
    images = request.backend.list_public(filters, params)
261

    
262
    # Remove keys that should not be returned
263
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
264
    for image in images:
265
        for key in image.keys():
266
            if key not in fields:
267
                del image[key]
268

    
269
    data = json.dumps(images, indent=settings.DEBUG)
270
    return HttpResponse(data)
271

    
272

    
273
@plankton_method('GET')
274
def list_shared_images_with(request, member):
275
    """Request shared images
276

277
    Described in:
278
    3.8. Requesting Shared Images
279

280
    Implementation notes:
281
      * It is not clear what this method should do. We return the IDs of
282
        the users's images that are accessible by `member`.
283
    """
284

    
285
    log.debug('list_shared_images_with %s', member)
286

    
287
    images = []
288
    for image in request.backend.iter_shared(member):
289
        image_id = image['id']
290
        images.append({'image_id': image_id, 'can_share': False})
291

    
292
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
293
    return HttpResponse(data)
294

    
295

    
296
@plankton_method('DELETE')
297
def remove_image_member(request, image_id, member):
298
    """Remove a member from an image
299

300
    Described in:
301
    3.10. Removing a Member from an Image
302
    """
303

    
304
    log.debug('remove_image_member %s %s', image_id, member)
305
    request.backend.remove_user(image_id, member)
306
    return HttpResponse(status=204)
307

    
308

    
309
@plankton_method('PUT')
310
def update_image(request, image_id):
311
    """Update an image
312

313
    Described in:
314
    3.6.2. Updating an Image
315

316
    Implementation notes:
317
      * It is not clear which metadata are allowed to be updated. We support:
318
        name, disk_format, container_format, is_public, owner, properties
319
        and status.
320
    """
321

    
322
    meta = _get_image_headers(request)
323
    log.debug('update_image %s', meta)
324

    
325
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
326

    
327
    image = request.backend.update(image_id, meta)
328
    return _create_image_response(image)
329

    
330

    
331
@plankton_method('PUT')
332
def update_image_members(request, image_id):
333
    """Replace a membership list for an image
334

335
    Described in:
336
    3.11. Replacing a Membership List for an Image
337

338
    Limitations:
339
      * can_share value is ignored
340
    """
341

    
342
    log.debug('update_image_members %s', image_id)
343
    members = []
344
    try:
345
        data = json.loads(request.raw_post_data)
346
        for member in data['memberships']:
347
            members.append(member['member_id'])
348
    except (ValueError, KeyError, TypeError):
349
        return HttpResponse(status=400)
350

    
351
    request.backend.replace_users(image_id, members)
352
    return HttpResponse(status=204)
353

    
354

    
355
@plankton_method('GET')
356
def list_shared_images(request, detail=False):
357
    def get_request_params(keys):
358
        params = {}
359
        for key in keys:
360
            val = request.GET.get(key, None)
361
            if val is not None:
362
                params[key] = val
363
        return params
364

    
365
    log.debug('list_shared_images detail=%s, request %s', detail, request)
366

    
367
    filters = get_request_params(FILTERS)
368
    params = get_request_params(PARAMS)
369

    
370
    params.setdefault('sort_key', 'created_at')
371
    params.setdefault('sort_dir', 'desc')
372

    
373
    assert params['sort_key'] in SORT_KEY_OPTIONS
374
    assert params['sort_dir'] in SORT_DIR_OPTIONS
375

    
376
    images = request.backend.list(filters, params)
377
    images = filter(lambda x: not x['is_public'], images)
378

    
379
    # Remove keys that should not be returned
380
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
381
    for image in images:
382
        for key in image.keys():
383
            if key not in fields:
384
                del image[key]
385

    
386
    data = json.dumps(images, indent=settings.DEBUG)
387
    return HttpResponse(data)