~ubuntu-branches/ubuntu/quantal/glance/quantal-updates

« back to all changes in this revision

Viewing changes to .pc/CVE-2012-4573b.patch/glance/api/v2/images.py

  • Committer: Package Import Robot
  • Author(s): Jamie Strandboge
  • Date: 2012-11-09 06:53:44 UTC
  • Revision ID: package-import@ubuntu.com-20121109065344-qtqcevhk2hm43lvv
Tags: 2012.2-0ubuntu2.3
* SECURITY UPDATE: deletion of arbitrary public and shared images via
  authenticated user
  - debian/patches/CVE-2012-4573b.patch: previous patch was incomplete.
    Make corresponding change to glance/api/v2/images.py
  - CVE-2012-4573
* debian/control: add Build-Depends-Indep on python-chardet. This is needed
  by python-requests to do encoding detection which otherwise fails in the
  new tests introduced in CVE-2012-4573b.patch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2012 OpenStack LLC.
 
2
# All Rights Reserved.
 
3
#
 
4
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
5
#    not use this file except in compliance with the License. You may obtain
 
6
#    a copy of the License at
 
7
#
 
8
#         http://www.apache.org/licenses/LICENSE-2.0
 
9
#
 
10
#    Unless required by applicable law or agreed to in writing, software
 
11
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
12
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
13
#    License for the specific language governing permissions and limitations
 
14
#    under the License.
 
15
 
 
16
import copy
 
17
import datetime
 
18
import json
 
19
import re
 
20
import urllib
 
21
 
 
22
import webob.exc
 
23
 
 
24
from glance.api import policy
 
25
import glance.api.v2 as v2
 
26
from glance.common import exception
 
27
from glance.common import utils
 
28
from glance.common import wsgi
 
29
import glance.db
 
30
import glance.notifier
 
31
from glance.openstack.common import cfg
 
32
import glance.openstack.common.log as logging
 
33
from glance.openstack.common import timeutils
 
34
import glance.schema
 
35
import glance.store
 
36
 
 
37
 
 
38
LOG = logging.getLogger(__name__)
 
39
 
 
40
CONF = cfg.CONF
 
41
 
 
42
 
 
43
class ImagesController(object):
 
44
    def __init__(self, db_api=None, policy_enforcer=None, notifier=None,
 
45
            store_api=None):
 
46
        self.db_api = db_api or glance.db.get_api()
 
47
        self.db_api.configure_db()
 
48
        self.policy = policy_enforcer or policy.Enforcer()
 
49
        self.notifier = notifier or glance.notifier.Notifier()
 
50
        self.store_api = store_api or glance.store
 
51
        self.store_api.create_stores()
 
52
 
 
53
    def _enforce(self, req, action):
 
54
        """Authorize an action against our policies"""
 
55
        try:
 
56
            self.policy.enforce(req.context, action, {})
 
57
        except exception.Forbidden:
 
58
            raise webob.exc.HTTPForbidden()
 
59
 
 
60
    def _normalize_properties(self, image):
 
61
        """Convert the properties from the stored format to a dict
 
62
 
 
63
        The db api returns a list of dicts that look like
 
64
        {'name': <key>, 'value': <value>}, while it expects a format
 
65
        like {<key>: <value>} in image create and update calls. This
 
66
        function takes the extra step that the db api should be
 
67
        responsible for in the image get calls.
 
68
 
 
69
        The db api will also return deleted image properties that must
 
70
        be filtered out.
 
71
        """
 
72
        properties = [(p['name'], p['value'])
 
73
                      for p in image['properties'] if not p['deleted']]
 
74
        image['properties'] = dict(properties)
 
75
        return image
 
76
 
 
77
    def _extract_tags(self, image):
 
78
        try:
 
79
            #NOTE(bcwaldon): cast to set to make the list unique, then
 
80
            # cast back to list since that's a more sane response type
 
81
            return list(set(image.pop('tags')))
 
82
        except KeyError:
 
