Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.6 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
                         HttpResponseBadRequest)
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_images(request, detail=False):
230
    """Return a list of available images.
231

232
    This includes images owned by the user, images shared with the user and
233
    public images.
234

235
    """
236

    
237
    def get_request_params(keys):
238
        params = {}
239
        for key in keys:
240
            val = request.GET.get(key, None)
241
            if val is not None:
242
                params[key] = val
243
        return params
244

    
245
    log.debug('list_public_images detail=%s', detail)
246

    
247
    filters = get_request_params(FILTERS)
248
    params = get_request_params(PARAMS)
249

    
250
    params.setdefault('sort_key', 'created_at')
251
    params.setdefault('sort_dir', 'desc')
252

    
253
    assert params['sort_key'] in SORT_KEY_OPTIONS
254
    assert params['sort_dir'] in SORT_DIR_OPTIONS
255

    
256
    if 'size_max' in filters:
257
        try:
258
            filters['size_max'] = int(filters['size_max'])
259
        except ValueError:
260
            return HttpResponseBadRequest('400 Bad Request')
261

    
262
    if 'size_min' in filters:
263
        try:
264
            filters['size_min'] = int(filters['size_min'])
265
        except ValueError:
266
            return HttpResponseBadRequest('400 Bad Request')
267

    
268
    images = request.backend.list(filters, params)
269

    
270
    # Remove keys that should not be returned
271
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
272
    for image in images:
273
        for key in image.keys():
274
            if key not in fields:
275
                del image[key]
276

    
277
    data = json.dumps(images, indent=settings.DEBUG)
278
    return HttpResponse(data)
279

    
280

    
281
@plankton_method('GET')
282
def list_shared_images(request, member):
283
    """Request shared images
284

285
    Described in:
286
    3.8. Requesting Shared Images
287

288
    Implementation notes:
289
      * It is not clear what this method should do. We return the IDs of
290
        the users's images that are accessible by `member`.
291
    """
292

    
293
    log.debug('list_shared_images %s', member)
294

    
295
    images = []
296
    for image in request.backend.iter_shared(member=member):
297
        image_id = image['id']
298
        images.append({'image_id': image_id, 'can_share': False})
299

    
300
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
301
    return HttpResponse(data)
302

    
303

    
304
@plankton_method('DELETE')
305
def remove_image_member(request, image_id, member):
306
    """Remove a member from an image
307

308
    Described in:
309
    3.10. Removing a Member from an Image
310
    """
311

    
312
    log.debug('remove_image_member %s %s', image_id, member)
313
    request.backend.remove_user(image_id, member)
314
    return HttpResponse(status=204)
315

    
316

    
317
@plankton_method('PUT')
318
def update_image(request, image_id):
319
    """Update an image
320

321
    Described in:
322
    3.6.2. Updating an Image
323

324
    Implementation notes:
325
      * It is not clear which metadata are allowed to be updated. We support:
326
        name, disk_format, container_format, is_public, owner, properties
327
        and status.
328
    """
329

    
330
    meta = _get_image_headers(request)
331
    log.debug('update_image %s', meta)
332

    
333
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
334

    
335
    image = request.backend.update(image_id, meta)
336
    return _create_image_response(image)
337

    
338

    
339
@plankton_method('PUT')
340
def update_image_members(request, image_id):
341
    """Replace a membership list for an image
342

343
    Described in:
344
    3.11. Replacing a Membership List for an Image
345

346
    Limitations:
347
      * can_share value is ignored
348
    """
349

    
350
    log.debug('update_image_members %s', image_id)
351
    members = []
352
    try:
353
        data = json.loads(request.raw_post_data)
354
        for member in data['memberships']:
355
            members.append(member['member_id'])
356
    except (ValueError, KeyError, TypeError):
357
        return HttpResponse(status=400)
358

    
359
    request.backend.replace_users(image_id, members)
360
    return HttpResponse(status=204)