Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.1 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.plankton.utils import image_backend
46

    
47

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

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

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

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

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

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

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

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

    
71

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

    
74

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

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

    
87
    return response
88

    
89

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

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

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

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

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

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

    
116
    return headers
117

    
118

    
119
@api.api_method(http_method="POST", user_required=True, logger=log)
120
def add_image(request):
121
    """Add a new virtual machine image
122

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

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

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

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

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

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

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

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

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

    
158
    return _create_image_response(image)
159

    
160

    
161
@api.api_method(http_method="DELETE", user_required=True, logger=log)
162
def delete_image(request, image_id):
163
    """Delete an Image.
164

165
    This API call is not described in the Openstack Glance API.
166

167
    Implementation notes:
168
      * The implementation does not delete the Image from the storage
169
        backend. Instead it unregisters the image by removing all the
170
        metadata from the plankton metadata domain.
171

172
    """
173
    log.info("delete_image '%s'" % image_id)
174
    userid = request.user_uniq
175
    with image_backend(userid) as backend:
176
        backend.unregister(image_id)
177
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
178
    return HttpResponse(status=204)
179

    
180

    
181
@api.api_method(http_method="PUT", user_required=True, logger=log)
182
def add_image_member(request, image_id, member):
183
    """Add a member to an image
184

185
    Described in:
186
    3.9. Adding a Member to an Image
187

188
    Limitations:
189
      * Passing a body to enable `can_share` is not supported.
190
    """
191

    
192
    log.debug('add_image_member %s %s', image_id, member)
193
    with image_backend(request.user_uniq) as backend:
194
        backend.add_user(image_id, member)
195
    return HttpResponse(status=204)
196

    
197

    
198
@api.api_method(http_method="GET", user_required=True, logger=log)
199
def get_image(request, image_id):
200
    """Retrieve a virtual machine image
201

202
    Described in:
203
    3.5. Retrieving a Virtual Machine Image
204

205
    Implementation notes:
206
      * The implementation is very inefficient as it loads the whole image
207
        in memory.
208
    """
209

    
210
    #image = backend.get_image(image_id)
211
    #if not image:
212
    #    return HttpResponseNotFound()
213
    #
214
    #response = _create_image_response(image)
215
    #data = backend.get_data(image)
216
    #response.content = data
217
    #response['Content-Length'] = len(data)
218
    #response['Content-Type'] = 'application/octet-stream'
219
    #response['ETag'] = image['checksum']
220
    #return response
221
    return HttpResponse(status=501)     # Not Implemented
222

    
223

    
224
@api.api_method(http_method="HEAD", user_required=True, logger=log)
225
def get_image_meta(request, image_id):
226
    """Return detailed metadata on a specific image
227

228
    Described in:
229
    3.4. Requesting Detailed Metadata on a Specific Image
230
    """
231

    
232
    with image_backend(request.user_uniq) as backend:
233
        image = backend.get_image(image_id)
234
    return _create_image_response(image)
235

    
236

    
237
@api.api_method(http_method="GET", user_required=True, logger=log)
238
def list_image_members(request, image_id):
239
    """List image memberships
240

241
    Described in:
242
    3.7. Requesting Image Memberships
243
    """
244

    
245
    with image_backend(request.user_uniq) as backend:
246
        users = backend.list_users(image_id)
247

    
248
    members = [{'member_id': u, 'can_share': False} for u in users]
249
    data = json.dumps({'members': members}, indent=settings.DEBUG)
250
    return HttpResponse(data)
251

    
252

    
253
@api.api_method(http_method="GET", user_required=True, logger=log)
254
def list_images(request, detail=False):
255
    """Return a list of available images.
256

257
    This includes images owned by the user, images shared with the user and
258
    public images.
259