83
            pass
 
84
 
 
85
    def _append_tags(self, context, image):
 
86
        image['tags'] = self.db_api.image_tag_get_all(context, image['id'])
 
87
        return image
 
88
 
 
89
    @utils.mutating
 
90
    def create(self, req, image):
 
91
        self._enforce(req, 'add_image')
 
92
        is_public = image.get('is_public')
 
93
        if is_public:
 
94
            self._enforce(req, 'publicize_image')
 
95
        image['owner'] = req.context.owner
 
96
        image['status'] = 'queued'
 
97
 
 
98
        tags = self._extract_tags(image)
 
99
 
 
100
        image = dict(self.db_api.image_create(req.context, image))
 
101
 
 
102
        if tags is not None:
 
103
            self.db_api.image_tag_set_all(req.context, image['id'], tags)
 
104
            image['tags'] = tags
 
105
        else:
 
106
            image['tags'] = []
 
107
 
 
108
        v2.update_image_read_acl(req, self.store_api, self.db_api, image)
 
109
        image = self._normalize_properties(dict(image))
 
110
        self.notifier.info('image.update', image)
 
111
        return image
 
112
 
 
113
    def index(self, req, marker=None, limit=None, sort_key='created_at',
 
114
              sort_dir='desc', filters={}):
 
115
        self._enforce(req, 'get_images')
 
116
        filters['deleted'] = False
 
117
        #NOTE(bcwaldon): is_public=True gets public images and those
 
118
        # owned by the authenticated tenant
 
119
        result = {}
 
120
        filters.setdefault('is_public', True)
 
121
        if limit is None:
 
122
            limit = CONF.limit_param_default
 
123
        limit = min(CONF.api_limit_max, limit)
 
124
 
 
125
        try:
 
126
            images = self.db_api.image_get_all(req.context, filters=filters,
 
127
                                               marker=marker, limit=limit,
 
128
                                               sort_key=sort_key,
 
129
                                               sort_dir=sort_dir)
 
130
            if len(images) != 0 and len(images) == limit:
 
131
                result['next_marker'] = images[-1]['id']
 
132
        except exception.InvalidFilterRangeValue as e:
 
133
            raise webob.exc.HTTPBadRequest(explanation=unicode(e))
 
134
        except exception.InvalidSortKey as e:
 
135
            raise webob.exc.HTTPBadRequest(explanation=unicode(e))
 
136
        except exception.NotFound as e:
 
137
            raise webob.exc.HTTPBadRequest(explanation=unicode(e))
 
138
        images = [self._normalize_properties(dict(image)) for image in images]
 
139
        result['images'] = [self._append_tags(req.context, image)
 
140
                            for image in images]
 
141
        return result
 
142
 
 
143
    def _get_image(self, context, image_id):
 
144
        try:
 
145
            return self.db_api.image_get(context, image_id)
 
146
        except (exception.NotFound, exception.Forbidden):
 
147
            raise webob.exc.HTTPNotFound()
 
148
 
 
149
    def show(self, req, image_id):
 
150
        self._enforce(req, 'get_image')
 
151
        image = self._get_image(req.context, image_id)
 
152
        image = self._normalize_properties(dict(image))
 
153
        return self._append_tags(req.context, image)
 
154
 
 
155
    @utils.mutating
 
156
    def update(self, req, image_id, changes):
 
157
        self._enforce(req, 'modify_image')
 
158
        context = req.context
 
159
        try:
 
160
            image = self.db_api.image_get(context, image_id)
 
161
        except (exception.NotFound, exception.Forbidden):
 
162
            msg = ("Failed to find image %(image_id)s to update" % locals())
 
163
            LOG.info(msg)
 
164
            raise webob.exc.HTTPNotFound(explanation=msg)
 
165
 
 
166
        image = self._normalize_properties(dict(image))
 
167
        updates = self._extract_updates(req, image, changes)
 
168
 
 
169
        tags = None
 
170
        if len(updates) > 0:
 
