Merge lp:~jaypipes/glance/checksum into lp:~glance-coresec/glance/cactus-trunk
- checksum
- Merge into cactus-trunk
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 88 |
Merged at revision: | 97 |
Proposed branch: | lp:~jaypipes/glance/checksum |
Merge into: | lp:~glance-coresec/glance/cactus-trunk |
Prerequisite: | lp:~jaypipes/glance/bug730213 |
Diff against target: |
1005 lines (+351/-129) 16 files modified
doc/source/glanceapi.rst (+20/-0) glance/client.py (+4/-1) glance/registry/db/api.py (+1/-1) glance/registry/db/migrate_repo/versions/004_add_checksum.py (+80/-0) glance/registry/db/models.py (+1/-0) glance/registry/server.py (+21/-8) glance/server.py (+70/-40) glance/store/__init__.py (+0/-37) glance/store/filesystem.py (+12/-21) glance/store/swift.py (+1/-1) tests/functional/__init__.py (+2/-2) tests/functional/test_curl_api.py (+2/-1) tests/stubs.py (+6/-1) tests/unit/test_api.py (+103/-5) tests/unit/test_filesystem_store.py (+11/-5) tests/unit/test_swift_store.py (+17/-6) |
To merge this branch: | bzr merge lp:~jaypipes/glance/checksum |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Cory Wright (community) | Approve | ||
Jay Pipes (community) | Approve | ||
Rick Harris (community) | Approve | ||
Thierry Carrez (community) | ffe | Approve | |
Sandy Walsh (community) | Approve | ||
Chuck Thier | Pending | ||
Review via email: mp+54537@code.launchpad.net |
This proposal supersedes a proposal from 2011-03-08.
Commit message
Description of the change
Adds checksumming to Glance.
When adding an image (or uploading an image during PUT operations),
you may now supply an optional X-Image-
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-
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.
Chuck Thier (cthier) wrote : Posted in a previous version of this proposal | # |
Jay Pipes (jaypipes) wrote : Posted in a previous version of this proposal | # |
thx Chuck. Made a note in the docs about the checksum being an MD5 checksum.
Rick Harris (rconradharris) wrote : Posted in a previous version of this proposal | # |
Nice patch.
Unfortunately with the state of Nova <=> Glance trunks at the moment, I wasn't able to test functionally. That said, this seems pretty safe to go ahead and merge, and if we have issues, we can fix them along with all the other fixes that are in progress at the moment :)
> 110 + checksum = Column(String(32))
I assume the answer is YAGNI, but I'll ask it anyway: Will we ever allow SHA1 or SHA256?
Would it make sense to make this a String(64) or, heck, a String(255) for flexibility?
Jay Pipes (jaypipes) wrote : Posted in a previous version of this proposal | # |
> > 110 + checksum = Column(String(32))
>
> I assume the answer is YAGNI, but I'll ask it anyway: Will we ever allow SHA1
> or SHA256?
>
> Would it make sense to make this a String(64) or, heck, a String(255) for
> flexibility?
Hmm, good question. Not sure, really. I suppose we could expand it in the future if we decide its a needed feature.
Cory Wright (corywright) wrote : Posted in a previous version of this proposal | # |
I noticed that there is no migration script to add the checksum column. Trunk is already missing a migration for disk_format/
I tried merging trunk into this branch to test these changes with the glance cli tool, but there were conflicts so you may want to merge trunk once more.
Otherwise, this looks good to me. I'm going to mark it as Needs Fixing because of the missing migration. If you think it's ok to merge this to trunk without the migration then let me know and I'll mark this approved.
Jay Pipes (jaypipes) wrote : Posted in a previous version of this proposal | # |
> I noticed that there is no migration script to add the checksum column. Trunk
> is already missing a migration for disk_format/
> probably shouldn't get in the habit of merging to trunk without migrations,
> since that effectively leaves trunk broken. However, the tests seem to run
> fine without the migration.
>
> I tried merging trunk into this branch to test these changes with the glance
> cli tool, but there were conflicts so you may want to merge trunk once more.
>
> Otherwise, this looks good to me. I'm going to mark it as Needs Fixing
> because of the missing migration. If you think it's ok to merge this to trunk
> without the migration then let me know and I'll mark this approved.
Agreed, though I did note this in the merge proposal commit message:
"NOTE: This does not include the DB migration script. Separate bug will be filed for that."
But, I'll work on a migrate script after merging trunk again. Thanks, Cory!
Cory Wright (corywright) wrote : Posted in a previous version of this proposal | # |
You sure did mention it, sorry about that. I read the commit message yesterday when I first looked at this and forgot.
Sandy Walsh (sandy-walsh) wrote : Posted in a previous version of this proposal | # |
Hmm, I got bitten by this migration bug yesterday ... can't this be put in this branch to keep trunk happy?
Jay Pipes (jaypipes) wrote : Posted in a previous version of this proposal | # |
> Hmm, I got bitten by this migration bug yesterday ... can't this be put in
> this branch to keep trunk happy?
? This hasn't been merged yet, so I'm unclear how this could have bitten you :)
FYI, I've now figured out the migrations stuff. Full steam ahead today on getting the image format migration bug done and getting this branch merged with a migration file.
Sandy Walsh (sandy-walsh) wrote : Posted in a previous version of this proposal | # |
sorry, it was a nova issue. _get_kernel_
I guess that just stresses the importance of having the migrations in place with the branch :)
Minor tweak ... ping me and I'll approve immediately after
278 missing _() ?
Jay Pipes (jaypipes) wrote : Posted in a previous version of this proposal | # |
On Wed, Mar 23, 2011 at 10:26 AM, Sandy Walsh <email address hidden> wrote:
> Review: Needs Fixing
> sorry, it was a nova issue. _get_kernel_
>
> I guess that just stresses the importance of having the migrations in place with the branch :)
Ya, I'm currently working on the migration script. should be done shortly...
> Minor tweak ... ping me and I'll approve immediately after
>
> 278 missing _() ?
Glance doesn't have i18n. Yet :)
Sandy Walsh (sandy-walsh) wrote : Posted in a previous version of this proposal | # |
ah! haha
Sandy Walsh (sandy-walsh) wrote : | # |
second time's a charm
Thierry Carrez (ttx) wrote : | # |
Essential feature. FFE preapproved for late merging until Tuesday EOD
Rick Harris (rconradharris) wrote : | # |
Really nice job, Jay. Very clean implementation, nice test coverage, and great docs. Bravo.
Jay Pipes (jaypipes) wrote : | # |
approving the stop_servers() fix.. looks like rick approved the MP right before that fix changed the diff...
Cory Wright (corywright) wrote : | # |
Looks great and is working for me. Nice work, Jay!
Preview Diff
1 | === modified file 'doc/source/glanceapi.rst' |
2 | --- doc/source/glanceapi.rst 2011-03-05 17:04:43 +0000 |
3 | +++ doc/source/glanceapi.rst 2011-03-25 19:01:10 +0000 |
4 | @@ -67,6 +67,7 @@ |
5 | 'disk_format': 'vhd', |
6 | 'container_format': 'ovf', |
7 | 'size': '5368709120', |
8 | + 'checksum': 'c2e5db72bd7fd153f53ede5da5a06de3', |
9 | 'location': 'swift://account:key/container/image.tar.gz.0', |
10 | 'created_at': '2010-02-03 09:34:01', |
11 | 'updated_at': '2010-02-03 09:34:01', |
12 | @@ -89,6 +90,8 @@ |
13 | The `properties` field is a mapping of free-form key/value pairs that |
14 | have been saved with the image metadata |
15 | |
16 | + The `checksum` field is an MD5 checksum of the image file data |
17 | + |
18 | |
19 | Requesting Detailed Metadata on a Specific Image |
20 | ------------------------------------------------ |
21 | @@ -116,6 +119,7 @@ |
22 | x-image-meta-disk-format vhd |
23 | x-image-meta-container-format ovf |
24 | x-image-meta-size 5368709120 |
25 | + x-image-meta-checksum c2e5db72bd7fd153f53ede5da5a06de3 |
26 | x-image-meta-location swift://account:key/container/image.tar.gz.0 |
27 | x-image-meta-created_at 2010-02-03 09:34:01 |
28 | x-image-meta-updated_at 2010-02-03 09:34:01 |
29 | @@ -137,6 +141,9 @@ |
30 | that have been saved with the image metadata. The key is the string |
31 | after `x-image-meta-property-` and the value is the value of the header |
32 | |
33 | + The response's `ETag` header will always be equal to the |
34 | + `x-image-meta-checksum` value |
35 | + |
36 | |
37 | Retrieving a Virtual Machine Image |
38 | ---------------------------------- |
39 | @@ -166,6 +173,7 @@ |
40 | x-image-meta-disk-format vhd |
41 | x-image-meta-container-format ovf |
42 | x-image-meta-size 5368709120 |
43 | + x-image-meta-checksum c2e5db72bd7fd153f53ede5da5a06de3 |
44 | x-image-meta-location swift://account:key/container/image.tar.gz.0 |
45 | x-image-meta-created_at 2010-02-03 09:34:01 |
46 | x-image-meta-updated_at 2010-02-03 09:34:01 |
47 | @@ -190,6 +198,9 @@ |
48 | The response's `Content-Length` header shall be equal to the value of |
49 | the `x-image-meta-size` header |
50 | |
51 | + The response's `ETag` header will always be equal to the |
52 | + `x-image-meta-checksum` value |
53 | + |
54 | The image data itself will be the body of the HTTP response returned |
55 | from the request, which will have content-type of |
56 | `application/octet-stream`. |
57 | @@ -284,6 +295,15 @@ |
58 | When not present, Glance will calculate the image's size based on the size |
59 | of the request body. |
60 | |
61 | +* ``x-image-meta-checksum`` |
62 | + |
63 | + This header is optional. When present it shall be the expected **MD5** |
64 | + checksum of the image file data. |
65 | + |
66 | + When present, Glance will verify the checksum generated from the backend |
67 | + store when storing your image against this value and return a |
68 | + **400 Bad Request** if the values do not match. |
69 | + |
70 | * ``x-image-meta-is-public`` |
71 | |
72 | This header is optional. |
73 | |
74 | === modified file 'glance/client.py' |
75 | --- glance/client.py 2011-03-06 17:02:44 +0000 |
76 | +++ glance/client.py 2011-03-25 19:01:10 +0000 |
77 | @@ -142,7 +142,10 @@ |
78 | c.request(method, action, body, headers) |
79 | res = c.getresponse() |
80 | status_code = self.get_status_code(res) |
81 | - if status_code == httplib.OK: |
82 | + if status_code in (httplib.OK, |
83 | + httplib.CREATED, |
84 | + httplib.ACCEPTED, |
85 | + httplib.NO_CONTENT): |
86 | return res |
87 | elif status_code == httplib.UNAUTHORIZED: |
88 | raise exception.NotAuthorized |
89 | |
90 | === modified file 'glance/registry/db/api.py' |
91 | --- glance/registry/db/api.py 2011-03-14 19:10:24 +0000 |
92 | +++ glance/registry/db/api.py 2011-03-25 19:01:10 +0000 |
93 | @@ -43,7 +43,7 @@ |
94 | |
95 | IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size', |
96 | 'disk_format', 'container_format', |
97 | - 'is_public', 'location']) |
98 | + 'is_public', 'location', 'checksum']) |
99 | |
100 | CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf'] |
101 | DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi'] |
102 | |
103 | === added file 'glance/registry/db/migrate_repo/versions/004_add_checksum.py' |
104 | --- glance/registry/db/migrate_repo/versions/004_add_checksum.py 1970-01-01 00:00:00 +0000 |
105 | +++ glance/registry/db/migrate_repo/versions/004_add_checksum.py 2011-03-25 19:01:10 +0000 |
106 | @@ -0,0 +1,80 @@ |
107 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
108 | + |
109 | +# Copyright 2011 OpenStack LLC. |
110 | +# All Rights Reserved. |
111 | +# |
112 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
113 | +# not use this file except in compliance with the License. You may obtain |
114 | +# a copy of the License at |
115 | +# |
116 | +# http://www.apache.org/licenses/LICENSE-2.0 |
117 | +# |
118 | +# Unless required by applicable law or agreed to in writing, software |
119 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
120 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
121 | +# License for the specific language governing permissions and limitations |
122 | +# under the License. |
123 | + |
124 | +from migrate.changeset import * |
125 | +from sqlalchemy import * |
126 | +from sqlalchemy.sql import and_, not_ |
127 | + |
128 | +from glance.registry.db.migrate_repo.schema import ( |
129 | + Boolean, DateTime, Integer, String, Text, from_migration_import) |
130 | + |
131 | + |
132 | +def get_images_table(meta): |
133 | + """ |
134 | + Returns the Table object for the images table that |
135 | + corresponds to the images table definition of this version. |
136 | + """ |
137 | + images = Table('images', meta, |
138 | + Column('id', Integer(), primary_key=True, nullable=False), |
139 | + Column('name', String(255)), |
140 | + Column('disk_format', String(20)), |
141 | + Column('container_format', String(20)), |
142 | + Column('size', Integer()), |
143 | + Column('status', String(30), nullable=False), |
144 | + Column('is_public', Boolean(), nullable=False, default=False, |
145 | + index=True), |
146 | + Column('location', Text()), |
147 | + Column('created_at', DateTime(), nullable=False), |
148 | + Column('updated_at', DateTime()), |
149 | + Column('deleted_at', DateTime()), |
150 | + Column('deleted', Boolean(), nullable=False, default=False, |
151 | + index=True), |
152 | + Column('checksum', String(32)), |
153 | + mysql_engine='InnoDB', |
154 | + useexisting=True) |
155 | + |
156 | + return images |
157 | + |
158 | + |
159 | +def get_image_properties_table(meta): |
160 | + """ |
161 | + No changes to the image properties table from 002... |
162 | + """ |
163 | + (define_image_properties_table,) = from_migration_import( |
164 | + '002_add_image_properties_table', ['define_image_properties_table']) |
165 | + |
166 | + image_properties = define_image_properties_table(meta) |
167 | + return image_properties |
168 | + |
169 | + |
170 | +def upgrade(migrate_engine): |
171 | + meta = MetaData() |
172 | + meta.bind = migrate_engine |
173 | + |
174 | + images = get_images_table(meta) |
175 | + |
176 | + checksum = Column('checksum', String(32)) |
177 | + checksum.create(images) |
178 | + |
179 | + |
180 | +def downgrade(migrate_engine): |
181 | + meta = MetaData() |
182 | + meta.bind = migrate_engine |
183 | + |
184 | + images = get_images_table(meta) |
185 | + |
186 | + images.columns['checksum'].drop() |
187 | |
188 | === modified file 'glance/registry/db/models.py' |
189 | --- glance/registry/db/models.py 2011-03-05 17:04:43 +0000 |
190 | +++ glance/registry/db/models.py 2011-03-25 19:01:10 +0000 |
191 | @@ -104,6 +104,7 @@ |
192 | status = Column(String(30), nullable=False) |
193 | is_public = Column(Boolean, nullable=False, default=False) |
194 | location = Column(Text) |
195 | + checksum = Column(String(32)) |
196 | |
197 | |
198 | class ImageProperty(BASE, ModelBase): |
199 | |
200 | === modified file 'glance/registry/server.py' |
201 | --- glance/registry/server.py 2011-02-25 16:52:12 +0000 |
202 | +++ glance/registry/server.py 2011-03-25 19:01:10 +0000 |
203 | @@ -14,8 +14,9 @@ |
204 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
205 | # License for the specific language governing permissions and limitations |
206 | # under the License. |
207 | + |
208 | """ |
209 | -Parllax Image controller |
210 | +Reference implementation registry server WSGI controller |
211 | """ |
212 | |
213 | import json |
214 | @@ -31,6 +32,10 @@ |
215 | |
216 | logger = logging.getLogger('glance.registry.server') |
217 | |
218 | +DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size', |
219 | + 'disk_format', 'container_format', |
220 | + 'checksum'] |
221 | + |
222 | |
223 | class Controller(wsgi.Controller): |
224 | """Controller for the reference implementation registry server""" |
225 | @@ -49,16 +54,24 @@ |
226 | |
227 | Where image_list is a sequence of mappings:: |
228 | |
229 | - {'id': image_id, 'name': image_name} |
230 | + { |
231 | + 'id': <ID>, |
232 | + 'name': <NAME>, |
233 | + 'size': <SIZE>, |
234 | + 'disk_format': <DISK_FORMAT>, |
235 | + 'container_format': <CONTAINER_FORMAT>, |
236 | + 'checksum': <CHECKSUM> |
237 | + } |
238 | |
239 | """ |
240 | images = db_api.image_get_all_public(None) |
241 | - image_dicts = [dict(id=i['id'], |
242 | - name=i['name'], |
243 | - disk_format=i['disk_format'], |
244 | - container_format=i['container_format'], |
245 | - size=i['size']) for i in images] |
246 | - return dict(images=image_dicts) |
247 | + results = [] |
248 | + for image in images: |
249 | + result = {} |
250 | + for field in DISPLAY_FIELDS_IN_INDEX: |
251 | + result[field] = image[field] |
252 | + results.append(result) |
253 | + return dict(images=results) |
254 | |
255 | def detail(self, req): |
256 | """Return detailed information for all public, non-deleted images |
257 | |
258 | === modified file 'glance/server.py' |
259 | --- glance/server.py 2011-03-24 23:03:17 +0000 |
260 | +++ glance/server.py 2011-03-25 19:01:10 +0000 |
261 | @@ -29,6 +29,7 @@ |
262 | |
263 | """ |
264 | |
265 | +import httplib |
266 | import json |
267 | import logging |
268 | import sys |
269 | @@ -68,8 +69,8 @@ |
270 | GET /images/<ID> -- Return image data for image with id <ID> |
271 | POST /images -- Store image data and return metadata about the |
272 | newly-stored image |
273 | - PUT /images/<ID> -- Update image metadata (not image data, since |
274 | - image data is immutable once stored) |
275 | + PUT /images/<ID> -- Update image metadata and/or upload image |
276 | + data for a previously-reserved image |
277 | DELETE /images/<ID> -- Delete the image with id <ID> |
278 | """ |
279 | |
280 | @@ -82,6 +83,9 @@ |
281 | |
282 | * id -- The opaque image identifier |
283 | * name -- The name of the image |
284 | + * disk_format -- The disk image format |
285 | + * container_format -- The "container" format of the image |
286 | + * checksum -- MD5 checksum of the image data |
287 | * size -- Size of image data in bytes |
288 | |
289 | :param request: The WSGI/Webob Request object |
290 | @@ -92,6 +96,7 @@ |
291 | 'name': <NAME>, |
292 | 'disk_format': <DISK_FORMAT>, |
293 | 'container_format': <DISK_FORMAT>, |
294 | + 'checksum': <CHECKSUM> |
295 | 'size': <SIZE>}, ... |
296 | ]} |
297 | """ |
298 | @@ -111,6 +116,7 @@ |
299 | 'size': <SIZE>, |
300 | 'disk_format': <DISK_FORMAT>, |
301 | 'container_format': <CONTAINER_FORMAT>, |
302 | + 'checksum': <CHECKSUM>, |
303 | 'store': <STORE>, |
304 | 'status': <STATUS>, |
305 | 'created_at': <TIMESTAMP>, |
306 | @@ -136,6 +142,8 @@ |
307 | |
308 | res = Response(request=req) |
309 | utils.inject_image_meta_into_headers(res, image) |
310 | + res.headers.add('Location', "/images/%s" % id) |
311 | + res.headers.add('ETag', image['checksum']) |
312 | |
313 | return req.get_response(res) |
314 | |
315 | @@ -167,6 +175,8 @@ |
316 | # Using app_iter blanks content-length, so we set it here... |
317 | res.headers.add('Content-Length', image['size']) |
318 | utils.inject_image_meta_into_headers(res, image) |
319 | + res.headers.add('Location', "/images/%s" % id) |
320 | + res.headers.add('ETag', image['checksum']) |
321 | return req.get_response(res) |
322 | |
323 | def _reserve(self, req): |
324 | @@ -227,36 +237,45 @@ |
325 | |
326 | store = self.get_store_or_400(req, store_name) |
327 | |
328 | - image_meta['status'] = 'saving' |
329 | image_id = image_meta['id'] |
330 | - logger.debug("Updating image metadata for image %s" |
331 | + logger.debug("Setting image %s to status 'saving'" |
332 | % image_id) |
333 | - registry.update_image_metadata(self.options, |
334 | - image_meta['id'], |
335 | - image_meta) |
336 | - |
337 | + registry.update_image_metadata(self.options, image_id, |
338 | + {'status': 'saving'}) |
339 | try: |
340 | logger.debug("Uploading image data for image %(image_id)s " |
341 | "to %(store_name)s store" % locals()) |
342 | - location, size = store.add(image_meta['id'], |
343 | - req.body_file, |
344 | - self.options) |
345 | - # If size returned from store is different from size |
346 | - # already stored in registry, update the registry with |
347 | - # the new size of the image |
348 | - if image_meta.get('size', 0) != size: |
349 | - image_meta['size'] = size |
350 | - logger.debug("Updating image metadata for image %s" |
351 | - % image_id) |
352 | - registry.update_image_metadata(self.options, |
353 | - image_meta['id'], |
354 | - image_meta) |
355 | + location, size, checksum = store.add(image_meta['id'], |
356 | + req.body_file, |
357 | + self.options) |
358 | + |
359 | + # Verify any supplied checksum value matches checksum |
360 | + # returned from store when adding image |
361 | + supplied_checksum = image_meta.get('checksum') |
362 | + if supplied_checksum and supplied_checksum != checksum: |
363 | + msg = ("Supplied checksum (%(supplied_checksum)s) and " |
364 | + "checksum generated from uploaded image " |
365 | + "(%(checksum)s) did not match. Setting image " |
366 | + "status to 'killed'.") % locals() |
367 | + self._safe_kill(req, image_meta) |
368 | + raise HTTPBadRequest(msg, content_type="text/plain", |
369 | + request=req) |
370 | + |
371 | + # Update the database with the checksum returned |
372 | + # from the backend store |
373 | + logger.debug("Updating image %(image_id)s data. " |
374 | + "Checksum set to %(checksum)s, size set " |
375 | + "to %(size)d" % locals()) |
376 | + registry.update_image_metadata(self.options, image_id, |
377 | + {'checksum': checksum, |
378 | + 'size': size}) |
379 | + |
380 | return location |
381 | except exception.Duplicate, e: |
382 | logger.error("Error adding image to store: %s", str(e)) |
383 | raise HTTPConflict(str(e), request=req) |
384 | |
385 | - def _activate(self, req, image_meta, location): |
386 | + def _activate(self, req, image_id, location): |
387 | """ |
388 | Sets the image status to `active` and the image's location |
389 | attribute. |
390 | @@ -265,25 +284,25 @@ |
391 | :param image_meta: Mapping of metadata about image |
392 | :param location: Location of where Glance stored this image |
393 | """ |
394 | + image_meta = {} |
395 | image_meta['location'] = location |
396 | image_meta['status'] = 'active' |
397 | - registry.update_image_metadata(self.options, |
398 | - image_meta['id'], |
399 | + return registry.update_image_metadata(self.options, |
400 | + image_id, |
401 | image_meta) |
402 | |
403 | - def _kill(self, req, image_meta): |
404 | + def _kill(self, req, image_id): |
405 | """ |
406 | Marks the image status to `killed` |
407 | |
408 | :param request: The WSGI/Webob Request object |
409 | - :param image_meta: Mapping of metadata about image |
410 | + :param image_id: Opaque image identifier |
411 | """ |
412 | - image_meta['status'] = 'killed' |
413 | registry.update_image_metadata(self.options, |
414 | - image_meta['id'], |
415 | - image_meta) |
416 | + image_id, |
417 | + {'status': 'killed'}) |
418 | |
419 | - def _safe_kill(self, req, image_meta): |
420 | + def _safe_kill(self, req, image_id): |
421 | """ |
422 | Mark image killed without raising exceptions if it fails. |
423 | |
424 | @@ -291,12 +310,13 @@ |
425 | not raise itself, rather it should just log its error. |
426 | |
427 | :param request: The WSGI/Webob Request object |
428 | + :param image_id: Opaque image identifier |
429 | """ |
430 | try: |
431 | - self._kill(req, image_meta) |
432 | + self._kill(req, image_id) |
433 | except Exception, e: |
434 | logger.error("Unable to kill image %s: %s", |
435 | - image_meta['id'], repr(e)) |
436 | + image_id, repr(e)) |
437 | |
438 | def _upload_and_activate(self, req, image_meta): |
439 | """ |
440 | @@ -306,13 +326,16 @@ |
441 | |
442 | :param request: The WSGI/Webob Request object |
443 | :param image_meta: Mapping of metadata about image |
444 | + |
445 | + :retval Mapping of updated image data |
446 | """ |
447 | try: |
448 | + image_id = image_meta['id'] |
449 | location = self._upload(req, image_meta) |
450 | - self._activate(req, image_meta, location) |
451 | + return self._activate(req, image_id, location) |
452 | except: # unqualified b/c we're re-raising it |
453 | exc_type, exc_value, exc_traceback = sys.exc_info() |
454 | - self._safe_kill(req, image_meta) |
455 | + self._safe_kill(req, image_id) |
456 | # NOTE(sirp): _safe_kill uses httplib which, in turn, uses |
457 | # Eventlet's GreenSocket. Eventlet subsequently clears exceptions |
458 | # by calling `sys.exc_clear()`. |
459 | @@ -356,19 +379,21 @@ |
460 | image data. |
461 | """ |
462 | image_meta = self._reserve(req) |
463 | + image_id = image_meta['id'] |
464 | |
465 | if utils.has_body(req): |
466 | - self._upload_and_activate(req, image_meta) |
467 | + image_meta = self._upload_and_activate(req, image_meta) |
468 | else: |
469 | if 'x-image-meta-location' in req.headers: |
470 | location = req.headers['x-image-meta-location'] |
471 | - self._activate(req, image_meta, location) |
472 | + image_meta = self._activate(req, image_id, location) |
473 | |
474 | # APP states we should return a Location: header with the edit |
475 | # URI of the resource newly-created. |
476 | res = Response(request=req, body=json.dumps(dict(image=image_meta)), |
477 | - content_type="text/plain") |
478 | - res.headers.add('Location', "/images/%s" % image_meta['id']) |
479 | + status=httplib.CREATED, content_type="text/plain") |
480 | + res.headers.add('Location', "/images/%s" % image_id) |
481 | + res.headers.add('ETag', image_meta['checksum']) |
482 | |
483 | return req.get_response(res) |
484 | |
485 | @@ -395,9 +420,14 @@ |
486 | id, |
487 | new_image_meta) |
488 | if has_body: |
489 | - self._upload_and_activate(req, image_meta) |
490 | + image_meta = self._upload_and_activate(req, image_meta) |
491 | |
492 | - return dict(image=image_meta) |
493 | + res = Response(request=req, |
494 | + body=json.dumps(dict(image=image_meta)), |
495 | + content_type="text/plain") |
496 | + res.headers.add('Location', "/images/%s" % id) |
497 | + res.headers.add('ETag', image_meta['checksum']) |
498 | + return res |
499 | except exception.Invalid, e: |
500 | msg = ("Failed to update image metadata. Got error: %(e)s" |
501 | % locals()) |
502 | |
503 | === modified file 'glance/store/__init__.py' |
504 | --- glance/store/__init__.py 2011-02-24 02:50:24 +0000 |
505 | +++ glance/store/__init__.py 2011-03-25 19:01:10 +0000 |
506 | @@ -141,40 +141,3 @@ |
507 | authurl = "https://%s" % '/'.join(path_parts) |
508 | |
509 | return user, key, authurl, container, obj |
510 | - |
511 | - |
512 | -def add_options(parser): |
513 | - """ |
514 | - Adds any configuration options that each store might |
515 | - have. |
516 | - |
517 | - :param parser: An optparse.OptionParser object |
518 | - :retval None |
519 | - """ |
520 | - # TODO(jaypipes): Remove these imports... |
521 | - from glance.store.http import HTTPBackend |
522 | - from glance.store.s3 import S3Backend |
523 | - from glance.store.swift import SwiftBackend |
524 | - from glance.store.filesystem import FilesystemBackend |
525 | - |
526 | - help_text = "The following configuration options are specific to the "\ |
527 | - "Glance image store." |
528 | - |
529 | - DEFAULT_STORE_CHOICES = ['file', 'swift', 's3'] |
530 | - group = optparse.OptionGroup(parser, "Image Store Options", help_text) |
531 | - group.add_option('--default-store', metavar="STORE", |
532 | - default="file", |
533 | - choices=DEFAULT_STORE_CHOICES, |
534 | - help="The backend store that Glance will use to store " |
535 | - "virtual machine images to. Choices: ('%s') " |
536 | - "Default: %%default" % "','".join(DEFAULT_STORE_CHOICES)) |
537 | - |
538 | - backend_classes = [FilesystemBackend, |
539 | - HTTPBackend, |
540 | - SwiftBackend, |
541 | - S3Backend] |
542 | - for backend_class in backend_classes: |
543 | - if hasattr(backend_class, 'add_options'): |
544 | - backend_class.add_options(group) |
545 | - |
546 | - parser.add_option_group(group) |
547 | |
548 | === modified file 'glance/store/filesystem.py' |
549 | --- glance/store/filesystem.py 2011-02-27 20:54:29 +0000 |
550 | +++ glance/store/filesystem.py 2011-03-25 19:01:10 +0000 |
551 | @@ -19,6 +19,7 @@ |
552 | A simple filesystem-backed store |
553 | """ |
554 | |
555 | +import hashlib |
556 | import logging |
557 | import os |
558 | import urlparse |
559 | @@ -110,9 +111,10 @@ |
560 | :param data: The image data to write, as a file-like object |
561 | :param options: Conf mapping |
562 | |
563 | - :retval Tuple with (location, size) |
564 | - The location that was written, with file:// scheme prepended |
565 | - and the size in bytes of the data written |
566 | + :retval Tuple with (location, size, checksum) |
567 | + The location that was written, with file:// scheme prepended, |
568 | + the size in bytes of the data written, and the checksum of |
569 | + the image added. |
570 | """ |
571 | datadir = options['filesystem_store_datadir'] |
572 | |
573 | @@ -127,6 +129,7 @@ |
574 | raise exception.Duplicate("Image file %s already exists!" |
575 | % filepath) |
576 | |
577 | + checksum = hashlib.md5() |
578 | bytes_written = 0 |
579 | with open(filepath, 'wb') as f: |
580 | while True: |
581 | @@ -134,23 +137,11 @@ |
582 | if not buf: |
583 | break |
584 | bytes_written += len(buf) |
585 | + checksum.update(buf) |
586 | f.write(buf) |
587 | |
588 | - logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s" |
589 | - % locals()) |
590 | - return ('file://%s' % filepath, bytes_written) |
591 | - |
592 | - @classmethod |
593 | - def add_options(cls, parser): |
594 | - """ |
595 | - Adds specific configuration options for this store |
596 | - |
597 | - :param parser: An optparse.OptionParser object |
598 | - :retval None |
599 | - """ |
600 | - |
601 | - parser.add_option('--filesystem-store-datadir', metavar="DIR", |
602 | - default="/var/lib/glance/images/", |
603 | - help="Location to write image data. This directory " |
604 | - "should be writeable by the user that runs the " |
605 | - "glance-api program. Default: %default") |
606 | + checksum_hex = checksum.hexdigest() |
607 | + |
608 | + logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s with " |
609 | + "checksum %(checksum_hex)s" % locals()) |
610 | + return ('file://%s' % filepath, bytes_written, checksum_hex) |
611 | |
612 | === modified file 'glance/store/swift.py' |
613 | --- glance/store/swift.py 2011-03-16 16:45:01 +0000 |
614 | +++ glance/store/swift.py 2011-03-25 19:01:10 +0000 |
615 | @@ -161,7 +161,7 @@ |
616 | # header keys are lowercased by Swift |
617 | if 'content-length' in resp_headers: |
618 | size = int(resp_headers['content-length']) |
619 | - return (location, size) |
620 | + return (location, size, obj_etag) |
621 | except swift_client.ClientException, e: |
622 | if e.http_status == httplib.CONFLICT: |
623 | raise exception.Duplicate("Swift already has an image at " |
624 | |
625 | === modified file 'tests/functional/__init__.py' |
626 | --- tests/functional/__init__.py 2011-03-17 22:15:41 +0000 |
627 | +++ tests/functional/__init__.py 2011-03-25 19:01:10 +0000 |
628 | @@ -203,14 +203,14 @@ |
629 | """ |
630 | |
631 | # Spin down the API and default registry server |
632 | - cmd = ("./bin/glance-control api start " |
633 | + cmd = ("./bin/glance-control api stop " |
634 | "%(conf_file_name)s --pid-file=%(api_pid_file)s" |
635 | % self.__dict__) |
636 | exitcode, out, err = execute(cmd) |
637 | self.assertEqual(0, exitcode, |
638 | "Failed to spin down the API server. " |
639 | "Got: %s" % err) |
640 | - cmd = ("./bin/glance-control registry start " |
641 | + cmd = ("./bin/glance-control registry stop " |
642 | "%(conf_file_name)s --pid-file=%(registry_pid_file)s" |
643 | % self.__dict__) |
644 | exitcode, out, err = execute(cmd) |
645 | |
646 | === modified file 'tests/functional/test_curl_api.py' |
647 | --- tests/functional/test_curl_api.py 2011-03-17 21:43:26 +0000 |
648 | +++ tests/functional/test_curl_api.py 2011-03-25 19:01:10 +0000 |
649 | @@ -110,7 +110,7 @@ |
650 | lines = out.split("\r\n") |
651 | status_line = lines[0] |
652 | |
653 | - self.assertEqual("HTTP/1.1 200 OK", status_line) |
654 | + self.assertEqual("HTTP/1.1 201 Created", status_line) |
655 | |
656 | # 4. HEAD /images |
657 | # Verify image found now |
658 | @@ -214,6 +214,7 @@ |
659 | "disk_format": None, |
660 | "id": 1, |
661 | "name": "Image1", |
662 | + "checksum": "c2e5db72bd7fd153f53ede5da5a06de3", |
663 | "size": 5120}]} |
664 | self.assertEqual(expected_result, json.loads(out.strip())) |
665 | |
666 | |
667 | === modified file 'tests/stubs.py' |
668 | --- tests/stubs.py 2011-03-14 19:10:24 +0000 |
669 | +++ tests/stubs.py 2011-03-25 19:01:10 +0000 |
670 | @@ -282,6 +282,7 @@ |
671 | 'updated_at': datetime.datetime.utcnow(), |
672 | 'deleted_at': None, |
673 | 'deleted': False, |
674 | + 'checksum': None, |
675 | 'size': 13, |
676 | 'location': "swift://user:passwd@acct/container/obj.tar.0", |
677 | 'properties': [{'key': 'type', |
678 | @@ -297,6 +298,7 @@ |
679 | 'updated_at': datetime.datetime.utcnow(), |
680 | 'deleted_at': None, |
681 | 'deleted': False, |
682 | + 'checksum': None, |
683 | 'size': 19, |
684 | 'location': "file:///tmp/glance-tests/2", |
685 | 'properties': []}] |
686 | @@ -316,6 +318,7 @@ |
687 | glance.registry.db.api.validate_image(values) |
688 | |
689 | values['size'] = values.get('size', 0) |
690 | + values['checksum'] = values.get('checksum') |
691 | values['deleted'] = False |
692 | values['properties'] = values.get('properties', {}) |
693 | values['created_at'] = datetime.datetime.utcnow() |
694 | @@ -348,6 +351,7 @@ |
695 | copy_image.update(values) |
696 | glance.registry.db.api.validate_image(copy_image) |
697 | props = [] |
698 | + orig_properties = image['properties'] |
699 | |
700 | if 'properties' in values.keys(): |
701 | for k, v in values['properties'].items(): |
702 | @@ -360,7 +364,8 @@ |
703 | p['deleted_at'] = None |
704 | props.append(p) |
705 | |
706 | - values['properties'] = props |
707 | + orig_properties = orig_properties + props |
708 | + values['properties'] = orig_properties |
709 | |
710 | image.update(values) |
711 | return image |
712 | |
713 | === modified file 'tests/unit/test_api.py' |
714 | --- tests/unit/test_api.py 2011-03-14 19:10:24 +0000 |
715 | +++ tests/unit/test_api.py 2011-03-25 19:01:10 +0000 |
716 | @@ -15,8 +15,9 @@ |
717 | # License for the specific language governing permissions and limitations |
718 | # under the License. |
719 | |
720 | +import hashlib |
721 | +import httplib |
722 | import os |
723 | -import httplib |
724 | import json |
725 | import unittest |
726 | |
727 | @@ -52,7 +53,9 @@ |
728 | |
729 | """ |
730 | fixture = {'id': 2, |
731 | - 'name': 'fake image #2'} |
732 | + 'name': 'fake image #2', |
733 | + 'size': 19, |
734 | + 'checksum': None} |
735 | req = webob.Request.blank('/') |
736 | res = req.get_response(self.api) |
737 | res_dict = json.loads(res.body) |
738 | @@ -70,7 +73,9 @@ |
739 | |
740 | """ |
741 | fixture = {'id': 2, |
742 | - 'name': 'fake image #2'} |
743 | + 'name': 'fake image #2', |
744 | + 'size': 19, |
745 | + 'checksum': None} |
746 | req = webob.Request.blank('/images') |
747 | res = req.get_response(self.api) |
748 | res_dict = json.loads(res.body) |
749 | @@ -90,6 +95,8 @@ |
750 | fixture = {'id': 2, |
751 | 'name': 'fake image #2', |
752 | 'is_public': True, |
753 | + 'size': 19, |
754 | + 'checksum': None, |
755 | 'disk_format': 'vhd', |
756 | 'container_format': 'ovf', |
757 | 'status': 'active'} |
758 | @@ -396,7 +403,7 @@ |
759 | for k, v in fixture_headers.iteritems(): |
760 | req.headers[k] = v |
761 | res = req.get_response(self.api) |
762 | - self.assertEquals(res.status_int, httplib.OK) |
763 | + self.assertEquals(res.status_int, httplib.CREATED) |
764 | |
765 | res_body = json.loads(res.body)['image'] |
766 | self.assertEquals('queued', res_body['status']) |
767 | @@ -431,7 +438,7 @@ |
768 | req.headers['Content-Type'] = 'application/octet-stream' |
769 | req.body = "chunk00000remainder" |
770 | res = req.get_response(self.api) |
771 | - self.assertEquals(res.status_int, 200) |
772 | + self.assertEquals(res.status_int, httplib.CREATED) |
773 | |
774 | res_body = json.loads(res.body)['image'] |
775 | self.assertEquals(res_body['location'], |
776 | @@ -445,6 +452,97 @@ |
777 | "res.headerlist = %r" % res.headerlist) |
778 | self.assertTrue('/images/3' in res.headers['location']) |
779 | |
780 | + def test_image_is_checksummed(self): |
781 | + """Test that the image contents are checksummed properly""" |
782 | + fixture_headers = {'x-image-meta-store': 'file', |
783 | + 'x-image-meta-disk-format': 'vhd', |
784 | + 'x-image-meta-container-format': 'ovf', |
785 | + 'x-image-meta-name': 'fake image #3'} |
786 | + image_contents = "chunk00000remainder" |
787 | + image_checksum = hashlib.md5(image_contents).hexdigest() |
788 | + |
789 | + req = webob.Request.blank("/images") |
790 | + req.method = 'POST' |
791 | + for k, v in fixture_headers.iteritems(): |
792 | + req.headers[k] = v |
793 | + |
794 | + req.headers['Content-Type'] = 'application/octet-stream' |
795 | + req.body = image_contents |
796 | + res = req.get_response(self.api) |
797 | + self.assertEquals(res.status_int, httplib.CREATED) |
798 | + |
799 | + res_body = json.loads(res.body)['image'] |
800 | + self.assertEquals(res_body['location'], |
801 | + 'file:///tmp/glance-tests/3') |
802 | + self.assertEquals(image_checksum, res_body['checksum'], |
803 | + "Mismatched checksum. Expected %s, got %s" % |
804 | + (image_checksum, res_body['checksum'])) |
805 | + |
806 | + def test_etag_equals_checksum_header(self): |
807 | + """Test that the ETag header matches the x-image-meta-checksum""" |
808 | + fixture_headers = {'x-image-meta-store': 'file', |
809 | + 'x-image-meta-disk-format': 'vhd', |
810 | + 'x-image-meta-container-format': 'ovf', |
811 | + 'x-image-meta-name': 'fake image #3'} |
812 | + image_contents = "chunk00000remainder" |
813 | + image_checksum = hashlib.md5(image_contents).hexdigest() |
814 | + |
815 | + req = webob.Request.blank("/images") |
816 | + req.method = 'POST' |
817 | + for k, v in fixture_headers.iteritems(): |
818 | + req.headers[k] = v |
819 | + |
820 | + req.headers['Content-Type'] = 'application/octet-stream' |
821 | + req.body = image_contents |
822 | + res = req.get_response(self.api) |
823 | + self.assertEquals(res.status_int, httplib.CREATED) |
824 | + |
825 | + # HEAD the image and check the ETag equals the checksum header... |
826 | + expected_headers = {'x-image-meta-checksum': image_checksum, |
827 | + 'etag': image_checksum} |
828 | + req = webob.Request.blank("/images/3") |
829 | + req.method = 'HEAD' |
830 | + res = req.get_response(self.api) |
831 | + self.assertEquals(res.status_int, 200) |
832 | + |
833 | + for key in expected_headers.keys(): |
834 | + self.assertTrue(key in res.headers, |
835 | + "required header '%s' missing from " |
836 | + "returned headers" % key) |
837 | + for key, value in expected_headers.iteritems(): |
838 | + self.assertEquals(value, res.headers[key]) |
839 | + |
840 | + def test_bad_checksum_kills_image(self): |
841 | + """Test that the image contents are checksummed properly""" |
842 | + image_contents = "chunk00000remainder" |
843 | + bad_checksum = hashlib.md5("invalid").hexdigest() |
844 | + fixture_headers = {'x-image-meta-store': 'file', |
845 | + 'x-image-meta-disk-format': 'vhd', |
846 | + 'x-image-meta-container-format': 'ovf', |
847 | + 'x-image-meta-name': 'fake image #3', |
848 | + 'x-image-meta-checksum': bad_checksum} |
849 | + |
850 | + req = webob.Request.blank("/images") |
851 | + req.method = 'POST' |
852 | + for k, v in fixture_headers.iteritems(): |
853 | + req.headers[k] = v |
854 | + |
855 | + req.headers['Content-Type'] = 'application/octet-stream' |
856 | + req.body = image_contents |
857 | + res = req.get_response(self.api) |
858 | + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) |
859 | + |
860 | + # Test the image was killed... |
861 | + expected_headers = {'x-image-meta-id': '3', |
862 | + 'x-image-meta-status': 'killed'} |
863 | + req = webob.Request.blank("/images/3") |
864 | + req.method = 'HEAD' |
865 | + res = req.get_response(self.api) |
866 | + self.assertEquals(res.status_int, 200) |
867 | + |
868 | + for key, value in expected_headers.iteritems(): |
869 | + self.assertEquals(value, res.headers[key]) |
870 | + |
871 | def test_image_meta(self): |
872 | """Test for HEAD /images/<ID>""" |
873 | expected_headers = {'x-image-meta-id': '2', |
874 | |
875 | === modified file 'tests/unit/test_filesystem_store.py' |
876 | --- tests/unit/test_filesystem_store.py 2011-02-27 20:54:29 +0000 |
877 | +++ tests/unit/test_filesystem_store.py 2011-03-25 19:01:10 +0000 |
878 | @@ -18,6 +18,7 @@ |
879 | """Tests the filesystem backend store""" |
880 | |
881 | import StringIO |
882 | +import hashlib |
883 | import unittest |
884 | import urlparse |
885 | |
886 | @@ -27,6 +28,11 @@ |
887 | from glance.store.filesystem import FilesystemBackend, ChunkedFile |
888 | from tests import stubs |
889 | |
890 | +FILESYSTEM_OPTIONS = { |
891 | + 'verbose': True, |
892 | + 'debug': True, |
893 | + 'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR} |
894 | + |
895 | |
896 | class TestFilesystemBackend(unittest.TestCase): |
897 | |
898 | @@ -75,17 +81,17 @@ |
899 | expected_image_id = 42 |
900 | expected_file_size = 1024 * 5 # 5K |
901 | expected_file_contents = "*" * expected_file_size |
902 | + expected_checksum = hashlib.md5(expected_file_contents).hexdigest() |
903 | expected_location = "file://%s/%s" % (stubs.FAKE_FILESYSTEM_ROOTDIR, |
904 | expected_image_id) |
905 | image_file = StringIO.StringIO(expected_file_contents) |
906 | - options = {'verbose': True, |
907 | - 'debug': True, |
908 | - 'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR} |
909 | |
910 | - location, size = FilesystemBackend.add(42, image_file, options) |
911 | + location, size, checksum = FilesystemBackend.add(42, image_file, |
912 | + FILESYSTEM_OPTIONS) |
913 | |
914 | self.assertEquals(expected_location, location) |
915 | self.assertEquals(expected_file_size, size) |
916 | + self.assertEquals(expected_checksum, checksum) |
917 | |
918 | url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42") |
919 | new_image_file = FilesystemBackend.get(url_pieces) |
920 | @@ -110,7 +116,7 @@ |
921 | 'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR} |
922 | self.assertRaises(exception.Duplicate, |
923 | FilesystemBackend.add, |
924 | - 2, image_file, options) |
925 | + 2, image_file, FILESYSTEM_OPTIONS) |
926 | |
927 | def test_delete(self): |
928 | """ |
929 | |
930 | === modified file 'tests/unit/test_swift_store.py' |
931 | --- tests/unit/test_swift_store.py 2011-03-16 16:45:01 +0000 |
932 | +++ tests/unit/test_swift_store.py 2011-03-25 19:01:10 +0000 |
933 | @@ -70,15 +70,20 @@ |
934 | if hasattr(contents, 'read'): |
935 | fixture_object = StringIO.StringIO() |
936 | chunk = contents.read(SwiftBackend.CHUNKSIZE) |
937 | + checksum = hashlib.md5() |
938 | while chunk: |
939 | fixture_object.write(chunk) |
940 | + checksum.update(chunk) |
941 | chunk = contents.read(SwiftBackend.CHUNKSIZE) |
942 | + etag = checksum.hexdigest() |
943 | else: |
944 | fixture_object = StringIO.StringIO(contents) |
945 | + etag = hashlib.md5(fixture_object.getvalue()).hexdigest() |
946 | + read_len = fixture_object.len |
947 | fixture_objects[fixture_key] = fixture_object |
948 | fixture_headers[fixture_key] = { |
949 | - 'content-length': fixture_object.len, |
950 | - 'etag': hashlib.md5(fixture_object.read()).hexdigest()} |
951 | + 'content-length': read_len, |
952 | + 'etag': etag} |
953 | return fixture_headers[fixture_key]['etag'] |
954 | else: |
955 | msg = ("Object PUT failed - Object with key %s already exists" |
956 | @@ -226,8 +231,9 @@ |
957 | def test_add(self): |
958 | """Test that we can add an image via the swift backend""" |
959 | expected_image_id = 42 |
960 | - expected_swift_size = 1024 * 5 # 5K |
961 | + expected_swift_size = FIVE_KB |
962 | expected_swift_contents = "*" * expected_swift_size |
963 | + expected_checksum = hashlib.md5(expected_swift_contents).hexdigest() |
964 | expected_location = format_swift_location( |
965 | SWIFT_OPTIONS['swift_store_user'], |
966 | SWIFT_OPTIONS['swift_store_key'], |
967 | @@ -236,10 +242,12 @@ |
968 | expected_image_id) |
969 | image_swift = StringIO.StringIO(expected_swift_contents) |
970 | |
971 | - location, size = SwiftBackend.add(42, image_swift, SWIFT_OPTIONS) |
972 | + location, size, checksum = SwiftBackend.add(42, image_swift, |
973 | + SWIFT_OPTIONS) |
974 | |
975 | self.assertEquals(expected_location, location) |
976 | self.assertEquals(expected_swift_size, size) |
977 | + self.assertEquals(expected_checksum, checksum) |
978 | |
979 | url_pieces = urlparse.urlparse(expected_location) |
980 | new_image_swift = SwiftBackend.get(url_pieces) |
981 | @@ -280,8 +288,9 @@ |
982 | options['swift_store_create_container_on_put'] = 'True' |
983 | options['swift_store_container'] = 'noexist' |
984 | expected_image_id = 42 |
985 | - expected_swift_size = 1024 * 5 # 5K |
986 | + expected_swift_size = FIVE_KB |
987 | expected_swift_contents = "*" * expected_swift_size |
988 | + expected_checksum = hashlib.md5(expected_swift_contents).hexdigest() |
989 | expected_location = format_swift_location( |
990 | options['swift_store_user'], |
991 | options['swift_store_key'], |
992 | @@ -290,10 +299,12 @@ |
993 | expected_image_id) |
994 | image_swift = StringIO.StringIO(expected_swift_contents) |
995 | |
996 | - location, size = SwiftBackend.add(42, image_swift, options) |
997 | + location, size, checksum = SwiftBackend.add(42, image_swift, |
998 | + options) |
999 | |
1000 | self.assertEquals(expected_location, location) |
1001 | self.assertEquals(expected_swift_size, size) |
1002 | + self.assertEquals(expected_checksum, checksum) |
1003 | |
1004 | url_pieces = urlparse.urlparse(expected_location) |
1005 | new_image_swift = SwiftBackend.get(url_pieces) |
The swift parts look reasonable to me. The only other thing that I might add is to clarify in the docs that the checksum is an md5 checksum, otherwise people may try to use CRC.