Merge lp:~jaypipes/glance/api-add-image into lp:~hudson-openstack/glance/trunk
- api-add-image
- Merge into trunk
Proposed by
Jay Pipes
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 30 |
Merged at revision: | 27 |
Proposed branch: | lp:~jaypipes/glance/api-add-image |
Merge into: | lp:~hudson-openstack/glance/trunk |
Diff against target: |
858 lines (+543/-33) 11 files modified
doc/source/index.rst (+239/-0) glance/client.py (+19/-8) glance/common/exception.py (+5/-0) glance/common/flags.py (+2/-0) glance/registry/db/sqlalchemy/api.py (+23/-8) glance/server.py (+43/-10) glance/store/__init__.py (+1/-1) glance/store/filesystem.py (+37/-1) tests/stubs.py (+17/-1) tests/unit/test_api.py (+69/-2) tests/unit/test_clients.py (+88/-2) |
To merge this branch: | bzr merge lp:~jaypipes/glance/api-add-image |
Related bugs: | |
Related blueprints: |
Implement adding image data directly to Glance
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher MacGown (community) | Approve | ||
Rick Harris | Pending | ||
Review via email:
|
Commit message
Description of the change
Enhances POST /images call to, you know, actually make it work...
Contains Rick's additions as well.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doc/source/index.rst' | |||
2 | --- doc/source/index.rst 2010-12-16 22:04:58 +0000 | |||
3 | +++ doc/source/index.rst 2010-12-21 15:27:59 +0000 | |||
4 | @@ -198,6 +198,122 @@ | |||
5 | 198 | .. toctree:: | 198 | .. toctree:: |
6 | 199 | :maxdepth: 1 | 199 | :maxdepth: 1 |
7 | 200 | 200 | ||
8 | 201 | |||
9 | 202 | Adding a New Virtual Machine Image | ||
10 | 203 | ---------------------------------- | ||
11 | 204 | |||
12 | 205 | We have created a new virtual machine image in some way (created a | ||
13 | 206 | "golden image" or snapshotted/backed up an existing image) and we | ||
14 | 207 | wish to do two things: | ||
15 | 208 | |||
16 | 209 | * Store the disk image data in Glance | ||
17 | 210 | * Store metadata about this image in Glance | ||
18 | 211 | |||
19 | 212 | We can do the above two activities in a single call to the Glance API. | ||
20 | 213 | Assuming, like in the examples above, that a Glance API server is running | ||
21 | 214 | at `glance.openstack.org`, we issue a `POST` request to add an image to | ||
22 | 215 | Glance:: | ||
23 | 216 | |||
24 | 217 | POST http://glance.openstack.org/images/ | ||
25 | 218 | |||
26 | 219 | The metadata about the image is sent to Glance in HTTP headers. The body | ||
27 | 220 | of the HTTP request to the Glance API will be the MIME-encoded disk | ||
28 | 221 | image data. | ||
29 | 222 | |||
30 | 223 | |||
31 | 224 | Adding Image Metadata in HTTP Headers | ||
32 | 225 | ************************************* | ||
33 | 226 | |||
34 | 227 | Glance will view as image metadata any HTTP header that it receives in a | ||
35 | 228 | `POST` request where the header key is prefixed with the strings | ||
36 | 229 | `x-image-meta-` and `x-image-meta-property-`. | ||
37 | 230 | |||
38 | 231 | The list of metadata headers that Glance accepts are listed below. | ||
39 | 232 | |||
40 | 233 | * `x-image-meta-name` | ||
41 | 234 | |||
42 | 235 | This header is required. Its value should be the name of the image. | ||
43 | 236 | |||
44 | 237 | Note that the name of an image *is not unique to a Glance node*. It | ||
45 | 238 | would be an unrealistic expectation of users to know all the unique | ||
46 | 239 | names of all other user's images. | ||
47 | 240 | |||
48 | 241 | * `x-image-meta-id` | ||
49 | 242 | |||
50 | 243 | This header is optional. | ||
51 | 244 | |||
52 | 245 | When present, Glance will use the supplied identifier for the image. | ||
53 | 246 | If the identifier already exists in that Glance node, then a | ||
54 | 247 | `409 Conflict` will be returned by Glance. | ||
55 | 248 | |||
56 | 249 | When this header is *not* present, Glance will generate an identifier | ||
57 | 250 | for the image and return this identifier in the response (see below) | ||
58 | 251 | |||
59 | 252 | * `x-image-meta-store` | ||
60 | 253 | |||
61 | 254 | This header is optional. Valid values are one of `file` or `swift` | ||
62 | 255 | |||
63 | 256 | When present, Glance will attempt to store the disk image data in the | ||
64 | 257 | backing store indicated by the value of the header. If the Glance node | ||
65 | 258 | does not support the backing store, Glance will return a `400 Bad Request`. | ||
66 | 259 | |||
67 | 260 | When not present, Glance will store the disk image data in the backing | ||
68 | 261 | store that is marked default. See the configuration option `default_store` | ||
69 | 262 | for more information. | ||
70 | 263 | |||
71 | 264 | * `x-image-meta-type` | ||
72 | 265 | |||
73 | 266 | This header is required. Valid values are one of `kernel`, `machine`, `raw`, | ||
74 | 267 | or `ramdisk`. | ||
75 | 268 | |||
76 | 269 | * `x-image-meta-size` | ||
77 | 270 | |||
78 | 271 | This header is optional. | ||
79 | 272 | |||
80 | 273 | When present, Glance assumes that the expected size of the request body | ||
81 | 274 | will be the value of this header. If the length in bytes of the request | ||
82 | 275 | body *does not match* the value of this header, Glance will return a | ||
83 | 276 | `400 Bad Request`. | ||
84 | 277 | |||
85 | 278 | When not present, Glance will calculate the image's size based on the size | ||
86 | 279 | of the request body. | ||
87 | 280 | |||
88 | 281 | * `x-image-meta-is_public` | ||
89 | 282 | |||
90 | 283 | This header is optional. | ||
91 | 284 | |||
92 | 285 | When present, Glance converts the value of the header to a boolean value, | ||
93 | 286 | so "on, 1, true" are all true values. When true, the image is marked as | ||
94 | 287 | a public image, meaning that any user may view its metadata and may read | ||
95 | 288 | the disk image from Glance. | ||
96 | 289 | |||
97 | 290 | When not present, the image is assumed to be *not public* and specific to | ||
98 | 291 | a user. | ||
99 | 292 | |||
100 | 293 | * `x-image-meta-property-*` | ||
101 | 294 | |||
102 | 295 | When Glance receives any HTTP header whose key begins with the string prefix | ||
103 | 296 | `x-image-meta-property-`, Glance adds the key and value to a set of custom, | ||
104 | 297 | free-form image properties stored with the image. The key is the | ||
105 | 298 | lower-cased string following the prefix `x-image-meta-property-` with dashes | ||
106 | 299 | and punctuation replaced with underscores. | ||
107 | 300 | |||
108 | 301 | For example, if the following HTTP header were sent:: | ||
109 | 302 | |||
110 | 303 | x-image-meta-property-distro Ubuntu 10.10 | ||
111 | 304 | |||
112 | 305 | Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the | ||
113 | 306 | image in Glance. | ||
114 | 307 | |||
115 | 308 | There is no limit on the number of free-form key/value attributes that can | ||
116 | 309 | be attached to the image. However, keep in mind that the 8K limit on the | ||
117 | 310 | size of all HTTP headers sent in a request will effectively limit the number | ||
118 | 311 | of image properties. | ||
119 | 312 | |||
120 | 313 | |||
121 | 314 | .. toctree:: | ||
122 | 315 | :maxdepth: 1 | ||
123 | 316 | |||
124 | 201 | Image Identifiers | 317 | Image Identifiers |
125 | 202 | ================= | 318 | ================= |
126 | 203 | 319 | ||
127 | @@ -325,6 +441,129 @@ | |||
128 | 325 | .. toctree:: | 441 | .. toctree:: |
129 | 326 | :maxdepth: 1 | 442 | :maxdepth: 1 |
130 | 327 | 443 | ||
131 | 444 | |||
132 | 445 | Adding a New Virtual Machine Image | ||
133 | 446 | ---------------------------------- | ||
134 | 447 | |||
135 | 448 | We have created a new virtual machine image in some way (created a | ||
136 | 449 | "golden image" or snapshotted/backed up an existing image) and we | ||
137 | 450 | wish to do two things: | ||
138 | 451 | |||
139 | 452 | * Store the disk image data in Glance | ||
140 | 453 | * Store metadata about this image in Glance | ||
141 | 454 | |||
142 | 455 | We can do the above two activities in a single call to the Glance client. | ||
143 | 456 | Assuming, like in the examples above, that a Glance API server is running | ||
144 | 457 | at `glance.openstack.org`, we issue a call to `glance.client.Client.add_image`. | ||
145 | 458 | |||
146 | 459 | The method signature is as follows:: | ||
147 | 460 | |||
148 | 461 | glance.client.Client.add_image(image_meta, image_data=None) | ||
149 | 462 | |||
150 | 463 | The `image_meta` argument is a mapping containing various image metadata. The | ||
151 | 464 | `image_data` argument is the disk image data. | ||
152 | 465 | |||
153 | 466 | The list of metadata that `image_meta` can contain are listed below. | ||
154 | 467 | |||
155 | 468 | * `name` | ||
156 | 469 | |||
157 | 470 | This key/value is required. Its value should be the name of the image. | ||
158 | 471 | |||
159 | 472 | Note that the name of an image *is not unique to a Glance node*. It | ||
160 | 473 | would be an unrealistic expectation of users to know all the unique | ||
161 | 474 | names of all other user's images. | ||
162 | 475 | |||
163 | 476 | * `id` | ||
164 | 477 | |||
165 | 478 | This key/value is optional. | ||
166 | 479 | |||
167 | 480 | When present, Glance will use the supplied identifier for the image. | ||
168 | 481 | If the identifier already exists in that Glance node, then a | ||
169 | 482 | `glance.common.exception.Duplicate` will be raised. | ||
170 | 483 | |||
171 | 484 | When this key/value is *not* present, Glance will generate an identifier | ||
172 | 485 | for the image and return this identifier in the response (see below) | ||
173 | 486 | |||
174 | 487 | * `store` | ||
175 | 488 | |||
176 | 489 | This key/value is optional. Valid values are one of `file` or `swift` | ||
177 | 490 | |||
178 | 491 | When present, Glance will attempt to store the disk image data in the | ||
179 | 492 | backing store indicated by the value. If the Glance node does not support | ||
180 | 493 | the backing store, Glance will raise a `glance.common.exception.BadRequest` | ||
181 | 494 | |||
182 | 495 | When not present, Glance will store the disk image data in the backing | ||
183 | 496 | store that is marked default. See the configuration option `default_store` | ||
184 | 497 | for more information. | ||
185 | 498 | |||
186 | 499 | * `type` | ||
187 | 500 | |||
188 | 501 | This key/values is required. Valid values are one of `kernel`, `machine`, | ||
189 | 502 | `raw`, or `ramdisk`. | ||
190 | 503 | |||
191 | 504 | * `size` | ||
192 | 505 | |||
193 | 506 | This key/value is optional. | ||
194 | 507 | |||
195 | 508 | When present, Glance assumes that the expected size of the request body | ||
196 | 509 | will be the value. If the length in bytes of the request body *does not | ||
197 | 510 | match* the value, Glance will raise a `glance.common.exception.BadRequest` | ||
198 | 511 | |||
199 | 512 | When not present, Glance will calculate the image's size based on the size | ||
200 | 513 | of the request body. | ||
201 | 514 | |||
202 | 515 | * `is_public` | ||
203 | 516 | |||
204 | 517 | This key/value is optional. | ||
205 | 518 | |||
206 | 519 | When present, Glance converts the value to a boolean value, so "on, 1, true" | ||
207 | 520 | are all true values. When true, the image is marked as a public image, | ||
208 | 521 | meaning that any user may view its metadata and may read the disk image from | ||
209 | 522 | Glance. | ||
210 | 523 | |||
211 | 524 | When not present, the image is assumed to be *not public* and specific to | ||
212 | 525 | a user. | ||
213 | 526 | |||
214 | 527 | * `properties` | ||
215 | 528 | |||
216 | 529 | This key/value is optional. | ||
217 | 530 | |||
218 | 531 | When present, the value is assumed to be a mapping of free-form key/value | ||
219 | 532 | attributes to store with the image. | ||
220 | 533 | |||
221 | 534 | For example, if the following is the value of the `properties` key in the | ||
222 | 535 | `image_meta` argument:: | ||
223 | 536 | |||
224 | 537 | {'distro': 'Ubuntu 10.10'} | ||
225 | 538 | |||
226 | 539 | Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the | ||
227 | 540 | image in Glance. | ||
228 | 541 | |||
229 | 542 | There is no limit on the number of free-form key/value attributes that can | ||
230 | 543 | be attached to the image with `properties`. However, keep in mind that there | ||
231 | 544 | is a 8K limit on the size of all HTTP headers sent in a request and this | ||
232 | 545 | number will effectively limit the number of image properties. | ||
233 | 546 | |||
234 | 547 | As a complete example, the following code would add a new machine image to | ||
235 | 548 | Glance:: | ||
236 | 549 | |||
237 | 550 | from glance.client import Client | ||
238 | 551 | |||
239 | 552 | c = Client("glance.openstack.org", 9292) | ||
240 | 553 | |||
241 | 554 | meta = {'name': 'Ubuntu 10.10 5G', | ||
242 | 555 | 'type': 'machine', | ||
243 | 556 | 'is_public': True, | ||
244 | 557 | 'properties': {'distro': 'Ubuntu 10.10'}} | ||
245 | 558 | |||
246 | 559 | new_meta = c.add_image(meta, open('/path/to/image.tar.gz')) | ||
247 | 560 | |||
248 | 561 | print 'Stored image. Got identifier: %s' % new_meta['id'] | ||
249 | 562 | |||
250 | 563 | |||
251 | 564 | .. toctree:: | ||
252 | 565 | :maxdepth: 1 | ||
253 | 566 | |||
254 | 328 | Indices and tables | 567 | Indices and tables |
255 | 329 | ================== | 568 | ================== |
256 | 330 | 569 | ||
257 | 331 | 570 | ||
258 | === modified file 'glance/client.py' | |||
259 | --- glance/client.py 2010-12-16 23:19:23 +0000 | |||
260 | +++ glance/client.py 2010-12-21 15:27:59 +0000 | |||
261 | @@ -46,11 +46,6 @@ | |||
262 | 46 | pass | 46 | pass |
263 | 47 | 47 | ||
264 | 48 | 48 | ||
265 | 49 | class BadInputError(Exception): | ||
266 | 50 | """Error resulting from a client sending bad input to a server""" | ||
267 | 51 | pass | ||
268 | 52 | |||
269 | 53 | |||
270 | 54 | class ImageBodyIterator(object): | 49 | class ImageBodyIterator(object): |
271 | 55 | 50 | ||
272 | 56 | """ | 51 | """ |
273 | @@ -150,7 +145,7 @@ | |||
274 | 150 | elif status_code == httplib.CONFLICT: | 145 | elif status_code == httplib.CONFLICT: |
275 | 151 | raise exception.Duplicate | 146 | raise exception.Duplicate |
276 | 152 | elif status_code == httplib.BAD_REQUEST: | 147 | elif status_code == httplib.BAD_REQUEST: |
278 | 153 | raise BadInputError | 148 | raise exception.BadInputError |
279 | 154 | else: | 149 | else: |
280 | 155 | raise Exception("Unknown error occurred! %d" % status_code) | 150 | raise Exception("Unknown error occurred! %d" % status_code) |
281 | 156 | 151 | ||
282 | @@ -235,17 +230,33 @@ | |||
283 | 235 | """ | 230 | """ |
284 | 236 | Tells Glance about an image's metadata as well | 231 | Tells Glance about an image's metadata as well |
285 | 237 | as optionally the image_data itself | 232 | as optionally the image_data itself |
286 | 233 | |||
287 | 234 | :param image_meta: Mapping of information about the | ||
288 | 235 | image | ||
289 | 236 | :param image_data: Optional string of raw image data | ||
290 | 237 | or file-like object that can be | ||
291 | 238 | used to read the image data | ||
292 | 239 | |||
293 | 240 | :retval The newly-stored image's metadata. | ||
294 | 238 | """ | 241 | """ |
295 | 239 | if not image_data and 'location' not in image_meta.keys(): | 242 | if not image_data and 'location' not in image_meta.keys(): |
296 | 240 | raise exception.Invalid("You must either specify a location " | 243 | raise exception.Invalid("You must either specify a location " |
297 | 241 | "for the image or supply the actual " | 244 | "for the image or supply the actual " |
298 | 242 | "image data when adding an image to " | 245 | "image data when adding an image to " |
299 | 243 | "Glance") | 246 | "Glance") |
300 | 244 | body = image_data | ||
301 | 245 | headers = util.image_meta_to_http_headers(image_meta) | 247 | headers = util.image_meta_to_http_headers(image_meta) |
302 | 248 | if image_data: | ||
303 | 249 | if hasattr(image_data, 'read'): | ||
304 | 250 | # TODO(jaypipes): This is far from efficient. Implement | ||
305 | 251 | # chunked transfer encoding if size is not in image_meta | ||
306 | 252 | body = image_data.read() | ||
307 | 253 | else: | ||
308 | 254 | body = image_data | ||
309 | 255 | headers['content-type'] = 'application/octet-stream' | ||
310 | 256 | else: | ||
311 | 257 | body = None | ||
312 | 246 | 258 | ||
313 | 247 | res = self.do_request("POST", "/images", body, headers) | 259 | res = self.do_request("POST", "/images", body, headers) |
314 | 248 | # Registry returns a JSONified dict(image=image_info) | ||
315 | 249 | data = json.loads(res.read()) | 260 | data = json.loads(res.read()) |
316 | 250 | return data['image']['id'] | 261 | return data['image']['id'] |
317 | 251 | 262 | ||
318 | 252 | 263 | ||
319 | === modified file 'glance/common/exception.py' | |||
320 | --- glance/common/exception.py 2010-11-23 18:12:11 +0000 | |||
321 | +++ glance/common/exception.py 2010-12-21 15:27:59 +0000 | |||
322 | @@ -70,6 +70,11 @@ | |||
323 | 70 | pass | 70 | pass |
324 | 71 | 71 | ||
325 | 72 | 72 | ||
326 | 73 | class BadInputError(Exception): | ||
327 | 74 | """Error resulting from a client sending bad input to a server""" | ||
328 | 75 | pass | ||
329 | 76 | |||
330 | 77 | |||
331 | 73 | def wrap_exception(f): | 78 | def wrap_exception(f): |
332 | 74 | def _wrap(*args, **kw): | 79 | def _wrap(*args, **kw): |
333 | 75 | try: | 80 | try: |
334 | 76 | 81 | ||
335 | === modified file 'glance/common/flags.py' | |||
336 | --- glance/common/flags.py 2010-09-29 00:20:11 +0000 | |||
337 | +++ glance/common/flags.py 2010-12-21 15:27:59 +0000 | |||
338 | @@ -173,3 +173,5 @@ | |||
339 | 173 | 'sqlite:///%s/glance.sqlite' % os.path.abspath("./"), | 173 | 'sqlite:///%s/glance.sqlite' % os.path.abspath("./"), |
340 | 174 | 'connection string for sql database') | 174 | 'connection string for sql database') |
341 | 175 | DEFINE_bool('verbose', False, 'show debug output') | 175 | DEFINE_bool('verbose', False, 'show debug output') |
342 | 176 | DEFINE_string('default_store', 'file', | ||
343 | 177 | 'Default storage backend. Default: "file"') | ||
344 | 176 | 178 | ||
345 | === modified file 'glance/registry/db/sqlalchemy/api.py' | |||
346 | --- glance/registry/db/sqlalchemy/api.py 2010-12-18 18:00:21 +0000 | |||
347 | +++ glance/registry/db/sqlalchemy/api.py 2010-12-21 15:27:59 +0000 | |||
348 | @@ -52,10 +52,19 @@ | |||
349 | 52 | 52 | ||
350 | 53 | 53 | ||
351 | 54 | def image_create(_context, values): | 54 | def image_create(_context, values): |
352 | 55 | values['size'] = int(values['size']) | ||
353 | 56 | values['is_public'] = bool(values.get('is_public', False)) | ||
354 | 57 | properties = values.pop('properties', {}) | ||
355 | 58 | |||
356 | 55 | image_ref = models.Image() | 59 | image_ref = models.Image() |
357 | 56 | image_ref.update(values) | 60 | image_ref.update(values) |
358 | 57 | image_ref.save() | 61 | image_ref.save() |
360 | 58 | return image_ref | 62 | |
361 | 63 | for key, value in properties.iteritems(): | ||
362 | 64 | prop_values = {'image_id': image_ref.id, 'key': key, 'value': value} | ||
363 | 65 | image_property_create(_context, prop_values) | ||
364 | 66 | |||
365 | 67 | return image_get(_context, image_ref.id) | ||
366 | 59 | 68 | ||
367 | 60 | 69 | ||
368 | 61 | def image_destroy(_context, image_id): | 70 | def image_destroy(_context, image_id): |
369 | @@ -102,18 +111,25 @@ | |||
370 | 102 | def image_update(_context, image_id, values): | 111 | def image_update(_context, image_id, values): |
371 | 103 | session = get_session() | 112 | session = get_session() |
372 | 104 | with session.begin(): | 113 | with session.begin(): |
373 | 114 | values['size'] = int(values['size']) | ||
374 | 115 | values['is_public'] = bool(values.get('is_public', False)) | ||
375 | 116 | properties = values.pop('properties', {}) | ||
376 | 117 | |||
377 | 105 | image_ref = models.Image.find(image_id, session=session) | 118 | image_ref = models.Image.find(image_id, session=session) |
378 | 106 | image_ref.update(values) | 119 | image_ref.update(values) |
379 | 107 | image_ref.save(session=session) | 120 | image_ref.save(session=session) |
380 | 108 | 121 | ||
381 | 122 | for key, value in properties.iteritems(): | ||
382 | 123 | prop_values = {'image_id': image_ref.id, 'key': key, 'value': value} | ||
383 | 124 | image_property_create(_context, prop_values) | ||
384 | 125 | |||
385 | 109 | 126 | ||
386 | 110 | ################### | 127 | ################### |
387 | 111 | 128 | ||
388 | 112 | 129 | ||
389 | 113 | def image_file_create(_context, values): | 130 | def image_file_create(_context, values): |
390 | 114 | image_file_ref = models.ImageFile() | 131 | image_file_ref = models.ImageFile() |
393 | 115 | for (key, value) in values.iteritems(): | 132 | image_file_ref.update(values) |
392 | 116 | image_file_ref[key] = value | ||
394 | 117 | image_file_ref.save() | 133 | image_file_ref.save() |
395 | 118 | return image_file_ref | 134 | return image_file_ref |
396 | 119 | 135 | ||
397 | @@ -122,8 +138,7 @@ | |||
398 | 122 | 138 | ||
399 | 123 | 139 | ||
400 | 124 | def image_property_create(_context, values): | 140 | def image_property_create(_context, values): |
406 | 125 | image_properties_ref = models.ImageProperty() | 141 | image_property_ref = models.ImageProperty() |
407 | 126 | for (key, value) in values.iteritems(): | 142 | image_property_ref.update(values) |
408 | 127 | image_properties_ref[key] = value | 143 | image_property_ref.save() |
409 | 128 | image_properties_ref.save() | 144 | return image_property_ref |
405 | 129 | return image_properties_ref | ||
410 | 130 | 145 | ||
411 | === modified file 'glance/server.py' | |||
412 | --- glance/server.py 2010-12-16 22:04:58 +0000 | |||
413 | +++ glance/server.py 2010-12-21 15:27:59 +0000 | |||
414 | @@ -23,6 +23,10 @@ | |||
415 | 23 | Configuration Options | 23 | Configuration Options |
416 | 24 | --------------------- | 24 | --------------------- |
417 | 25 | 25 | ||
418 | 26 | `default_store`: When no x-image-meta-store header is sent for a | ||
419 | 27 | `POST /images` request, this store will be used | ||
420 | 28 | for storing the image data. Default: 'file' | ||
421 | 29 | |||
422 | 26 | """ | 30 | """ |
423 | 27 | 31 | ||
424 | 28 | import json | 32 | import json |
425 | @@ -39,7 +43,9 @@ | |||
426 | 39 | from glance.common import wsgi | 43 | from glance.common import wsgi |
427 | 40 | from glance.store import (get_from_backend, | 44 | from glance.store import (get_from_backend, |
428 | 41 | delete_from_backend, | 45 | delete_from_backend, |
430 | 42 | get_store_from_location) | 46 | get_store_from_location, |
431 | 47 | get_backend_class, | ||
432 | 48 | UnsupportedBackend) | ||
433 | 43 | from glance import registry | 49 | from glance import registry |
434 | 44 | from glance import util | 50 | from glance import util |
435 | 45 | 51 | ||
436 | @@ -174,9 +180,6 @@ | |||
437 | 174 | 180 | ||
438 | 175 | :param request: The WSGI/Webob Request object | 181 | :param request: The WSGI/Webob Request object |
439 | 176 | 182 | ||
440 | 177 | :see The `id_type` configuration option (default: uuid) determines | ||
441 | 178 | the type of identifier that Glance generates for an image | ||
442 | 179 | |||
443 | 180 | :raises HTTPBadRequest if no x-image-meta-location is missing | 183 | :raises HTTPBadRequest if no x-image-meta-location is missing |
444 | 181 | and the request body is not application/octet-stream | 184 | and the request body is not application/octet-stream |
445 | 182 | image data. | 185 | image data. |
446 | @@ -195,7 +198,7 @@ | |||
447 | 195 | "mime-encoded as application/" | 198 | "mime-encoded as application/" |
448 | 196 | "octet-stream.", request=req) | 199 | "octet-stream.", request=req) |
449 | 197 | else: | 200 | else: |
451 | 198 | if 'x-image-meta-store' in headers_keys: | 201 | if 'x-image-meta-store' in header_keys: |
452 | 199 | image_store = req.headers['x-image-meta-store'] | 202 | image_store = req.headers['x-image-meta-store'] |
453 | 200 | image_status = 'pending' # set to available when stored... | 203 | image_status = 'pending' # set to available when stored... |
454 | 201 | image_in_body = True | 204 | image_in_body = True |
455 | @@ -204,23 +207,34 @@ | |||
456 | 204 | image_store = get_store_from_location(image_location) | 207 | image_store = get_store_from_location(image_location) |
457 | 205 | image_status = 'available' | 208 | image_status = 'available' |
458 | 206 | 209 | ||
459 | 210 | # If image is the request body, validate that the requested | ||
460 | 211 | # or default store is capable of storing the image data... | ||
461 | 212 | if not image_store: | ||
462 | 213 | image_store = FLAGS.default_store | ||
463 | 214 | if image_in_body: | ||
464 | 215 | store = self.get_store_or_400(req, image_store) | ||
465 | 216 | |||
466 | 207 | image_meta = util.get_image_meta_from_headers(req) | 217 | image_meta = util.get_image_meta_from_headers(req) |
467 | 218 | |||
468 | 208 | image_meta['status'] = image_status | 219 | image_meta['status'] = image_status |
469 | 209 | image_meta['store'] = image_store | 220 | image_meta['store'] = image_store |
470 | 210 | |||
471 | 211 | try: | 221 | try: |
472 | 212 | image_meta = registry.add_image_metadata(image_meta) | 222 | image_meta = registry.add_image_metadata(image_meta) |
473 | 213 | 223 | ||
474 | 214 | if image_in_body: | 224 | if image_in_body: |
477 | 215 | #store = stores.get_store() | 225 | try: |
478 | 216 | #store.add_image(req.body) | 226 | location = store.add(image_meta['id'], req.body) |
479 | 227 | except exception.Duplicate, e: | ||
480 | 228 | return HTTPConflict(str(e), request=req) | ||
481 | 217 | image_meta['status'] = 'available' | 229 | image_meta['status'] = 'available' |
483 | 218 | registries.update_image(image_meta) | 230 | image_meta['location'] = location |
484 | 231 | registry.update_image_metadata(image_meta['id'], image_meta) | ||
485 | 219 | 232 | ||
486 | 220 | return dict(image=image_meta) | 233 | return dict(image=image_meta) |
487 | 221 | 234 | ||
488 | 222 | except exception.Duplicate: | 235 | except exception.Duplicate: |
490 | 223 | return HTTPConflict() | 236 | return HTTPConflict("An image with identifier %s already exists" |
491 | 237 | % image_meta['id'], request=req) | ||
492 | 224 | except exception.Invalid: | 238 | except exception.Invalid: |
493 | 225 | return HTTPBadRequest() | 239 | return HTTPBadRequest() |
494 | 226 | 240 | ||
495 | @@ -275,6 +289,25 @@ | |||
496 | 275 | request=request, | 289 | request=request, |
497 | 276 | content_type='text/plain') | 290 | content_type='text/plain') |
498 | 277 | 291 | ||
499 | 292 | def get_store_or_400(self, request, store_name): | ||
500 | 293 | """ | ||
501 | 294 | Grabs the storage backend for the supplied store name | ||
502 | 295 | or raises an HTTPBadRequest (400) response | ||
503 | 296 | |||
504 | 297 | :param request: The WSGI/Webob Request object | ||
505 | 298 | :param id: The opaque image identifier | ||
506 | 299 | |||
507 | 300 | :raises HTTPNotFound if image does not exist | ||
508 | 301 | """ | ||
509 | 302 | try: | ||
510 | 303 | return get_backend_class(store_name) | ||
511 | 304 | except UnsupportedBackend: | ||
512 | 305 | raise HTTPBadRequest(body='Requested store %s not available ' | ||
513 | 306 | 'for storage on this Glance node' | ||
514 | 307 | % store_name, | ||
515 | 308 | request=request, | ||
516 | 309 | content_type='text/plain') | ||
517 | 310 | |||
518 | 278 | 311 | ||
519 | 279 | class API(wsgi.Router): | 312 | class API(wsgi.Router): |
520 | 280 | 313 | ||
521 | 281 | 314 | ||
522 | === modified file 'glance/store/__init__.py' | |||
523 | --- glance/store/__init__.py 2010-12-14 19:34:04 +0000 | |||
524 | +++ glance/store/__init__.py 2010-12-21 15:27:59 +0000 | |||
525 | @@ -66,7 +66,7 @@ | |||
526 | 66 | try: | 66 | try: |
527 | 67 | return BACKENDS[backend] | 67 | return BACKENDS[backend] |
528 | 68 | except KeyError: | 68 | except KeyError: |
530 | 69 | raise UnsupportedBackend("No backend found for '%s'" % scheme) | 69 | raise UnsupportedBackend("No backend found for '%s'" % backend) |
531 | 70 | 70 | ||
532 | 71 | 71 | ||
533 | 72 | def get_from_backend(uri, **kwargs): | 72 | def get_from_backend(uri, **kwargs): |
534 | 73 | 73 | ||
535 | === modified file 'glance/store/filesystem.py' | |||
536 | --- glance/store/filesystem.py 2010-12-16 22:04:58 +0000 | |||
537 | +++ glance/store/filesystem.py 2010-12-21 15:27:59 +0000 | |||
538 | @@ -26,6 +26,11 @@ | |||
539 | 26 | from glance.common import flags | 26 | from glance.common import flags |
540 | 27 | import glance.store | 27 | import glance.store |
541 | 28 | 28 | ||
542 | 29 | |||
543 | 30 | flags.DEFINE_string('filesystem_store_datadir', '/var/lib/glance/images/', | ||
544 | 31 | 'Location to write image data. ' | ||
545 | 32 | 'Default: /var/lib/glance/images/') | ||
546 | 33 | |||
547 | 29 | FLAGS = flags.FLAGS | 34 | FLAGS = flags.FLAGS |
548 | 30 | 35 | ||
549 | 31 | 36 | ||
550 | @@ -94,4 +99,35 @@ | |||
551 | 94 | except OSError: | 99 | except OSError: |
552 | 95 | raise exception.NotAuthorized("You cannot delete file %s" % fn) | 100 | raise exception.NotAuthorized("You cannot delete file %s" % fn) |
553 | 96 | else: | 101 | else: |
555 | 97 | raise exception.NotFound("Image file %s does not exist" % fn) | 102 | raise exception.NotFound("Image file %s does not exist" % fn) |
556 | 103 | |||
557 | 104 | @classmethod | ||
558 | 105 | def add(cls, id, data): | ||
559 | 106 | """ | ||
560 | 107 | Stores image data to disk and returns a location that the image was | ||
561 | 108 | written to. By default, the backend writes the image data to a file | ||
562 | 109 | `/<DATADIR>/<ID>`, where <DATADIR> is the value of | ||
563 | 110 | FLAGS.filesystem_store_datadir and <ID> is the supplied image ID. | ||
564 | 111 | |||
565 | 112 | :param id: The opaque image identifier | ||
566 | 113 | :param data: The image data to write | ||
567 | 114 | |||
568 | 115 | :retval The location that was written, with file:// scheme prepended | ||
569 | 116 | """ | ||
570 | 117 | |||
571 | 118 | datadir = FLAGS.filesystem_store_datadir | ||
572 | 119 | |||
573 | 120 | if not os.path.exists(datadir): | ||
574 | 121 | os.makedirs(datadir) | ||
575 | 122 | |||
576 | 123 | filepath = os.path.join(datadir, str(id)) | ||
577 | 124 | |||
578 | 125 | if os.path.exists(filepath): | ||
579 | 126 | raise exception.Duplicate("Image file %s already exists!" | ||
580 | 127 | % filepath) | ||
581 | 128 | |||
582 | 129 | f = open(filepath, 'wb') | ||
583 | 130 | f.write(data) | ||
584 | 131 | f.close() | ||
585 | 132 | |||
586 | 133 | return 'file://%s' % filepath | ||
587 | 98 | 134 | ||
588 | === modified file 'tests/stubs.py' | |||
589 | --- tests/stubs.py 2010-12-18 18:00:21 +0000 | |||
590 | +++ tests/stubs.py 2010-12-21 15:27:59 +0000 | |||
591 | @@ -37,7 +37,7 @@ | |||
592 | 37 | import glance.registry.db.sqlalchemy.api | 37 | import glance.registry.db.sqlalchemy.api |
593 | 38 | 38 | ||
594 | 39 | 39 | ||
596 | 40 | FAKE_FILESYSTEM_ROOTDIR = os.path.join('//tmp', 'glance-tests') | 40 | FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests') |
597 | 41 | 41 | ||
598 | 42 | 42 | ||
599 | 43 | def stub_out_http_backend(stubs): | 43 | def stub_out_http_backend(stubs): |
600 | @@ -356,6 +356,22 @@ | |||
601 | 356 | return values | 356 | return values |
602 | 357 | 357 | ||
603 | 358 | def image_update(self, _context, image_id, values): | 358 | def image_update(self, _context, image_id, values): |
604 | 359 | |||
605 | 360 | props = [] | ||
606 | 361 | |||
607 | 362 | if 'properties' in values.keys(): | ||
608 | 363 | for k,v in values['properties'].iteritems(): | ||
609 | 364 | p = {} | ||
610 | 365 | p['key'] = k | ||
611 | 366 | p['value'] = v | ||
612 | 367 | p['deleted'] = False | ||
613 | 368 | p['created_at'] = datetime.datetime.utcnow() | ||
614 | 369 | p['updated_at'] = datetime.datetime.utcnow() | ||
615 | 370 | p['deleted_at'] = None | ||
616 | 371 | props.append(p) | ||
617 | 372 | |||
618 | 373 | values['properties'] = props | ||
619 | 374 | |||
620 | 359 | image = self.image_get(_context, image_id) | 375 | image = self.image_get(_context, image_id) |
621 | 360 | image.update(values) | 376 | image.update(values) |
622 | 361 | return image | 377 | return image |
623 | 362 | 378 | ||
624 | === modified file 'tests/unit/test_api.py' | |||
625 | --- tests/unit/test_api.py 2010-12-18 18:00:21 +0000 | |||
626 | +++ tests/unit/test_api.py 2010-12-21 15:27:59 +0000 | |||
627 | @@ -22,11 +22,14 @@ | |||
628 | 22 | import webob | 22 | import webob |
629 | 23 | 23 | ||
630 | 24 | from glance import server | 24 | from glance import server |
631 | 25 | from glance.common import flags | ||
632 | 25 | from glance.registry import server as rserver | 26 | from glance.registry import server as rserver |
633 | 26 | from tests import stubs | 27 | from tests import stubs |
634 | 27 | 28 | ||
637 | 28 | 29 | FLAGS = flags.FLAGS | |
638 | 29 | class TestImageController(unittest.TestCase): | 30 | |
639 | 31 | |||
640 | 32 | class TestRegistryAPI(unittest.TestCase): | ||
641 | 30 | def setUp(self): | 33 | def setUp(self): |
642 | 31 | """Establish a clean test environment""" | 34 | """Establish a clean test environment""" |
643 | 32 | self.stubs = stubout.StubOutForTesting() | 35 | self.stubs = stubout.StubOutForTesting() |
644 | @@ -230,7 +233,71 @@ | |||
645 | 230 | self.assertEquals(res.status_int, | 233 | self.assertEquals(res.status_int, |
646 | 231 | webob.exc.HTTPNotFound.code) | 234 | webob.exc.HTTPNotFound.code) |
647 | 232 | 235 | ||
648 | 236 | |||
649 | 237 | class TestGlanceAPI(unittest.TestCase): | ||
650 | 238 | def setUp(self): | ||
651 | 239 | """Establish a clean test environment""" | ||
652 | 240 | self.stubs = stubout.StubOutForTesting() | ||
653 | 241 | stubs.stub_out_registry_and_store_server(self.stubs) | ||
654 | 242 | stubs.stub_out_registry_db_image_api(self.stubs) | ||
655 | 243 | stubs.stub_out_filesystem_backend() | ||
656 | 244 | self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir | ||
657 | 245 | FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR | ||
658 | 246 | |||
659 | 247 | def tearDown(self): | ||
660 | 248 | """Clear the test environment""" | ||
661 | 249 | FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir | ||
662 | 250 | stubs.clean_out_fake_filesystem_backend() | ||
663 | 251 | self.stubs.UnsetAll() | ||
664 | 252 | |||
665 | 253 | def test_add_image_no_location_no_image_as_body(self): | ||
666 | 254 | """Tests raises BadRequest for no body and no loc header""" | ||
667 | 255 | fixture_headers = {'x-image-meta-store': 'file', | ||
668 | 256 | 'x-image-meta-name': 'fake image #3'} | ||
669 | 257 | |||
670 | 258 | req = webob.Request.blank("/images") | ||
671 | 259 | req.method = 'POST' | ||
672 | 260 | for k,v in fixture_headers.iteritems(): | ||
673 | 261 | req.headers[k] = v | ||
674 | 262 | res = req.get_response(server.API()) | ||
675 | 263 | self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) | ||
676 | 264 | |||
677 | 265 | def test_add_image_bad_store(self): | ||
678 | 266 | """Tests raises BadRequest for invalid store header""" | ||
679 | 267 | fixture_headers = {'x-image-meta-store': 'bad', | ||
680 | 268 | 'x-image-meta-name': 'fake image #3'} | ||
681 | 269 | |||
682 | 270 | req = webob.Request.blank("/images") | ||
683 | 271 | req.method = 'POST' | ||
684 | 272 | for k,v in fixture_headers.iteritems(): | ||
685 | 273 | req.headers[k] = v | ||
686 | 274 | |||
687 | 275 | req.headers['Content-Type'] = 'application/octet-stream' | ||
688 | 276 | req.body = "chunk00000remainder" | ||
689 | 277 | res = req.get_response(server.API()) | ||
690 | 278 | self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) | ||
691 | 279 | |||
692 | 280 | def test_add_image_basic_file_store(self): | ||
693 | 281 | """Tests raises BadRequest for invalid store header""" | ||
694 | 282 | fixture_headers = {'x-image-meta-store': 'file', | ||
695 | 283 | 'x-image-meta-name': 'fake image #3'} | ||
696 | 284 | |||
697 | 285 | req = webob.Request.blank("/images") | ||
698 | 286 | req.method = 'POST' | ||
699 | 287 | for k,v in fixture_headers.iteritems(): | ||
700 | 288 | req.headers[k] = v | ||
701 | 289 | |||
702 | 290 | req.headers['Content-Type'] = 'application/octet-stream' | ||
703 | 291 | req.body = "chunk00000remainder" | ||
704 | 292 | res = req.get_response(server.API()) | ||
705 | 293 | self.assertEquals(res.status_int, 200) | ||
706 | 294 | |||
707 | 295 | res_body = json.loads(res.body)['image'] | ||
708 | 296 | self.assertEquals(res_body['location'], | ||
709 | 297 | 'file:///tmp/glance-tests/3') | ||
710 | 298 | |||
711 | 233 | def test_image_meta(self): | 299 | def test_image_meta(self): |
712 | 300 | """Test for HEAD /images/<ID>""" | ||
713 | 234 | expected_headers = {'x-image-meta-id': 2, | 301 | expected_headers = {'x-image-meta-id': 2, |
714 | 235 | 'x-image-meta-name': 'fake image #2'} | 302 | 'x-image-meta-name': 'fake image #2'} |
715 | 236 | req = webob.Request.blank("/images/2") | 303 | req = webob.Request.blank("/images/2") |
716 | 237 | 304 | ||
717 | === modified file 'tests/unit/test_clients.py' | |||
718 | --- tests/unit/test_clients.py 2010-12-16 22:04:58 +0000 | |||
719 | +++ tests/unit/test_clients.py 2010-12-21 15:27:59 +0000 | |||
720 | @@ -16,6 +16,7 @@ | |||
721 | 16 | # under the License. | 16 | # under the License. |
722 | 17 | 17 | ||
723 | 18 | import json | 18 | import json |
724 | 19 | import os | ||
725 | 19 | import stubout | 20 | import stubout |
726 | 20 | import StringIO | 21 | import StringIO |
727 | 21 | import unittest | 22 | import unittest |
728 | @@ -24,9 +25,12 @@ | |||
729 | 24 | 25 | ||
730 | 25 | from glance import client | 26 | from glance import client |
731 | 26 | from glance.registry import client as rclient | 27 | from glance.registry import client as rclient |
732 | 28 | from glance.common import flags | ||
733 | 27 | from glance.common import exception | 29 | from glance.common import exception |
734 | 28 | from tests import stubs | 30 | from tests import stubs |
735 | 29 | 31 | ||
736 | 32 | FLAGS = flags.FLAGS | ||
737 | 33 | |||
738 | 30 | 34 | ||
739 | 31 | class TestBadClients(unittest.TestCase): | 35 | class TestBadClients(unittest.TestCase): |
740 | 32 | 36 | ||
741 | @@ -40,7 +44,7 @@ | |||
742 | 40 | 1) | 44 | 1) |
743 | 41 | 45 | ||
744 | 42 | def test_bad_address(self): | 46 | def test_bad_address(self): |
746 | 43 | """Test unsupported protocol raised""" | 47 | """Test ClientConnectionError raised""" |
747 | 44 | c = client.Client(address="http://127.999.1.1/") | 48 | c = client.Client(address="http://127.999.1.1/") |
748 | 45 | self.assertRaises(client.ClientConnectionError, | 49 | self.assertRaises(client.ClientConnectionError, |
749 | 46 | c.get_image, | 50 | c.get_image, |
750 | @@ -212,7 +216,7 @@ | |||
751 | 212 | 'location': "file:///tmp/glance-tests/2", | 216 | 'location': "file:///tmp/glance-tests/2", |
752 | 213 | } | 217 | } |
753 | 214 | 218 | ||
755 | 215 | self.assertRaises(client.BadInputError, | 219 | self.assertRaises(exception.BadInputError, |
756 | 216 | self.client.add_image, | 220 | self.client.add_image, |
757 | 217 | fixture) | 221 | fixture) |
758 | 218 | 222 | ||
759 | @@ -279,10 +283,13 @@ | |||
760 | 279 | stubs.stub_out_registry_db_image_api(self.stubs) | 283 | stubs.stub_out_registry_db_image_api(self.stubs) |
761 | 280 | stubs.stub_out_registry_and_store_server(self.stubs) | 284 | stubs.stub_out_registry_and_store_server(self.stubs) |
762 | 281 | stubs.stub_out_filesystem_backend() | 285 | stubs.stub_out_filesystem_backend() |
763 | 286 | self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir | ||
764 | 287 | FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR | ||
765 | 282 | self.client = client.Client() | 288 | self.client = client.Client() |
766 | 283 | 289 | ||
767 | 284 | def tearDown(self): | 290 | def tearDown(self): |
768 | 285 | """Clear the test environment""" | 291 | """Clear the test environment""" |
769 | 292 | FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir | ||
770 | 286 | stubs.clean_out_fake_filesystem_backend() | 293 | stubs.clean_out_fake_filesystem_backend() |
771 | 287 | self.stubs.UnsetAll() | 294 | self.stubs.UnsetAll() |
772 | 288 | 295 | ||
773 | @@ -480,6 +487,85 @@ | |||
774 | 480 | 487 | ||
775 | 481 | self.assertEquals(data['status'], 'available') | 488 | self.assertEquals(data['status'], 'available') |
776 | 482 | 489 | ||
777 | 490 | def test_add_image_with_image_data_as_string(self): | ||
778 | 491 | """Tests can add image by passing image data as string""" | ||
779 | 492 | fixture = {'name': 'fake public image', | ||
780 | 493 | 'is_public': True, | ||
781 | 494 | 'type': 'kernel', | ||
782 | 495 | 'size': 19, | ||
783 | 496 | 'properties': {'distro': 'Ubuntu 10.04 LTS'} | ||
784 | 497 | } | ||
785 | 498 | |||
786 | 499 | image_data_fixture = r"chunk0000remainder" | ||
787 | 500 | |||
788 | 501 | new_id = self.client.add_image(fixture, image_data_fixture) | ||
789 | 502 | |||
790 | 503 | self.assertEquals(3, new_id) | ||
791 | 504 | |||
792 | 505 | new_meta, new_image_chunks = self.client.get_image(3) | ||
793 | 506 | |||
794 | 507 | new_image_data = "" | ||
795 | 508 | for image_chunk in new_image_chunks: | ||
796 | 509 | new_image_data += image_chunk | ||
797 | 510 | |||
798 | 511 | self.assertEquals(image_data_fixture, new_image_data) | ||
799 | 512 | for k,v in fixture.iteritems(): | ||
800 | 513 | self.assertEquals(v, new_meta[k]) | ||
801 | 514 | |||
802 | 515 | def test_add_image_with_image_data_as_file(self): | ||
803 | 516 | """Tests can add image by passing image data as file""" | ||
804 | 517 | fixture = {'name': 'fake public image', | ||
805 | 518 | 'is_public': True, | ||
806 | 519 | 'type': 'kernel', | ||
807 | 520 | 'size': 19, | ||
808 | 521 | 'properties': {'distro': 'Ubuntu 10.04 LTS'} | ||
809 | 522 | } | ||
810 | 523 | |||
811 | 524 | image_data_fixture = r"chunk0000remainder" | ||
812 | 525 | |||
813 | 526 | tmp_image_filepath = '/tmp/rubbish-image' | ||
814 | 527 | |||
815 | 528 | if os.path.exists(tmp_image_filepath): | ||
816 | 529 | os.unlink(tmp_image_filepath) | ||
817 | 530 | |||
818 | 531 | tmp_file = open(tmp_image_filepath, 'wb') | ||
819 | 532 | tmp_file.write(image_data_fixture) | ||
820 | 533 | tmp_file.close() | ||
821 | 534 | |||
822 | 535 | new_id = self.client.add_image(fixture, open(tmp_image_filepath)) | ||
823 | 536 | |||
824 | 537 | self.assertEquals(3, new_id) | ||
825 | 538 | |||
826 | 539 | if os.path.exists(tmp_image_filepath): | ||
827 | 540 | os.unlink(tmp_image_filepath) | ||
828 | 541 | |||
829 | 542 | new_meta, new_image_chunks = self.client.get_image(3) | ||
830 | 543 | |||
831 | 544 | new_image_data = "" | ||
832 | 545 | for image_chunk in new_image_chunks: | ||
833 | 546 | new_image_data += image_chunk | ||
834 | 547 | |||
835 | 548 | self.assertEquals(image_data_fixture, new_image_data) | ||
836 | 549 | for k,v in fixture.iteritems(): | ||
837 | 550 | self.assertEquals(v, new_meta[k]) | ||
838 | 551 | |||
839 | 552 | def test_add_image_with_bad_store(self): | ||
840 | 553 | """Tests BadRequest raised when supplying bad store name in meta""" | ||
841 | 554 | fixture = {'name': 'fake public image', | ||
842 | 555 | 'is_public': True, | ||
843 | 556 | 'type': 'kernel', | ||
844 | 557 | 'size': 19, | ||
845 | 558 | 'store': 'bad', | ||
846 | 559 | 'properties': {'distro': 'Ubuntu 10.04 LTS'} | ||
847 | 560 | } | ||
848 | 561 | |||
849 | 562 | image_data_fixture = r"chunk0000remainder" | ||
850 | 563 | |||
851 | 564 | self.assertRaises(exception.BadInputError, | ||
852 | 565 | self.client.add_image, | ||
853 | 566 | fixture, | ||
854 | 567 | image_data_fixture) | ||
855 | 568 | |||
856 | 483 | def test_update_image(self): | 569 | def test_update_image(self): |
857 | 484 | """Tests that the /images PUT registry API updates the image""" | 570 | """Tests that the /images PUT registry API updates the image""" |
858 | 485 | fixture = {'name': 'fake public image #2', | 571 | fixture = {'name': 'fake public image #2', |
lgtm