171
            tags = self._extract_tags(updates)
 
172
            purge_props = 'properties' in updates
 
173
            try:
 
174
                image = self.db_api.image_update(context, image_id, updates,
 
175
                                                 purge_props)
 
176
            except (exception.NotFound, exception.Forbidden):
 
177
                raise webob.exc.HTTPNotFound()
 
178
            image = self._normalize_properties(dict(image))
 
179
 
 
180
        v2.update_image_read_acl(req, self.store_api, self.db_api, image)
 
181
 
 
182
        if tags is not None:
 
183
            self.db_api.image_tag_set_all(req.context, image_id, tags)
 
184
            image['tags'] = tags
 
185
        else:
 
186
            self._append_tags(req.context, image)
 
187
 
 
188
        self.notifier.info('image.update', image)
 
189
        return image
 
190
 
 
191
    def _extract_updates(self, req, image, changes):
 
192
        """ Determine the updates to pass to the database api.
 
193
 
 
194
        Given the current image, convert a list of changes to be made
 
195
        into the corresponding update dictionary that should be passed to
 
196
        db_api.image_update.
 
197
 
 
198
        Changes have the following parts
 
199
        op    - 'add' a new attribute, 'replace' an existing attribute, or
 
200
                'remove' an existing attribute.
 
201
        path  - A list of path parts for determining which attribute the
 
202
                the operation applies to.
 
203
        value - For 'add' and 'replace', the new value the attribute should
 
204
                assume.
 
205
 
 
206
        For the current use case, there are two types of valid paths. For base
 
207
        attributes (fields stored directly on the Image object) the path
 
208
        must take the form ['<attribute name>']. These attributes are always
 
209
        present so the only valid operation on them is 'replace'. For image
 
210
        properties, the path takes the form ['properties', '<property name>']
 
211
        and all operations are valid.
 
212
 
 
213
        Future refactoring should simplify this code by hardening the image
 
214
        abstraction such that database details such as how image properties
 
215
        are stored do not have any influence here.
 
216
        """
 
217
        updates = {}
 
218
        property_updates = image['properties']
 
219
        for change in changes:
 
220
            path = change['path']
 
221
            if len(path) == 1:
 
222
                assert change['op'] == 'replace'
 
223
                key = change['path'][0]
 
224
                if key == 'is_public' and change['value']:
 
225
                    self._enforce(req, 'publicize_image')
 
226
                updates[key] = change['value']
 
227
            else:
 
228
                assert len(path) == 2
 
229
                assert path[0] == 'properties'
 
230
                update_method_name = '_do_%s_property' % change['op']
 
231
                assert hasattr(self, update_method_name)
 
232
                update_method = getattr(self, update_method_name)
 
233
                update_method(property_updates, change)
 
234
                updates['properties'] = property_updates
 
235
        return updates
 
236
 
 
237
    def _do_replace_property(self, updates, change):
 
238
        """ Replace a single image property, ensuring it's present. """
 
239
        key = change['path'][1]
 
240
        if key not in updates:
 
241
            msg = _("Property %s does not exist.")
 
242
            raise webob.exc.HTTPConflict(msg % key)
 
243
        updates[key] = change['value']
 
244
 
 
245
    def _do_add_property(self, updates, change):
 
246
        """ Add a new image property, ensuring it does not already exist. """
 
247
        key = change['path'][1]
 
248
        if key in updates:
 
249
            msg = _("Property %s already present.")
 
250
            raise webob.exc.HTTPConflict(msg % key)
 
251
        updates[key] = change['value']
 
252
 
 
253
    def _do_remove_property(self, updates, change):
 
254
        """ Remove an image property, ensuring it's present. """
 
255
        key = change['path'][1]
 
256
        if key not in updates:
 
257
            msg = _("Property %s does not exist.")
 
258
            raise webob.exc.HTTPConflict(msg % key)
 
259
        del updates[key]
 
260
 
 
261
    @utils.mutating
 