260
    """
261

    
262
    def get_request_params(keys):
263
        params = {}
264
        for key in keys:
265
            val = request.GET.get(key, None)
266
            if val is not None:
267
                params[key] = val
268
        return params
269

    
270
    log.debug('list_public_images detail=%s', detail)
271

    
272
    filters = get_request_params(FILTERS)
273
    params = get_request_params(PARAMS)
274

    
275
    params.setdefault('sort_key', 'created_at')
276
    params.setdefault('sort_dir', 'desc')
277

    
278
    assert params['sort_key'] in SORT_KEY_OPTIONS
279
    assert params['sort_dir'] in SORT_DIR_OPTIONS
280

    
281
    if 'size_max' in filters:
282
        try:
283
            filters['size_max'] = int(filters['size_max'])
284
        except ValueError:
285
            raise faults.BadRequest("Malformed request.")
286

    
287
    if 'size_min' in filters:
288
        try:
289
            filters['size_min'] = int(filters['size_min'])
290
        except ValueError:
291
            raise faults.BadRequest("Malformed request.")
292

    
293
    with image_backend(request.user_uniq) as backend:
294
        images = backend.list_images(filters, params)
295

    
296
    # Remove keys that should not be returned
297
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
298
    for image in images:
299
        for key in image.keys():
300
            if key not in fields:
301
                del image[key]
302

    
303
    data = json.dumps(images, indent=settings.DEBUG)
304
    return HttpResponse(data)
305

    
306

    
307
@api.api_method(http_method="GET", user_required=True, logger=log)
308
def list_shared_images(request, member):
309
    """Request shared images
310

311
    Described in:
312
    3.8. Requesting Shared Images
313

314
    Implementation notes:
315
      * It is not clear what this method should do. We return the IDs of
316
        the users's images that are accessible by `member`.
317
    """
318

    
319
    log.debug('list_shared_images %s', member)
320

    
321
    images = []
322
    with image_backend(request.user_uniq) as backend:
323
        for image in backend.list_shared_images(member=member):
324
            image_id = image['id']
325
            images.append({'image_id': image_id, 'can_share': False})
326

    
327
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
328
    return HttpResponse(data)
329

    
330

    
331
@api.api_method(http_method="DELETE", user_required=True, logger=log)
332
def remove_image_member(request, image_id, member):
333
    """Remove a member from an image
334

335
    Described in:
336
    3.10. Removing a Member from an Image
337
    """
338

    
339
    log.debug('remove_image_member %s %s', image_id, member)
340
    with image_backend(request.user_uniq) as backend:
341
        backend.remove_user(image_id, member)
342
    return HttpResponse(status=204)
343

    
344

    
345
@api.api_method(http_method="PUT", user_required=True, logger=log)
346
def update_image(request, image_id):
347
    """Update an image
348

349
    Described in:
350
    3.6.2. Updating an Image
351

352
    Implementation notes:
353
      * It is not clear which metadata are allowed to be updated. We support:
354
        name, disk_format, container_format, is_public, owner, properties
355
        and status.
356
    """
357

    
358
    meta = _get_image_headers(request)
359
    log.debug('update_image %s', meta)
360

    
361
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
362

    
363
    with image_backend(request.user_uniq) as backend:
364
        image = backend.update_metadata(image_id, meta)
365
    return _create_image_response(image)
366

    
367

    
368
@api.api_method(http_method="PUT", user_required=True, logger=log)
369
def update_image_members(request, image_id):
370
    """Replace a membership list for an image
371

372
    Described in:
373
    3.11. Replacing a Membership List for an Image
374

375
    Limitations:
376
      * can_share value is ignored
377
    """
378

    
379
    log.debug('update_image_members %s', image_id)
380
    members = []
381
    try:
382
        data = json.loads(request.raw_post_data)
383
        for member in data['memberships']:
384
            members.append(member['member_id'])
385
    except (ValueError, KeyError, TypeError):
386
        return HttpResponse(status=400)
387

    
388
    with image_backend(request.user_uniq) as backend:
389
        backend.replace_users(image_id, members)
390
    return HttpResponse(status=204)