~jaypipes/glance/bug759018

« back to all changes in this revision

Viewing changes to glance/server.py

  • Committer: Tarmac
  • Author(s): jaypipes at gmail
  • Date: 2011-03-25 19:17:35 UTC
  • mfrom: (75.3.13 checksum)
  • Revision ID: tarmac-20110325191735-rk1wy065cloviubu
Adds checksumming to Glance.

When adding an image (or uploading an image during PUT operations),
you may now supply an optional X-Image-Meta-Checksum header. When
storing the uploaded image, the backend image stores now are required
to return a checksum of the data they just stored. The optional
X-Image-Meta-Checksum header is compared against this generated checksum
and returns a 409 Bad Request if there is a mismatch.

The ETag header is now properly set to the image's checksum now
for all GET /images/<ID>, HEAD /images/<ID>, POST /images and
PUT /images/<ID> operations.

Adds unit tests verifying the checksumming behaviour in the API, and
in the Swift and Filesystem backend stores.

Includes migration script.

NOTE: This does not include the DB migration script. Separate bug will be filed for that.

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
 
30
30
"""
31
31
 
 
32
import httplib
32
33
import json
33
34
import logging
34
35
import sys
68
69
        GET /images/<ID> -- Return image data for image with id <ID>
69
70
        POST /images -- Store image data and return metadata about the
70
71
                        newly-stored image
71
 
        PUT /images/<ID> -- Update image metadata (not image data, since
72
 
                            image data is immutable once stored)
 
72
        PUT /images/<ID> -- Update image metadata and/or upload image
 
73
                            data for a previously-reserved image
73
74
        DELETE /images/<ID> -- Delete the image with id <ID>
74
75
    """
75
76
 
82
83
 
83
84
            * id -- The opaque image identifier
84
85
            * name -- The name of the image
 
86
            * disk_format -- The disk image format
 
87
            * container_format -- The "container" format of the image
 
88
            * checksum -- MD5 checksum of the image data
85
89
            * size -- Size of image data in bytes
86
90
 
87
91
        :param request: The WSGI/Webob Request object
92
96
                 'name': <NAME>,
93
97
                 'disk_format': <DISK_FORMAT>,
94
98
                 'container_format': <DISK_FORMAT>,
 
99
                 'checksum': <CHECKSUM>
95
100
                 'size': <SIZE>}, ...
96
101
            ]}
97
102
        """
111
116
                 'size': <SIZE>,
112
117
                 'disk_format': <DISK_FORMAT>,
113
118
                 'container_format': <CONTAINER_FORMAT>,
 
119
                 'checksum': <CHECKSUM>,
114
120
                 'store': <STORE>,
115
121
                 'status': <STATUS>,
116
122
                 'created_at': <TIMESTAMP>,
136
142
 
137
143
        res = Response(request=req)
138
144
        utils.inject_image_meta_into_headers(res, image)
 
145
        res.headers.add('Location', "/images/%s" % id)
 
146
        res.headers.add('ETag', image['checksum'])
139
147
 
140
148
        return req.get_response(res)
141
149
 
167
175
        # Using app_iter blanks content-length, so we set it here...
168
176
        res.headers.add('Content-Length', image['size'])
169
177
        utils.inject_image_meta_into_headers(res, image)
 
178
        res.headers.add('Location', "/images/%s" % id)
 
179
        res.headers.add('ETag', image['checksum'])
170
180
        return req.get_response(res)
171
181
 
172
182
    def _reserve(self, req):
227
237
 
228
238
        store = self.get_store_or_400(req, store_name)
229
239
 
230
 
        image_meta['status'] = 'saving'
231
240
        image_id = image_meta['id']
232
 
        logger.debug("Updating image metadata for image %s"
 
241
        logger.debug("Setting image %s to status 'saving'"
233
242
                     % image_id)
234
 
        registry.update_image_metadata(self.options,
235
 
                                       image_meta['id'],
236
 
                                       image_meta)
237
 
 
 
243
        registry.update_image_metadata(self.options, image_id,
 
244
                                       {'status': 'saving'})
238
245
        try:
239
246
            logger.debug("Uploading image data for image %(image_id)s "
240
247
                         "to %(store_name)s store" % locals())
241
 
            location, size = store.add(image_meta['id'],
242
 
                                       req.body_file,
243
 
                                       self.options)
244
 
            # If size returned from store is different from size
245
 
            # already stored in registry, update the registry with
246
 
            # the new size of the image
247
 
            if image_meta.get('size', 0) != size:
248
 
                image_meta['size'] = size
249
 
                logger.debug("Updating image metadata for image %s"
250
 
                             % image_id)
251
 
                registry.update_image_metadata(self.options,
252
 
                                               image_meta['id'],
253
 
                                               image_meta)
 
248
            location, size, checksum = store.add(image_meta['id'],
 
249
                                                 req.body_file,
 
250
                                                 self.options)
 
251
 
 
252
            # Verify any supplied checksum value matches checksum
 
253
            # returned from store when adding image
 
254
            supplied_checksum = image_meta.get('checksum')
 
255
            if supplied_checksum and supplied_checksum != checksum:
 
256
                msg = ("Supplied checksum (%(supplied_checksum)s) and "
 
257
                       "checksum generated from uploaded image "
 
258
                       "(%(checksum)s) did not match. Setting image "
 
259
                       "status to 'killed'.") % locals()
 
260
                self._safe_kill(req, image_meta)
 
261
                raise HTTPBadRequest(msg, content_type="text/plain",
 
262
                                     request=req)
 
263
 
 
264
            # Update the database with the checksum returned
 
265
            # from the backend store
 
266
            logger.debug("Updating image %(image_id)s data. "
 
267
                         "Checksum set to %(checksum)s, size set "
 
268
                         "to %(size)d" % locals())
 
269
            registry.update_image_metadata(self.options, image_id,
 
270
                                           {'checksum': checksum,
 
271
                                            'size': size})
 
272
 
254
273
            return location
255
274
        except exception.Duplicate, e:
256
275
            logger.error("Error adding image to store: %s", str(e))
257
276
            raise HTTPConflict(str(e), request=req)
258
277
 
259
 
    def _activate(self, req, image_meta, location):
 
278
    def _activate(self, req, image_id, location):
260
279
        """