262
    def delete(self, req, image_id):
 
263
        self._enforce(req, 'delete_image')
 
264
        image = self._get_image(req.context, image_id)
 
265
 
 
266
        if image['protected']:
 
267
            msg = _("Unable to delete as image %(image_id)s is protected"
 
268
                    % locals())
 
269
            raise webob.exc.HTTPForbidden(explanation=msg)
 
270
 
 
271
        status = 'deleted'
 
272
        if image['location']:
 
273
            if CONF.delayed_delete:
 
274
                status = 'pending_delete'
 
275
                self.store_api.schedule_delayed_delete_from_backend(
 
276
                                image['location'], id)
 
277
            else:
 
278
                self.store_api.safe_delete_from_backend(image['location'],
 
279
                                                        req.context, id)
 
280
 
 
281
        try:
 
282
            self.db_api.image_update(req.context, image_id, {'status': status})
 
283
            self.db_api.image_destroy(req.context, image_id)
 
284
        except (exception.NotFound, exception.Forbidden):
 
285
            msg = ("Failed to find image %(image_id)s to delete" % locals())
 
286
            LOG.info(msg)
 
287
            raise webob.exc.HTTPNotFound()
 
288
        else:
 
289
            self.notifier.info('image.delete', image)
 
290
 
 
291
 
 
292
class RequestDeserializer(wsgi.JSONRequestDeserializer):
 
293
 
 
294
    _readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
 
295
            'size', 'direct_url', 'self', 'file', 'schema']
 
296
    _reserved_properties = ['owner', 'is_public', 'location',
 
297
            'deleted', 'deleted_at']
 
298
    _base_properties = ['checksum', 'created_at', 'container_format',
 
299
            'disk_format', 'id', 'min_disk', 'min_ram', 'name', 'size',
 
300
            'status', 'tags', 'updated_at', 'visibility', 'protected']
 
301
 
 
302
    def __init__(self, schema=None):
 
303
        super(RequestDeserializer, self).__init__()
 
304
        self.schema = schema or get_schema()
 
305
 
 
306
    def _parse_image(self, request):
 
307
        body = self._get_request_body(request)
 
308
        try:
 
309
            self.schema.validate(body)
 
310
        except exception.InvalidObject as e:
 
311
            raise webob.exc.HTTPBadRequest(explanation=unicode(e))
 
312
 
 
313
        # Ensure all specified properties are allowed
 
314
        self._check_readonly(body)
 
315
        self._check_reserved(body)
 
316
 
 
317
        # Create a dict of base image properties, with user- and deployer-
 
318
        # defined properties contained in a 'properties' dictionary
 
319
        image = {'properties': body}
 
320
        for key in self._base_properties:
 
321
            try:
 
322
                image[key] = image['properties'].pop(key)
 
323
            except KeyError:
 
324
                pass
 
325
 
 
326
        if 'visibility' in image:
 
327
            image['is_public'] = image.pop('visibility') == 'public'
 
328
 
 
329
        return {'image': image}
 
330
 
 
331
    def _get_request_body(self, request):
 
332
        output = super(RequestDeserializer, self).default(request)
 
333
        if not 'body' in output:
 
334
            msg = _('Body expected in request.')
 
335
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
336
        return output['body']
 
337
 
 
338
    @classmethod
 
339
    def _check_readonly(cls, image):
 
340
        for key in cls._readonly_properties:
 
341
            if key in image:
 
342
                msg = "Attribute \'%s\' is read-only." % key
 
343
                raise webob.exc.HTTPForbidden(explanation=unicode(msg))
 
344
 
 
345
    @classmethod
 
346
    def _check_reserved(cls, image):
 
347
        for key in cls._reserved_properties:
 
348
            if key in image:
 
349
                msg = "Attribute \'%s\' is reserved." % key
 
350
                raise webob.exc.HTTPForbidden(explanation=unicode(msg))
 
351
 
 
352
    def create(self, request):
 
353
        return self._parse_image(request)
 
