images.py 12.5 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
from django.conf.urls import patterns
40
41
from django.http import HttpResponse
from django.template.loader import render_to_string
42
from django.utils import simplejson as json
43

44
45
from snf_django.lib import api
from snf_django.lib.api import faults, utils
46
from synnefo.api import util
47
from synnefo.plankton import backend
48

49

50
log = getLogger(__name__)
51

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
61

62
63
64
65
66
67
def demux(request):
    if request.method == 'GET':
        return list_images(request)
    elif request.method == 'POST':
        return create_image(request)
    else:
68
69
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET', 'POST'])
70

Christos Stavrakakis's avatar
Christos Stavrakakis committed
71

72
73
74
75
76
77
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:
78
79
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET', 'DELETE'])
80

Christos Stavrakakis's avatar
Christos Stavrakakis committed
81

82
83
84
85
86
87
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:
88
89
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET', 'POST'])
90

Christos Stavrakakis's avatar
Christos Stavrakakis committed
91

92
93
94
95
96
97
98
99
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:
100
101
102
103
        return api.api_method_not_allowed(request,
                                          allowed_methods=['GET',
                                                           'PUT',
                                                           'DELETE'])
104

105

106
107
108
109
110
111
112
API_STATUS_FROM_IMAGE_STATUS = {
    "CREATING": "SAVING",
    "AVAILABLE": "ACTIVE",
    "ERROR": "ERROR",
    "DELETED": "DELETED"}


113
def image_to_dict(image, detail=True):
114
    d = dict(id=image['id'], name=image['name'])
115
    if detail:
116
117
        d['updated'] = utils.isoformat(date_parse(image['updated_at']))
        d['created'] = utils.isoformat(date_parse(image['created_at']))
118
119
120
121
        img_status = image.get("status", "").upper()
        status = API_STATUS_FROM_IMAGE_STATUS.get(img_status, "UNKNOWN")
        d['status'] = status
        d['progress'] = 100 if status == 'ACTIVE' else 0
122
123
        d['user_id'] = image['owner']
        d['tenant_id'] = image['owner']
124
        d['links'] = util.image_to_links(image["id"])
125
        if image["properties"]:
126
            d['metadata'] = image['properties']
127
128
        else:
            d['metadata'] = {}
129
        d["is_snapshot"] = image["is_snapshot"]
130
131
132
    return d


133
@api.api_method("GET", user_required=True, logger=log)
134
135
136
137
138
139
140
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)
141

Christos Stavrakakis's avatar
Christos Stavrakakis committed
142
    log.debug('list_images detail=%s', detail)
143
    since = utils.isoparse(request.GET.get('changes-since'))
144
    with backend.PlanktonBackend(request.user_uniq) as b:
145
        images = b.list_images()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
146
        if since:
147
148
            updated_since = lambda img: date_parse(img["updated_at"]) >= since
            images = ifilter(updated_since, images)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
149
150
            if not images:
                return HttpResponse(status=304)
151
152

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
155
    if request.serialization == 'xml':
156
157
        data = render_to_string('list_images.xml',
                                dict(images=reply, detail=detail))
158
    else:
159
        data = json.dumps(dict(images=reply))
160

Giorgos Verigakis's avatar
Giorgos Verigakis committed
161
    return HttpResponse(data, status=200)
162

163

164
@api.api_method('POST', user_required=True, logger=log)
165
166
167
168
169
170
171
172
173
174
175
176
177
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)
178

179
    raise faults.NotImplemented('Not supported.')
180
181


182
@api.api_method('GET', user_required=True, logger=log)
183
184
185
186
187
188
189
190
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)
191

Christos Stavrakakis's avatar
Christos Stavrakakis committed
192
    log.debug('get_image_details %s', image_id)
193
    with backend.PlanktonBackend(request.user_uniq) as b:
194
        image = b.get_image(image_id)
195
    reply = image_to_dict(image)
196

Giorgos Verigakis's avatar
Giorgos Verigakis committed
197
    if request.serialization == 'xml':
