Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / images.py @ f13aab5d

History | View | Annotate | Download (11.5 kB)

1
# Copyright 2011-2012 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
from logging import getLogger
35

    
36
import dateutil.parser
37

    
38
from django.conf.urls.defaults import patterns
39
from django.http import HttpResponse
40
from django.template.loader import render_to_string
41
from django.utils import simplejson as json
42

    
43
from contextlib import contextmanager
44

    
45
from synnefo.api import util
46
from synnefo.api.common import method_not_allowed
47
from synnefo.api.faults import BadRequest, ItemNotFound, ServiceUnavailable
48
from synnefo.api.util import api_method, isoformat, isoparse
49
from synnefo.plankton.backend import ImageBackend
50

    
51

    
52
log = getLogger('synnefo.api')
53

    
54
urlpatterns = patterns(
55
    'synnefo.api.images',
56
    (r'^(?:/|.json|.xml)?$', 'demux'),
57
    (r'^/detail(?:.json|.xml)?$', 'list_images', {'detail': True}),
58
    (r'^/([\w-]+)(?:.json|.xml)?$', 'image_demux'),
59
    (r'^/([\w-]+)/meta(?:.json|.xml)?$', 'metadata_demux'),
60
    (r'^/([\w-]+)/meta/(.+?)(?:.json|.xml)?$', 'metadata_item_demux')
61
)
62

    
63

    
64
def demux(request):
65
    if request.method == 'GET':
66
        return list_images(request)
67
    elif request.method == 'POST':
68
        return create_image(request)
69
    else:
70
        return method_not_allowed(request)
71

    
72

    
73
def image_demux(request, image_id):
74
    if request.method == 'GET':
75
        return get_image_details(request, image_id)
76
    elif request.method == 'DELETE':
77
        return delete_image(request, image_id)
78
    else:
79
        return method_not_allowed(request)
80

    
81

    
82
def metadata_demux(request, image_id):
83
    if request.method == 'GET':
84
        return list_metadata(request, image_id)
85
    elif request.method == 'POST':
86
        return update_metadata(request, image_id)
87
    else:
88
        return method_not_allowed(request)
89

    
90

    
91
def metadata_item_demux(request, image_id, key):
92
    if request.method == 'GET':
93
        return get_metadata_item(request, image_id, key)
94
    elif request.method == 'PUT':
95
        return create_metadata_item(request, image_id, key)
96
    elif request.method == 'DELETE':
97
        return delete_metadata_item(request, image_id, key)
98
    else:
99
        return method_not_allowed(request)
100

    
101

    
102
def image_to_dict(image, detail=True):
103
    d = dict(id=image['id'], name=image['name'])
104
    if detail:
105
        d['updated'] = isoformat(dateutil.parser.parse(image['updated_at']))
106
        d['created'] = isoformat(dateutil.parser.parse(image['created_at']))
107
        d['status'] = 'DELETED' if image['deleted_at'] else 'ACTIVE'
108
        d['progress'] = 100 if image['status'] == 'available' else 0
109
        if image['properties']:
110
            d['metadata'] = {'values': image['properties']}
111
    return d
112

    
113

    
114
@contextmanager
115
def image_backend(userid):
116
    backend = ImageBackend(userid)
117
    try:
118
        yield backend
119
    finally:
120
        backend.close()
121

    
122

    
123
@api_method('GET')
124
def list_images(request, detail=False):
125
    # Normal Response Codes: 200, 203
126
    # Error Response Codes: computeFault (400, 500),
127
    #                       serviceUnavailable (503),
128
    #                       unauthorized (401),
129
    #                       badRequest (400),
130
    #                       overLimit (413)
131

    
132
    log.debug('list_images detail=%s', detail)
133
    with image_backend(request.user_uniq) as backend:
134
        since = isoparse(request.GET.get('changes-since'))
135
        if since:
136
            images = []
137
            for image in backend.iter():
138
                updated = dateutil.parser.parse(image['updated_at'])
139
                if updated >= since:
140
                    images.append(image)
141
            if not images:
142
                return HttpResponse(status=304)
143
        else:
144
            images = backend.list()
145

    
146
    images = sorted(images, key=lambda x: x['id'])
147
    reply = [image_to_dict(image, detail) for image in images]
148

    
149
    if request.serialization == 'xml':
150
        data = render_to_string('list_images.xml',
151
                                dict(images=reply, detail=detail))
152
    else:
153
        data = json.dumps(dict(images={'values': reply}))
154

    
155
    return HttpResponse(data, status=200)
156

    
157

    
158
@api_method('POST')
159
def create_image(request):
160
    # Normal Response Code: 202
161
    # Error Response Codes: computeFault (400, 500),
162
    #                       serviceUnavailable (503),
163
    #                       unauthorized (401),
164
    #                       badMediaType(415),
165
    #                       itemNotFound (404),
166
    #                       badRequest (400),
167
    #                       serverCapacityUnavailable (503),
168
    #                       buildInProgress (409),
169
    #                       resizeNotAllowed (403),
170
    #                       backupOrResizeInProgress (409),
171
    #                       overLimit (413)
172

    
173
    raise ServiceUnavailable('Not supported.')
174

    
175

    
176
@api_method('GET')
177
def get_image_details(request, image_id):
178
    # Normal Response Codes: 200, 203
179
    # Error Response Codes: computeFault (400, 500),
180
    #                       serviceUnavailable (503),
181
    #                       unauthorized (401),
182
    #                       badRequest (400),