354
 
 
355
    def _get_change_operation(self, raw_change):
 
356
        op = None
 
357
        for key in ['replace', 'add', 'remove']:
 
358
            if key in raw_change:
 
359
                if op is not None:
 
360
                    msg = _('Operation objects must contain only one member'
 
361
                            ' named "add", "remove", or "replace".')
 
362
                    raise webob.exc.HTTPBadRequest(explanation=msg)
 
363
                op = key
 
364
        if op is None:
 
365
            msg = _('Operation objects must contain exactly one member'
 
366
                    ' named "add", "remove", or "replace".')
 
367
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
368
        return op
 
369
 
 
370
    def _get_change_path(self, raw_change, op):
 
371
        key = self._decode_json_pointer(raw_change[op])
 
372
        if key in self._readonly_properties:
 
373
            msg = "Attribute \'%s\' is read-only." % key
 
374
            raise webob.exc.HTTPForbidden(explanation=unicode(msg))
 
375
        if key in self._reserved_properties:
 
376
            msg = "Attribute \'%s\' is reserved." % key
 
377
            raise webob.exc.HTTPForbidden(explanation=unicode(msg))
 
378
 
 
379
        # For image properties, we need to put "properties" at the beginning
 
380
        if key not in self._base_properties:
 
381
            return ['properties', key]
 
382
        return [key]
 
383
 
 
384
    def _decode_json_pointer(self, pointer):
 
385
        """ Parse a json pointer.
 
386
 
 
387
        Json Pointers are defined in
 
388
        http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer .
 
389
        The pointers use '/' for separation between object attributes, such
 
390
        that '/A/B' would evaluate to C in {"A": {"B": "C"}}. A '/' character
 
391
        in an attribute name is encoded as "~1" and a '~' character is encoded
 
392
        as "~0".
 
393
        """
 
394
        self._validate_json_pointer(pointer)
 
395
        return pointer.lstrip('/').replace('~1', '/').replace('~0', '~')
 
396
 
 
397
    def _validate_json_pointer(self, pointer):
 
398
        """ Validate a json pointer.
 
399
 
 
400
        We only accept a limited form of json pointers. Specifically, we do
 
401
        not allow multiple levels of indirection, so there can only be one '/'
 
402
        in the pointer, located at the start of the string.
 
403
        """
 
404
        if not pointer.startswith('/'):
 
405
            msg = _('Pointer `%s` does not start with "/".' % pointer)
 
406
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
407
        if '/' in pointer[1:]:
 
408
            msg = _('Pointer `%s` contains more than one "/".' % pointer)
 
409
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
410
        if re.match('~[^01]', pointer):
 
411
            msg = _('Pointer `%s` contains "~" not part of'
 
412
                    ' a recognized escape sequence.' % pointer)
 
413
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
414
 
 
415
    def _get_change_value(self, raw_change, op):
 
416
        if 'value' not in raw_change:
 
417
            msg = _('Operation "%s" requires a member named "value".')
 
418
            raise webob.exc.HTTPBadRequest(explanation=msg % op)
 
419
        return raw_change['value']
 
420
 
 
421
    def _validate_change(self, change):
 
422
        if change['op'] == 'delete':
 
423
            return
 
424
        partial_image = {change['path'][-1]: change['value']}
 
425
        try:
 
426
            self.schema.validate(partial_image)
 
427
        except exception.InvalidObject as e:
 
428
            raise webob.exc.HTTPBadRequest(explanation=unicode(e))
 
429
 
 
430
    def update(self, request):
 
431
        changes = []
 
432
        valid_content_types = [
 
433
            'application/openstack-images-v2.0-json-patch'
 
434
        ]
 
435
        if request.content_type not in valid_content_types:
 
436
            headers = {'Accept-Patch': ','.join(valid_content_types)}
 
437
            raise webob.exc.HTTPUnsupportedMediaType(headers=headers)
 
438
        body = self._get_request_body(request)
 
