Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.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 StringIO import StringIO
39
from urllib import unquote
40

    
41
from django.conf import settings
42
from django.http import HttpResponse, HttpResponseNotFound
43

    
44
from synnefo.plankton.util import plankton_method
45

    
46

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

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

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

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

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

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

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

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

    
70

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

    
73

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

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

    
86
    return response
87

    
88

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

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

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

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

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

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

    
115
    return headers
116

    
117

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

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

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

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

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

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

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

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

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

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

    
156
    return _create_image_response(image)
157

    
158

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

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

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

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

    
174

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

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

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

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

    
200

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

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

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

    
214

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

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

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

    
228

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

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

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

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

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

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

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

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

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

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

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

    
273

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

278
    Described in:
279
    3.8. Requesting Shared Images
280

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

    
286
    log.debug('list_shared_images %s', member)
287

    
288
    images = []
289
    for image_id in request.backend.iter_shared(member):
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)