183
    #                       itemNotFound (404),
184
    #                       overLimit (413)
185

    
186
    log.debug('get_image_details %s', image_id)
187
    image = util.get_image(image_id, request.user_uniq)
188
    reply = image_to_dict(image)
189

    
190
    if request.serialization == 'xml':
191
        data = render_to_string('image.xml', dict(image=reply))
192
    else:
193
        data = json.dumps(dict(image=reply))
194

    
195
    return HttpResponse(data, status=200)
196

    
197

    
198
@api_method('DELETE')
199
def delete_image(request, image_id):
200
    # Normal Response Code: 204
201
    # Error Response Codes: computeFault (400, 500),
202
    #                       serviceUnavailable (503),
203
    #                       unauthorized (401),
204
    #                       itemNotFound (404),
205
    #                       overLimit (413)
206

    
207
    log.info('delete_image %s', image_id)
208
    with image_backend(request.user_uniq) as backend:
209
        backend.unregister(image_id)
210
    log.info('User %s deleted image %s', request.user_uniq, image_id)
211
    return HttpResponse(status=204)
212

    
213

    
214
@api_method('GET')
215
def list_metadata(request, image_id):
216
    # Normal Response Codes: 200, 203
217
    # Error Response Codes: computeFault (400, 500),
218
    #                       serviceUnavailable (503),
219
    #                       unauthorized (401),
220
    #                       badRequest (400),
221
    #                       overLimit (413)
222

    
223
    log.debug('list_image_metadata %s', image_id)
224
    image = util.get_image(image_id, request.user_uniq)
225
    metadata = image['properties']
226
    return util.render_metadata(request, metadata, use_values=True, status=200)
227

    
228

    
229
@api_method('POST')
230
def update_metadata(request, image_id):
231
    # Normal Response Code: 201
232
    # Error Response Codes: computeFault (400, 500),
233
    #                       serviceUnavailable (503),
234
    #                       unauthorized (401),
235
    #                       badRequest (400),
236
    #                       buildInProgress (409),
237
    #                       badMediaType(415),
238
    #                       overLimit (413)
239

    
240
    req = util.get_request_dict(request)
241
    log.info('update_image_metadata %s %s', image_id, req)
242
    image = util.get_image(image_id, request.user_uniq)
243
    try:
244
        metadata = req['metadata']
245
        assert isinstance(metadata, dict)
246
    except (KeyError, AssertionError):
247
        raise BadRequest('Malformed request.')
248

    
249
    properties = image['properties']
250
    properties.update(metadata)
251

    
252
    with image_backend(request.user_uniq) as backend:
253
        backend.update(image_id, dict(properties=properties))
254

    
255
    return util.render_metadata(request, properties, status=201)
256

    
257

    
258
@api_method('GET')
259
def get_metadata_item(request, image_id, key):
260
    # Normal Response Codes: 200, 203
261
    # Error Response Codes: computeFault (400, 500),
262
    #                       serviceUnavailable (503),
263
    #                       unauthorized (401),
264
    #                       itemNotFound (404),
265
    #                       badRequest (400),
266
    #                       overLimit (413)
267

    
268
    log.debug('get_image_metadata_item %s %s', image_id, key)
269
    image = util.get_image(image_id, request.user_uniq)
270
    val = image['properties'].get(key)
271
    if val is None:
272
        raise ItemNotFound('Metadata key not found.')
273
    return util.render_meta(request, {key: val}, status=200)
274

    
275

    
276
@api_method('PUT')
277
def create_metadata_item(request, image_id, key):
278
    # Normal Response Code: 201
279
    # Error Response Codes: computeFault (400, 500),
280
    #                       serviceUnavailable (503),
281
    #                       unauthorized (401),
282
    #                       itemNotFound (404),
283
    #                       badRequest (400),
284
    #                       buildInProgress (409),
285
    #                       badMediaType(415),
286
    #                       overLimit (413)
287

    
288
    req = util.get_request_dict(request)
289
    log.info('create_image_metadata_item %s %s %s', image_id, key, req)
290
    try:
291
        metadict = req['meta']
292
        assert isinstance(metadict, dict)
293
        assert len(metadict) == 1
294
        assert key in metadict
295
    except (KeyError, AssertionError):
296
        raise BadRequest('Malformed request.')
297

    
298
    val = metadict[key]
299
    image = util.get_image(image_id, request.user_uniq)
300
    properties = image['properties']
301
    properties[key] = val
302

    
303
    with image_backend(request.user_uniq) as backend:
304
        backend.update(image_id, dict(properties=properties))
305

    
306
    return util.render_meta(request, {key: val}, status=201)
307

    
308

    
309
@api_method('DELETE')
310
def delete_metadata_item(request, image_id, key):
311
    # Normal Response Code: 204
312
    # Error Response Codes: computeFault (400, 500),
313
    #                       serviceUnavailable (503),
314
    #                       unauthorized (401),
315
    #                       itemNotFound (404),
316
    #                       badRequest (400),
317
    #                       buildInProgress (409),
318
    #                       badMediaType(415),
319
    #                       overLimit (413),
320

    
321
    log.info('delete_image_metadata_item %s %s', image_id, key)
322
    image = util.get_image(image_id, request.user_uniq)
323
    properties = image['properties']
324
    properties.pop(key, None)
325

    
326
    with image_backend(request.user_uniq) as backend:
327
        backend.update(image_id, dict(properties=properties))
328

    
329
    return HttpResponse(status=204)