439
        if not isinstance(body, list):
 
440
            msg = _('Request body must be a JSON array of operation objects.')
 
441
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
442
        for raw_change in body:
 
443
            if not isinstance(raw_change, dict):
 
444
                msg = _('Operations must be JSON objects.')
 
445
                raise webob.exc.HTTPBadRequest(explanation=msg)
 
446
            op = self._get_change_operation(raw_change)
 
447
            path = self._get_change_path(raw_change, op)
 
448
            change = {'op': op, 'path': path}
 
449
            if not op == 'remove':
 
450
                change['value'] = self._get_change_value(raw_change, op)
 
451
                self._validate_change(change)
 
452
                if change['path'] == ['visibility']:
 
453
                    change['path'] = ['is_public']
 
454
                    change['value'] = change['value'] == 'public'
 
455
            changes.append(change)
 
456
        return {'changes': changes}
 
457
 
 
458
    def _validate_limit(self, limit):
 
459
        try:
 
460
            limit = int(limit)
 
461
        except ValueError:
 
462
            msg = _("limit param must be an integer")
 
463
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
464
 
 
465
        if limit < 0:
 
466
            msg = _("limit param must be positive")
 
467
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
468
 
 
469
        return limit
 
470
 
 
471
    def _validate_sort_dir(self, sort_dir):
 
472
        if sort_dir not in ['asc', 'desc']:
 
473
            msg = _('Invalid sort direction: %s' % sort_dir)
 
474
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
475
 
 
476
        return sort_dir
 
477
 
 
478
    def _get_filters(self, filters):
 
479
        visibility = filters.pop('visibility', None)
 
480
        if visibility:
 
481
            if visibility in ['public', 'private']:
 
482
                filters['is_public'] = visibility == 'public'
 
483
            else:
 
484
                msg = _('Invalid visibility value: %s') % visibility
 
485
                raise webob.exc.HTTPBadRequest(explanation=msg)
 
486
 
 
487
        return filters
 
488
 
 
489
    def index(self, request):
 
490
        params = request.params.copy()
 
491
        limit = params.pop('limit', None)
 
492
        marker = params.pop('marker', None)
 
493
        sort_dir = params.pop('sort_dir', 'desc')
 
494
        query_params = {
 
495
            'sort_key': params.pop('sort_key', 'created_at'),
 
496
            'sort_dir': self._validate_sort_dir(sort_dir),
 
497
            'filters': self._get_filters(params),
 
498
        }
 
499
 
 
500
        if marker is not None:
 
501
            query_params['marker'] = marker
 
502
 
 
503
        if limit is not None:
 
504
            query_params['limit'] = self._validate_limit(limit)
 
505
 
 
506
        return query_params
 
507
 
 
508
 
 
509
class ResponseSerializer(wsgi.JSONResponseSerializer):
 
510
    def __init__(self, schema=None):
 
511
        super(ResponseSerializer, self).__init__()
 
512
        self.schema = schema or get_schema()
 
513
 
 
514
    def _get_image_href(self, image, subcollection=''):
 
515
        base_href = '/v2/images/%s' % image['id']
 
516
        if subcollection:
 
517
            base_href = '%s/%s' % (base_href, subcollection)
 
518
        return base_href
 
519
 
 
520
    def _get_image_links(self, image):
 
521
        return [
 
522
            {'rel': 'self', 'href': self._get_image_href(image)},
 
523
            {'rel': 'file', 'href': self._get_image_href(image, 'file')},
 
524
            {'rel': 'describedby', 'href': '/v2/schemas/image'},
 
525
        ]
 
526
 
 
527
    def _format_image(self, image):
 
528
        #NOTE(bcwaldon): merge the contained properties dict with the
 
529
        # top-level image object
 
530
        image_view = image['properties']
 
531
        attributes = ['id', 'name', 'disk_format', 'container_format',
 
532
                      'size', 'status', 'checksum', 'tags', 'protected',
 
533
                      'created_at', 'updated_at', 'min_ram', 'min_disk']
 
