images.py 11.9 KB
Newer Older
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
6
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
10
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
11
12
13
14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
15
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
16
17
18
19
20
21
22
23
24
25
26
27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
28
#
Giorgos Verigakis's avatar
Giorgos Verigakis committed
29
30
31
32
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
33

34
from logging import getLogger
35
from itertools import ifilter
36

37
from dateutil.parser import parse as date_parse
38

39
40
41
42
43
try:
    from django.conf.urls import patterns
except ImportError:  # Django==1.2
    from django.conf.urls.defaults import patterns

44
45
from django.http import HttpResponse
from django.template.loader import render_to_string
46
from django.utils import simplejson as json
47

48
49
from snf_django.lib import api
from snf_django.lib.api import faults, utils
50
from synnefo.api import util
51
from synnefo.plankton.utils import image_backend
52

53

54
log = getLogger(__name__)
55

Christos Stavrakakis's avatar
Christos Stavrakakis committed
56
57
urlpatterns = patterns(
    'synnefo.api.images',
58
59
    (r'^(?:/|.json|.xml)?$', 'demux'),
    (r'^/detail(?:.json|.xml)?$', 'list_images', {'detail': True}),
60
    (r'^/([\w-]+)(?:.json|.xml)?$', 'image_demux'),
61
62
    (r'^/([\w-]+)/metadata(?:.json|.xml)?$', 'metadata_demux'),
    (r'^/([\w-]+)/metadata/(.+?)(?:.json|.xml)?$', 'metadata_item_demux')
63
64
)

Christos Stavrakakis's avatar
Christos Stavrakakis committed
65

66
67
68
69
70
71
def demux(request):
    if request.method == 'GET':
        return list_images(request)
    elif request.method == 'POST':
        return create_image(request)
    else:
72
        return api.api_method_not_allowed(request)
73

Christos Stavrakakis's avatar
Christos Stavrakakis committed
74

75
76
77
78
79
80
def image_demux(request, image_id):
    if request.method == 'GET':
        return get_image_details(request, image_id)
    elif request.method == 'DELETE':
        return delete_image(request, image_id)
    else:
81
        return api.api_method_not_allowed(request)
82

Christos Stavrakakis's avatar
Christos Stavrakakis committed
83

84
85
86
87
88
89
def metadata_demux(request, image_id):
    if request.method == 'GET':
        return list_metadata(request, image_id)
    elif request.method == 'POST':
        return update_metadata(request, image_id)
    else:
90
        return api.api_method_not_allowed(request)
91

Christos Stavrakakis's avatar
Christos Stavrakakis committed
92

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

103
104

def image_to_dict(image, detail=True):
105
    d = dict(id=image['id'], name=image['name'])
106
    if detail:
107
108
        d['updated'] = utils.isoformat(date_parse(image['updated_at']))
        d['created'] = utils.isoformat(date_parse(image['created_at']))
109
110
        d['status'] = 'DELETED' if image['deleted_at'] else 'ACTIVE'
        d['progress'] = 100 if image['status'] == 'available' else 0
111
112
        d['user_id'] = image['owner']
        d['tenant_id'] = image['owner']
113
        d['links'] = util.image_to_links(image["id"])
114
        if image["properties"]:
115
            d['metadata'] = image['properties']
116
117
        else:
            d['metadata'] = {}
118
119
120
    return d


121
@api.api_method("GET", user_required=True, logger=log)
122
123
124
125
126
127
128
def list_images(request, detail=False):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       overLimit (413)
129

Christos Stavrakakis's avatar
Christos Stavrakakis committed
130
    log.debug('list_images detail=%s', detail)
131
    since = utils.isoparse(request.GET.get('changes-since'))
Christos Stavrakakis's avatar
Christos Stavrakakis committed
132
    with image_backend(request.user_uniq) as backend:
133
        images = backend.list_images()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
134
        if since:
135
136
            updated_since = lambda img: date_parse(img["updated_at"]) >= since
            images = ifilter(updated_since, images)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
137
138
            if not images:
                return HttpResponse(status=304)
139
140

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
143
    if request.serialization == 'xml':
144
145
        data = render_to_string('list_images.xml',
                                dict(images=reply, detail=detail))
146
    else:
147
        data = json.dumps(dict(images=reply))
148

Giorgos Verigakis's avatar
Giorgos Verigakis committed
149
    return HttpResponse(data, status=200)
150

151

