Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.4 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', "is_snapshot")
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
    def normalize(s):
79
        return ''.join('_' if c in punctuation else c.lower() for c in s)\
80
                .replace("\n", "_")
81

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

    
91
    return response
92

    
93

    
94
def _get_image_headers(request):
95
    def normalize(s):
96
        return ''.join('_' if c in punctuation else c.lower() for c in s)
97

    
98
    META_PREFIX = 'HTTP_X_IMAGE_META_'
99
    META_PREFIX_LEN = len(META_PREFIX)
100
    META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
101
    META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
102

    
103
    headers = {'properties': {}}
104

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

    
113
    is_public = headers.get('is_public', None)
114
    if is_public is not None:
115
        headers['is_public'] = True if is_public.lower() == 'true' else False
116

    
117
    if not headers['properties']:
118
        del headers['properties']
119

    
120
    return headers
121

    
122

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

127
    Described in:
128
    3.6. Adding a New Virtual Machine Image
129

130
    Implementation notes:
131
      * The implementation is very inefficient as it loads the whole image
132
        in memory.
133

134
    Limitations:
135
      * x-image-meta-id is not supported. Will always return 409 Conflict.
136

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

    
142
    params = _get_image_headers(request)
143
    log.debug('add_image %s', params)
144

    
145
    assert 'name' in params
146
    assert set(params.keys()).issubset(set(ADD_FIELDS))
147

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

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

    
163
    if not image:
164
        return HttpResponse('Registration failed', status=500)
165

    
166
    return _create_image_response(image)
167

    
168

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

173
    This API call is not described in the Openstack Glance API.
174

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

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

    
188

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

193
    Described in:
194
    3.9. Adding a Member to an Image
195

196
    Limitations:
197
      * Passing a body to enable `can_share` is not supported.
198
    """
199

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

    
205

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

210
    Described in:
211
    3.5. Retrieving a Virtual Machine Image
212

213
    Implementation notes:
214
      * The implementation is very inefficient as it loads the whole image
215
        in memory.
216
    """
217

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

    
231

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

236
    Described in:
237
    3.4. Requesting Detailed Metadata on a Specific Image
238
    """
239

    
240
    with image_backend(request.user_uniq) as backend:
241
        image = backend.get_image(image_id)
242
    return _create_image_response(image)
243

    
244

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

249
    Described in:
250
    3.7. Requesting Image Memberships
251
    """
252

    
253
    with image_backend(request.user_uniq) as backend:
254
        users = backend.list_users(image_id)
255

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

    
260

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

265
    This includes images owned by the user, images shared with the user and
266
    public images.
267

268
    """
269

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

    
278
    log.debug('list_public_images detail=%s', detail)
279

    
280
    filters = get_request_params(FILTERS)
281
    params = get_request_params(PARAMS)
282

    
283
    params.setdefault('sort_key', 'created_at')
284
    params.setdefault('sort_dir', 'desc')
285

    
286
    assert params['sort_key'] in SORT_KEY_OPTIONS
287
    assert params['sort_dir'] in SORT_DIR_OPTIONS
288

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

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

    
301
    with image_backend(request.user_uniq) as backend:
302
        images = backend.list_images(filters, params)
303

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

    
311
    data = json.dumps(images, indent=settings.DEBUG)
312
    return HttpResponse(data)
313

    
314

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

319
    Described in:
320
    3.8. Requesting Shared Images
321

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

    
327
    log.debug('list_shared_images %s', member)
328

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

    
335
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
336
    return HttpResponse(data)
337

    
338

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

343
    Described in:
344
    3.10. Removing a Member from an Image
345
    """
346

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

    
352

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

357
    Described in:
358
    3.6.2. Updating an Image
359

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

    
366
    meta = _get_image_headers(request)
367
    log.debug('update_image %s', meta)
368

    
369
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
370

    
371
    with image_backend(request.user_uniq) as backend:
372
        image = backend.update_metadata(image_id, meta)
373
    return _create_image_response(image)
374

    
375

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

380
    Described in:
381
    3.11. Replacing a Membership List for an Image
382

383
    Limitations:
384
      * can_share value is ignored
385
    """
386

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

    
396
    with image_backend(request.user_uniq) as backend:
397
        backend.replace_users(image_id, members)
398
    return HttpResponse(status=204)