534
        for key in attributes:
 
535
            image_view[key] = image[key]
 
536
 
 
537
        location = image['location']
 
538
        if CONF.show_image_direct_url and location is not None:
 
539
            image_view['direct_url'] = location
 
540
 
 
541
        visibility = 'public' if image['is_public'] else 'private'
 
542
        image_view['visibility'] = visibility
 
543
 
 
544
        image_view['self'] = self._get_image_href(image)
 
545
        image_view['file'] = self._get_image_href(image, 'file')
 
546
        image_view['schema'] = '/v2/schemas/image'
 
547
 
 
548
        self._serialize_datetimes(image_view)
 
549
        image_view = self.schema.filter(image_view)
 
550
 
 
551
        return image_view
 
552
 
 
553
    @staticmethod
 
554
    def _serialize_datetimes(image):
 
555
        for (key, value) in image.iteritems():
 
556
            if isinstance(value, datetime.datetime):
 
557
                image[key] = timeutils.isotime(value)
 
558
 
 
559
    def create(self, response, image):
 
560
        response.status_int = 201
 
561
        body = json.dumps(self._format_image(image), ensure_ascii=False)
 
562
        response.unicode_body = unicode(body)
 
563
        response.content_type = 'application/json'
 
564
        response.location = self._get_image_href(image)
 
565
 
 
566
    def show(self, response, image):
 
567
        body = json.dumps(self._format_image(image), ensure_ascii=False)
 
568
        response.unicode_body = unicode(body)
 
569
        response.content_type = 'application/json'
 
570
 
 
571
    def update(self, response, image):
 
572
        body = json.dumps(self._format_image(image), ensure_ascii=False)
 
573
        response.unicode_body = unicode(body)
 
574
        response.content_type = 'application/json'
 
575
 
 
576
    def index(self, response, result):
 
577
        params = dict(response.request.params)
 
578
        params.pop('marker', None)
 
579
        query = urllib.urlencode(params)
 
580
        body = {
 
581
               'images': [self._format_image(i) for i in result['images']],
 
582
               'first': '/v2/images',
 
583
               'schema': '/v2/schemas/images',
 
584
        }
 
585
        if query:
 
586
            body['first'] = '%s?%s' % (body['first'], query)
 
587
        if 'next_marker' in result:
 
588
            params['marker'] = result['next_marker']
 
589
            next_query = urllib.urlencode(params)
 
590
            body['next'] = '/v2/images?%s' % next_query
 
591
        response.unicode_body = unicode(json.dumps(body, ensure_ascii=False))
 
592
        response.content_type = 'application/json'
 
593
 
 
594
    def delete(self, response, result):
 
595
        response.status_int = 204
 