152
@api.api_method('POST', user_required=True, logger=log)
153
154
155
156
157
158
159
160
161
162
163
164
165
def create_image(request):
    # Normal Response Code: 202
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badMediaType(415),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       serverCapacityUnavailable (503),
    #                       buildInProgress (409),
    #                       resizeNotAllowed (403),
    #                       backupOrResizeInProgress (409),
    #                       overLimit (413)
166

167
    raise faults.NotImplemented('Not supported.')
168
169


170
@api.api_method('GET', user_required=True, logger=log)
171
172
173
174
175
176
177
178
def get_image_details(request, image_id):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       itemNotFound (404),
    #                       overLimit (413)
179

Christos Stavrakakis's avatar
Christos Stavrakakis committed
180
    log.debug('get_image_details %s', image_id)
181
182
    with image_backend(request.user_uniq) as backend:
        image = backend.get_image(image_id)
183
    reply = image_to_dict(image)
184

Giorgos Verigakis's avatar
Giorgos Verigakis committed
185
    if request.serialization == 'xml':
186
        data = render_to_string('image.xml', dict(image=reply))
187
    else:
188
        data = json.dumps(dict(image=reply))
189

190
191
    return HttpResponse(data, status=200)

192

193
@api.api_method('DELETE', user_required=True, logger=log)
194
195
196
197
198
199
200
def delete_image(request, image_id):
    # Normal Response Code: 204
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       overLimit (413)
201

Christos Stavrakakis's avatar
Christos Stavrakakis committed
202
    log.info('delete_image %s', image_id)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
203
    with image_backend(request.user_uniq) as backend:
204
        backend.unregister(image_id)
205
    log.info('User %s deleted image %s', request.user_uniq, image_id)
206
    return HttpResponse(status=204)
207

208

209
@api.api_method('GET', user_required=True, logger=log)
210
211
212
213
214
215
216
def list_metadata(request, image_id):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       overLimit (413)
217

Christos Stavrakakis's avatar
Christos Stavrakakis committed
218
    log.debug('list_image_metadata %s', image_id)
219
220
    with image_backend(request.user_uniq) as backend:
        image = backend.get_image(image_id)
221
    metadata = image['properties']
222
223
    return util.render_metadata(request, metadata, use_values=False,
                                status=200)
224

225

226
@api.api_method('POST', user_required=True, logger=log)
227
228
229
230
231
232
233
234
235
def update_metadata(request, image_id):
    # Normal Response Code: 201
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       badRequest (400),
    #                       buildInProgress (409),
    #                       badMediaType(415),
    #                       overLimit (413)
236

237
    req = utils.get_request_dict(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
238
    log.info('update_image_metadata %s %s', image_id, req)
239
240
241
242
243
244
245
    with image_backend(request.user_uniq) as backend:
        image = backend.get_image(image_id)
        try:
            metadata = req['metadata']
            assert isinstance(metadata, dict)
        except (KeyError, AssertionError):
            raise faults.BadRequest('Malformed request.')
246

247
248
        properties = image['properties']
        properties.update(metadata)
249

250
        backend.update_metadata(image_id, dict(properties=properties))
251

252
    return util.render_metadata(request, properties, status=201)
253

254

255
@api.api_method('GET', user_required=True, logger=log)
256
257
258
259
260
261
262
263
def get_metadata_item(request, image_id, key):
    # Normal Response Codes: 200, 203
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       badRequest (400),
    #                       overLimit (413)
264

Christos Stavrakakis's avatar
Christos Stavrakakis committed
265
    log.debug('get_image_metadata_item %s %s', image_id, key)
266
267
    with image_backend(request.user_uniq) as backend:
        image = backend.get_image(image_id)
268
269
    val = image['properties'].get(key)
    if val is None:
270
        raise faults.ItemNotFound('Metadata key not found.')
271
272
    return util.render_meta(request, {key: val}, status=200)

273

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

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

    val = metadict[key]
Christos Stavrakakis's avatar
Christos Stavrakakis committed
297
    with image_backend(request.user_uniq) as backend:
298
299
300
301
302
        image = backend.get_image(image_id)
        properties = image['properties']
        properties[key] = val

        backend.update_metadata(image_id, dict(properties=properties))
303

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

306

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
319
    log.info('delete_image_metadata_item %s %s', image_id, key)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
320
    with image_backend(request.user_uniq) as backend:
321
322
323
324
325
        image = backend.get_image(image_id)
        properties = image['properties']
        properties.pop(key, None)

        backend.update_metadata(image_id, dict(properties=properties))
326

327
    return HttpResponse(status=204)