Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.2 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_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
    images = request.backend.list(filters, params)
257

    
258
    # Remove keys that should not be returned
259
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
260
    for image in images:
261
        for key in image.keys():
262
            if key not in fields:
263
                del image[key]
264

    
265
    data = json.dumps(images, indent=settings.DEBUG)
266
    return HttpResponse(data)
267

    
268

    
269
@plankton_method('GET')
270
def list_shared_images(request, member):
271
    """Request shared images
272

273
    Described in:
274
    3.8. Requesting Shared Images
275

276
    Implementation notes:
277
      * It is not clear what this method should do. We return the IDs of
278
        the users's images that are accessible by `member`.
279
    """
280

    
281
    log.debug('list_shared_images %s', member)
282

    
283
    images = []
284
    for image in request.backend.iter_shared(member=member):
285
        image_id = image['id']
286
        images.append({'image_id': image_id, 'can_share': False})
287

    
288
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
289
    return HttpResponse(data)
290

    
291

    
292
@plankton_method('DELETE')
293
def remove_image_member(request, image_id, member):
294
    """Remove a member from an image
295

296
    Described in:
297
    3.10. Removing a Member from an Image
298
    """
299

    
300
    log.debug('remove_image_member %s %s', image_id, member)
301
    request.backend.remove_user(image_id, member)
302
    return HttpResponse(status=204)
303

    
304

    
305
@plankton_method('PUT')
306
def update_image(request, image_id):
307
    """Update an image
308

309
    Described in:
310
    3.6.2. Updating an Image
311

312
    Implementation notes:
313
      * It is not clear which metadata are allowed to be updated. We support:
314
        name, disk_format, container_format, is_public, owner, properties
315
        and status.
316
    """
317

    
318
    meta = _get_image_headers(request)
319
    log.debug('update_image %s', meta)
320

    
321
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
322

    
323
    image = request.backend.update(image_id, meta)
324
    return _create_image_response(image)
325

    
326

    
327
@plankton_method('PUT')
328
def update_image_members(request, image_id):
329
    """Replace a membership list for an image
330

331
    Described in:
332
    3.11. Replacing a Membership List for an Image
333

334
    Limitations:
335
      * can_share value is ignored
336
    """
337

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

    
347
    request.backend.replace_users(image_id, members)
348
    return HttpResponse(status=204)