Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.3 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
from synnefo.plankton.backend import split_url
47

    
48

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

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

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

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

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

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

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

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

    
72

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

    
75

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

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

    
88
    return response
89

    
90

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

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

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

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

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

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

    
117
    return headers
118

    
119

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

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

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

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

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

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

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

    
145
    name = params.pop('name')
146
    location = params.pop('location', None)
147
    try:
148
        split_url(location)
149
    except AssertionError:
150
        raise faults.BadRequest("Invalid location '%s'" % location)
151

    
152
    if location:
153
        with image_backend(request.user_uniq) as backend:
154
            image = backend.register(name, location, params)
155
    else:
156
        #f = StringIO(request.raw_post_data)
157
        #image = backend.put(name, f, params)
158
        return HttpResponse(status=501)     # Not Implemented
159

    
160
    if not image:
161
        return HttpResponse('Registration failed', status=500)
162

    
163
    return _create_image_response(image)
164

    
165

    
166
@api.api_method(http_method="DELETE", user_required=True, logger=log)
167
def delete_image(request, image_id):
168
    """Delete an Image.
169

170
    This API call is not described in the Openstack Glance API.
171

172
    Implementation notes:
173
      * The implementation does not delete the Image from the storage
174
        backend. Instead it unregisters the image by removing all the
175
        metadata from the plankton metadata domain.
176

177
    """
178
    log.info("delete_image '%s'" % image_id)
179
    userid = request.user_uniq
180
    with image_backend(userid) as backend:
181
        backend.unregister(image_id)
182
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
183
    return HttpResponse(status=204)
184

    
185

    
186
@api.api_method(http_method="PUT", user_required=True, logger=log)
187
def add_image_member(request, image_id, member):
188
    """Add a member to an image
189

190
    Described in:
191
    3.9. Adding a Member to an Image
192

193
    Limitations:
194
      * Passing a body to enable `can_share` is not supported.
195
    """
196

    
197
    log.debug('add_image_member %s %s', image_id, member)
198
    with image_backend(request.user_uniq) as backend:
199
        backend.add_user(image_id, member)
200
    return HttpResponse(status=204)
201

    
202

    
203
@api.api_method(http_method="GET", user_required=True, logger=log)
204
def get_image(request, image_id):
205
    """Retrieve a virtual machine image
206

207
    Described in:
208
    3.5. Retrieving a Virtual Machine Image
209

210
    Implementation notes:
211
      * The implementation is very inefficient as it loads the whole image
212
        in memory.
213
    """
214

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

    
228

    
229
@api.api_method(http_method="HEAD", user_required=True, logger=log)
230
def get_image_meta(request, image_id):
231
    """Return detailed metadata on a specific image
232

233
    Described in:
234
    3.4. Requesting Detailed Metadata on a Specific Image
235
    """
236

    
237
    with image_backend(request.user_uniq) as backend:
238
        image = backend.get_image(image_id)
239
    return _create_image_response(image)
240

    
241

    
242
@api.api_method(http_method="GET", user_required=True, logger=log)
243
def list_image_members(request, image_id):
244
    """List image memberships
245

246
    Described in:
247
    3.7. Requesting Image Memberships
248
    """
249

    
250
    with image_backend(request.user_uniq) as backend:
251
        users = backend.list_users(image_id)
252

    
253
    members = [{'member_id': u, 'can_share': False} for u in users]
254
    data = json.dumps({'members': members}, indent=settings.DEBUG)
255
    return HttpResponse(data)
256

    
257

    
258
@api.api_method(http_method="GET", user_required=True, logger=log)
259
def list_images(request, detail=False):
260
    """Return a list of available images.
261

262
    This includes images owned by the user, images shared with the user and
263
    public images.
264

265
    """
266

    
267
    def get_request_params(keys):
268
        params = {}
269
        for key in keys:
270
            val = request.GET.get(key, None)
271
            if val is not None:
272
                params[key] = val
273
        return params
274

    
275
    log.debug('list_public_images detail=%s', detail)
276

    
277
    filters = get_request_params(FILTERS)
278
    params = get_request_params(PARAMS)
279

    
280
    params.setdefault('sort_key', 'created_at')
281
    params.setdefault('sort_dir', 'desc')
282

    
283
    assert params['sort_key'] in SORT_KEY_OPTIONS
284
    assert params['sort_dir'] in SORT_DIR_OPTIONS
285

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

    
292
    if 'size_min' in filters:
293
        try:
294
            filters['size_min'] = int(filters['size_min'])
295
        except ValueError:
296
            raise faults.BadRequest("Malformed request.")
297

    
298
    with image_backend(request.user_uniq) as backend:
299
        images = backend.list_images(filters, params)
300

    
301
    # Remove keys that should not be returned
302
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
303
    for image in images:
304
        for key in image.keys():
305
            if key not in fields:
306
                del image[key]
307

    
308
    data = json.dumps(images, indent=settings.DEBUG)
309
    return HttpResponse(data)
310

    
311

    
312
@api.api_method(http_method="GET", user_required=True, logger=log)
313
def list_shared_images(request, member):
314
    """Request shared images
315

316
    Described in:
317
    3.8. Requesting Shared Images
318

319
    Implementation notes:
320
      * It is not clear what this method should do. We return the IDs of
321
        the users's images that are accessible by `member`.
322
    """
323

    
324
    log.debug('list_shared_images %s', member)
325

    
326
    images = []
327
    with image_backend(request.user_uniq) as backend:
328
        for image in backend.list_shared_images(member=member):
329
            image_id = image['id']
330
            images.append({'image_id': image_id, 'can_share': False})
331

    
332
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
333
    return HttpResponse(data)
334

    
335

    
336
@api.api_method(http_method="DELETE", user_required=True, logger=log)
337
def remove_image_member(request, image_id, member):
338
    """Remove a member from an image
339

340
    Described in:
341
    3.10. Removing a Member from an Image
342
    """
343

    
344
    log.debug('remove_image_member %s %s', image_id, member)
345
    with image_backend(request.user_uniq) as backend:
346
        backend.remove_user(image_id, member)
347
    return HttpResponse(status=204)
348

    
349

    
350
@api.api_method(http_method="PUT", user_required=True, logger=log)
351
def update_image(request, image_id):
352
    """Update an image
353

354
    Described in:
355
    3.6.2. Updating an Image
356

357
    Implementation notes:
358
      * It is not clear which metadata are allowed to be updated. We support:
359
        name, disk_format, container_format, is_public, owner, properties
360
        and status.
361
    """
362

    
363
    meta = _get_image_headers(request)
364
    log.debug('update_image %s', meta)
365

    
366
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
367

    
368
    with image_backend(request.user_uniq) as backend:
369
        image = backend.update_metadata(image_id, meta)
370
    return _create_image_response(image)
371

    
372

    
373
@api.api_method(http_method="PUT", user_required=True, logger=log)
374
def update_image_members(request, image_id):
375
    """Replace a membership list for an image
376

377
    Described in:
378
    3.11. Replacing a Membership List for an Image
379

380
    Limitations:
381
      * can_share value is ignored
382
    """
383

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

    
393
    with image_backend(request.user_uniq) as backend:
394
        backend.replace_users(image_id, members)
395
    return HttpResponse(status=204)