261
280
        Sets the image status to `active` and the image's location
262
281
        attribute.
265
284
        :param image_meta: Mapping of metadata about image
266
285
        :param location: Location of where Glance stored this image
267
286
        """
 
287
        image_meta = {}
268
288
        image_meta['location'] = location
269
289
        image_meta['status'] = 'active'
270
 
        registry.update_image_metadata(self.options,
271
 
                                       image_meta['id'],
 
290
        return registry.update_image_metadata(self.options,
 
291
                                       image_id,
272
292
                                       image_meta)
273
293
 
274
 
    def _kill(self, req, image_meta):
 
294
    def _kill(self, req, image_id):
275
295
        """
276
296
        Marks the image status to `killed`
277
297
 
278
298
        :param request: The WSGI/Webob Request object
279
 
        :param image_meta: Mapping of metadata about image
 
299
        :param image_id: Opaque image identifier
280
300
        """
281
 
        image_meta['status'] = 'killed'
282
301
        registry.update_image_metadata(self.options,
283
 
                                       image_meta['id'],
284
 
                                       image_meta)
 
302
                                       image_id,
 
303
                                       {'status': 'killed'})
285
304
 
286
 
    def _safe_kill(self, req, image_meta):
 
305
    def _safe_kill(self, req, image_id):
287
306
        """
288
307
        Mark image killed without raising exceptions if it fails.
289
308
 
291
310
        not raise itself, rather it should just log its error.
292
311
 
293
312
        :param request: The WSGI/Webob Request object
 
313
        :param image_id: Opaque image identifier
294
314
        """
295
315
        try:
296
 
            self._kill(req, image_meta)
 
316
            self._kill(req, image_id)
297
317
        except Exception, e:
298
318
            logger.error("Unable to kill image %s: %s",
299
 
                          image_meta['id'], repr(e))
 
319
                          image_id, repr(e))
300
320
 
301
321
    def _upload_and_activate(self, req, image_meta):
302
322
        """
306
326
 
307
327
        :param request: The WSGI/Webob Request object
308
328
        :param image_meta: Mapping of metadata about image
 
329
 
 
330
        :retval Mapping of updated image data
309
331
        """
310
332
        try:
 
333
            image_id = image_meta['id']
311
334
            location = self._upload(req, image_meta)
312
 
            self._activate(req, image_meta, location)
 
335
            return self._activate(req, image_id, location)
313
336
        except:  # unqualified b/c we're re-raising it
314
337
            exc_type, exc_value, exc_traceback = sys.exc_info()
315
 
            self._safe_kill(req, image_meta)
 
338
            self._safe_kill(req, image_id)
316
339
            # NOTE(sirp): _safe_kill uses httplib which, in turn, uses
317
340
            # Eventlet's GreenSocket. Eventlet subsequently clears exceptions
318
341
            # by calling `sys.exc_clear()`.
356
379
                image data.
357
380
        """
358
381
        image_meta = self._reserve(req)
 
382
        image_id = image_meta['id']
359
383
 
360
384
        if utils.has_body(req):
361
 
            self._upload_and_activate(req, image_meta)
 
385
            image_meta = self._upload_and_activate(req, image_meta)
362
386
        else:
363
387
            if 'x-image-meta-location' in req.headers:
364
388
                location = req.headers['x-image-meta-location']
365
 
                self._activate(req, image_meta, location)
 
389
                image_meta = self._activate(req, image_id, location)
366
390
 
367
391
        # APP states we should return a Location: header with the edit
368
392
        # URI of the resource newly-created.
369
393
        res = Response(request=req, body=json.dumps(dict(image=image_meta)),
370
 
                       content_type="text/plain")
371
 
        res.headers.add('Location', "/images/%s" % image_meta['id'])
 
394
                       status=httplib.CREATED, content_type="text/plain")
 
395
        res.headers.add('Location', "/images/%s" % image_id)
 
396
        res.headers.add('ETag', image_meta['checksum'])
372
397
 
373
398
        return req.get_response(res)
374
399
 
395
420
                                                        id,
396
421
                                                        new_image_meta)
397
422
            if has_body:
398
 
                self._upload_and_activate(req, image_meta)
 
423
                image_meta = self._upload_and_activate(req, image_meta)
399
424
 
400
 
            return dict(image=image_meta)
 
425
            res = Response(request=req,
 
426
                           body=json.dumps(dict(image=image_meta)),
 
427
                           content_type="text/plain")
 
428
            res.headers.add('Location', "/images/%s" % id)
 
429
            res.headers.add('ETag', image_meta['checksum'])
 
430
            return res
401
431
        except exception.Invalid, e:
402
432
            msg = ("Failed to update image metadata. Got error: %(e)s"
403
433
                   % locals())