198
        data = render_to_string('image.xml', dict(image=reply))
199
    else:
200
        data = json.dumps(dict(image=reply))
201

202
203
    return HttpResponse(data, status=200)

204

205
@api.api_method('DELETE', user_required=True, logger=log)
206
207
208
209
210
211
212
def delete_image(request, image_id):
    # Normal Response Code: 204
    # Error Response Codes: computeFault (400, 500),
    #                       serviceUnavailable (503),
    #                       unauthorized (401),
    #                       itemNotFound (404),
    #                       overLimit (413)
213

Christos Stavrakakis's avatar
Christos Stavrakakis committed
214
    log.info('delete_image %s', image_id)
215
    with backend.PlanktonBackend(request.user_uniq) as b:
216
        b.unregister(image_id)
217
    log.info('User %s deleted image %s', request.user_uniq, image_id)
218
    return HttpResponse(status=204)
219

220

221
@api.api_method('GET', user_required=True, logger=log)
222
223
224
225
226
227
228
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)
229

Christos Stavrakakis's avatar
Christos Stavrakakis committed
230
    log.debug('list_image_metadata %s', image_id)
231
    with backend.PlanktonBackend(request.user_uniq) as b:
232
        image = b.get_image(image_id)
233
    metadata = image['properties']
234
235
    return util.render_metadata(request, metadata, use_values=False,
                                status=200)
236

237

238
@api.api_method('POST', user_required=True, logger=log)
239
240
241
242
243
244
245
246
247
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)
248

249
    req = utils.get_request_dict(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
250
    log.info('update_image_metadata %s %s', image_id, req)
251
    with backend.PlanktonBackend(request.user_uniq) as b:
252
        image = b.get_image(image_id)
253
254
255
256
257
        try:
            metadata = req['metadata']
            assert isinstance(metadata, dict)
        except (KeyError, AssertionError):
            raise faults.BadRequest('Malformed request.')
258

259
260
        properties = image['properties']
        properties.update(metadata)
261

262
        b.update_metadata(image_id, dict(properties=properties))
263

264
    return util.render_metadata(request, properties, status=201)
265

266

267
@api.api_method('GET', user_required=True, logger=log)
268
269
270
271
272
273
274
275
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)
276

Christos Stavrakakis's avatar
Christos Stavrakakis committed
277
    log.debug('get_image_metadata_item %s %s', image_id, key)
278
    with backend.PlanktonBackend(request.user_uniq) as b:
279
        image = b.get_image(image_id)
280
281
    val = image['properties'].get(key)
    if val is None:
282
        raise faults.ItemNotFound('Metadata key not found.')
283
284
    return util.render_meta(request, {key: val}, status=200)

285

286
@api.api_method('PUT', user_required=True, logger=log)
287
288
289
290
291
292
293
294
295
296
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)
297

298
    req = utils.get_request_dict(request)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
299
    log.info('create_image_metadata_item %s %s %s', image_id, key, req)
300
301
302
303
304
305
    try:
        metadict = req['meta']
        assert isinstance(metadict, dict)
        assert len(metadict) == 1
        assert key in metadict
    except (KeyError, AssertionError):
306
        raise faults.BadRequest('Malformed request.')
307
308

    val = metadict[key]
309
    with backend.PlanktonBackend(request.user_uniq) as b:
310
        image = b.get_image(image_id)
311
312
313
        properties = image['properties']
        properties[key] = val

314
        b.update_metadata(image_id, dict(properties=properties))
315

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

318

319
@api.api_method('DELETE', user_required=True, logger=log)
320
321
322
323
324
325
326
327
328
329
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),
330

Christos Stavrakakis's avatar
Christos Stavrakakis committed
331
    log.info('delete_image_metadata_item %s %s', image_id, key)
332
    with backend.PlanktonBackend(request.user_uniq) as b:
333
        image = b.get_image(image_id)
334
335
336
        properties = image['properties']
        properties.pop(key, None)

337
        b.update_metadata(image_id, dict(properties=properties))
338

339
    return HttpResponse(status=204)