596
 
 
597
 
 
598
_BASE_PROPERTIES = {
 
599
    'id': {
 
600
        'type': 'string',
 
601
        'description': 'An identifier for the image',
 
602
        'pattern': ('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}'
 
603
                    '-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$'),
 
604
    },
 
605
    'name': {
 
606
        'type': 'string',
 
607
        'description': 'Descriptive name for the image',
 
608
        'maxLength': 255,
 
609
    },
 
610
    'status': {
 
611
      'type': 'string',
 
612
      'description': 'Status of the image',
 
613
      'enum': ['queued', 'saving', 'active', 'killed',
 
614
               'deleted', 'pending_delete'],
 
615
    },
 
616
    'visibility': {
 
617
        'type': 'string',
 
618
        'description': 'Scope of image accessibility',
 
619
        'enum': ['public', 'private'],
 
620
    },
 
621
    'protected': {
 
622
        'type': 'boolean',
 
623
        'description': 'If true, image will not be deletable.',
 
624
    },
 
625
    'checksum': {
 
626
        'type': 'string',
 
627
        'description': 'md5 hash of image contents.',
 
628
        'type': 'string',
 
629
        'maxLength': 32,
 
630
    },
 
631
    'size': {
 
632
        'type': 'integer',
 
633
        'description': 'Size of image file in bytes',
 
634
    },
 
635
    'container_format': {
 
636
        'type': 'string',
 
637
        'description': '',
 
638
        'type': 'string',
 
639
        'enum': ['bare', 'ovf', 'ami', 'aki', 'ari'],
 
640
    },
 
641
    'disk_format': {
 
642
        'type': 'string',
 
643
        'description': '',
 
644
        'type': 'string',
 
645
        'enum': ['raw', 'vhd', 'vmdk', 'vdi', 'iso', 'qcow2',
 
646
                 'aki', 'ari', 'ami'],
 
647
    },
 
648
    'created_at': {
 
649
        'type': 'string',
 
650
        'description': 'Date and time of image registration',
 
651
        #TODO(bcwaldon): our jsonschema library doesn't seem to like the
 
652
        # format attribute, figure out why!
 
653
        #'format': 'date-time',
 
654
    },
 
655
    'updated_at': {
 
656
        'type': 'string',
 
657
        'description': 'Date and time of the last image modification',
 
658
        #'format': 'date-time',
 
659
    },
 
660
    'tags': {
 
661
        'type': 'array',
 
662
        'description': 'List of strings related to the image',
 
663
        'items': {
 
664
            'type': 'string',
 
665
            'maxLength': 255,
 
666
        },
 
667
    },
 
668
    'direct_url': {
 
669
        'type': 'string',
 
670
        'description': 'URL to access the image file kept in external store',
 
671
    },
 
672
    'min_ram': {
 
673
        'type': 'integer',
 
674
        'description': 'Amount of ram (in MB) required to boot image.',
 
675
    },
 
676
    'min_disk': {
 
677
        'type': 'integer',
 
678
        'description': 'Amount of disk space (in GB) required to boot image.',
 
679
    },
 
680
    'self': {'type': 'string'},
 
681
    'file': {'type': 'string'},
 
682
    'schema': {'type': 'string'},
 
683
}
 
684
 
 
685
_BASE_LINKS = [
 
686
    {'rel': 'self', 'href': '{self}'},
 
687
    {'rel': 'enclosure', 'href': '{file}'},
 
688
    {'rel': 'describedby', 'href': '{schema}'},
 
689
]
 
690
 
 
691
 
 
692
def get_schema(custom_properties=None):
 
693
    properties = copy.deepcopy(_BASE_PROPERTIES)
 
694
    links = copy.deepcopy(_BASE_LINKS)
 
695
    if CONF.allow_additional_image_properties:
 
696
        schema = glance.schema.PermissiveSchema('image', properties, links)
 
697
    else:
 
698
        schema = glance.schema.Schema('image', properties)
 
699
    schema.merge_properties(custom_properties or {})
 
700
    return schema
 
701
 
 
702
 
 
703
def get_collection_schema(custom_properties=None):
 
704
    image_schema = get_schema(custom_properties)
 
705
    return glance.schema.CollectionSchema('images', image_schema)
 
706
 
 
707
 
 
708
def load_custom_properties():
 
709
    """Find the schema properties files and load them into a dict."""
 
710
    filename = 'schema-image.json'
 
711
    match = CONF.find_file(filename)
 
712
    if match:
 
713
        schema_file = open(match)
 
714
        schema_data = schema_file.read()
 
715
        return json.loads(schema_data)
 
716
    else:
 
717
        msg = _('Could not find schema properties file %s. Continuing '
 
718
                'without custom properties')
 
719
        LOG.warn(msg % filename)
 
720
        return {}
 
721
 
 
722
 
 
723
def create_resource(custom_properties=None):
 
724
    """Images resource factory method"""
 
725
    schema = get_schema(custom_properties)
 
726
    deserializer = RequestDeserializer(schema)
 
727
    serializer = ResponseSerializer(schema)
 
728
    controller = ImagesController()
 
729
    return wsgi.Resource(controller, deserializer, serializer)