Merge lp:~jaypipes/nova/glance-image-service into lp:~hudson-openstack/nova/trunk
- glance-image-service
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 325 |
Merged at revision: | 330 |
Proposed branch: | lp:~jaypipes/nova/glance-image-service |
Merge into: | lp:~hudson-openstack/nova/trunk |
Prerequisite: | lp:~jaypipes/nova/cleanup-for-nosetests |
Diff against target: |
594 lines (+420/-41) 7 files modified
nova/api/rackspace/images.py (+6/-1) nova/api/rackspace/servers.py (+1/-1) nova/flags.py (+4/-0) nova/image/service.py (+215/-22) nova/tests/api/rackspace/fakes.py (+74/-0) nova/tests/api/rackspace/test_images.py (+119/-17) nova/tests/api/rackspace/test_servers.py (+1/-0) |
To merge this branch: | bzr merge lp:~jaypipes/nova/glance-image-service |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Gundlach (community) | Approve | ||
Rick Harris (community) | Approve | ||
Christopher MacGown (community) | Approve | ||
Review via email: mp+37523@code.launchpad.net |
Commit message
Adds stubs and tests for GlanceImageService and LocalImageService.
Adds basic plumbing for ParallaxClient and TellerClient and hooks that into the GlanceImageService.
Fixes lp654843
Description of the change
Adds stubs and tests for GlanceImageService and LocalImageService.
Adds basic plumbing for ParallaxClient and TellerClient and hooks that into the GlanceImageService.
Fixes lp654843
Rick Harris (rconradharris) wrote : | # |
Overall, looks excellent.
I think the Parallax get_image and get_image_metadata might need a slight modification. Currently, Parallax returns the JSON with the form dict(image=
try:
return json.loads(
except KeyError:
raise ImageServiceExc
(The reason I return the json as dict(image=
A teensy correction:
186 + # TODO(jaypipes): return or raise HTTP error?
187 + return []
get_image_metadata should return None if image is not found/available.
Jay Pipes (jaypipes) wrote : | # |
Got it, thanks Rick. Will fix up shortly.
Jay Pipes (jaypipes) wrote : | # |
OK, changes made and a new unit test added...
Rick Harris (rconradharris) wrote : | # |
Looks good, thanks :)
Preview Diff
1 | === modified file 'nova/api/rackspace/images.py' |
2 | --- nova/api/rackspace/images.py 2010-09-29 16:27:56 +0000 |
3 | +++ nova/api/rackspace/images.py 2010-10-05 20:21:00 +0000 |
4 | @@ -17,12 +17,17 @@ |
5 | |
6 | from webob import exc |
7 | |
8 | +from nova import flags |
9 | +from nova import utils |
10 | from nova import wsgi |
11 | from nova.api.rackspace import _id_translator |
12 | import nova.api.rackspace |
13 | import nova.image.service |
14 | from nova.api.rackspace import faults |
15 | |
16 | + |
17 | +FLAGS = flags.FLAGS |
18 | + |
19 | class Controller(wsgi.Controller): |
20 | |
21 | _serialization_metadata = { |
22 | @@ -35,7 +40,7 @@ |
23 | } |
24 | |
25 | def __init__(self): |
26 | - self._service = nova.image.service.ImageService.load() |
27 | + self._service = utils.import_object(FLAGS.image_service) |
28 | self._id_translator = _id_translator.RackspaceAPIIdTranslator( |
29 | "image", self._service.__class__.__name__) |
30 | |
31 | |
32 | === modified file 'nova/api/rackspace/servers.py' |
33 | --- nova/api/rackspace/servers.py 2010-10-05 20:07:11 +0000 |
34 | +++ nova/api/rackspace/servers.py 2010-10-05 20:21:00 +0000 |
35 | @@ -42,7 +42,7 @@ |
36 | |
37 | def _image_service(): |
38 | """ Helper method for initializing the image id translator """ |
39 | - service = nova.image.service.ImageService.load() |
40 | + service = utils.import_object(FLAGS.image_service) |
41 | return (service, _id_translator.RackspaceAPIIdTranslator( |
42 | "image", service.__class__.__name__)) |
43 | |
44 | |
45 | === modified file 'nova/flags.py' |
46 | --- nova/flags.py 2010-09-29 00:53:27 +0000 |
47 | +++ nova/flags.py 2010-10-05 20:21:00 +0000 |
48 | @@ -222,6 +222,10 @@ |
49 | DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager', |
50 | 'Manager for scheduler') |
51 | |
52 | +# The service to use for image search and retrieval |
53 | +DEFINE_string('image_service', 'nova.image.service.LocalImageService', |
54 | + 'The service to use for retrieving and searching for images.') |
55 | + |
56 | DEFINE_string('host', socket.gethostname(), |
57 | 'name of this node') |
58 | |
59 | |
60 | === modified file 'nova/image/service.py' |
61 | --- nova/image/service.py 2010-09-13 15:53:53 +0000 |
62 | +++ nova/image/service.py 2010-10-05 20:21:00 +0000 |
63 | @@ -16,38 +16,215 @@ |
64 | # under the License. |
65 | |
66 | import cPickle as pickle |
67 | +import httplib |
68 | +import json |
69 | +import logging |
70 | import os.path |
71 | import random |
72 | import string |
73 | - |
74 | -class ImageService(object): |
75 | - """Provides storage and retrieval of disk image objects.""" |
76 | - |
77 | - @staticmethod |
78 | - def load(): |
79 | - """Factory method to return image service.""" |
80 | - #TODO(gundlach): read from config. |
81 | - class_ = LocalImageService |
82 | - return class_() |
83 | +import urlparse |
84 | + |
85 | +import webob.exc |
86 | + |
87 | +from nova import utils |
88 | +from nova import flags |
89 | +from nova import exception |
90 | + |
91 | + |
92 | +FLAGS = flags.FLAGS |
93 | + |
94 | + |
95 | +flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1', |
96 | + 'IP address or URL where Glance\'s Teller service resides') |
97 | +flags.DEFINE_string('glance_teller_port', '9191', |
98 | + 'Port for Glance\'s Teller service') |
99 | +flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1', |
100 | + 'IP address or URL where Glance\'s Parallax service resides') |
101 | +flags.DEFINE_string('glance_parallax_port', '9292', |
102 | + 'Port for Glance\'s Parallax service') |
103 | + |
104 | + |
105 | +class BaseImageService(object): |
106 | + |
107 | + """Base class for providing image search and retrieval services""" |
108 | |
109 | def index(self): |
110 | """ |
111 | Return a dict from opaque image id to image data. |
112 | """ |
113 | + raise NotImplementedError |
114 | |
115 | def show(self, id): |
116 | """ |
117 | Returns a dict containing image data for the given opaque image id. |
118 | - """ |
119 | - |
120 | - |
121 | -class GlanceImageService(ImageService): |
122 | + |
123 | + :raises NotFound if the image does not exist |
124 | + """ |
125 | + raise NotImplementedError |
126 | + |
127 | + def create(self, data): |
128 | + """ |
129 | + Store the image data and return the new image id. |
130 | + |
131 | + :raises AlreadyExists if the image already exist. |
132 | + |
133 | + """ |
134 | + raise NotImplementedError |
135 | + |
136 | + def update(self, image_id, data): |
137 | + """Replace the contents of the given image with the new data. |
138 | + |
139 | + :raises NotFound if the image does not exist. |
140 | + |
141 | + """ |
142 | + raise NotImplementedError |
143 | + |
144 | + def delete(self, image_id): |
145 | + """ |
146 | + Delete the given image. |
147 | + |
148 | + :raises NotFound if the image does not exist. |
149 | + |
150 | + """ |
151 | + raise NotImplementedError |
152 | + |
153 | + |
154 | +class TellerClient(object): |
155 | + |
156 | + def __init__(self): |
157 | + self.address = FLAGS.glance_teller_address |
158 | + self.port = FLAGS.glance_teller_port |
159 | + url = urlparse.urlparse(self.address) |
160 | + self.netloc = url.netloc |
161 | + self.connection_type = {'http': httplib.HTTPConnection, |
162 | + 'https': httplib.HTTPSConnection}[url.scheme] |
163 | + |
164 | + |
165 | +class ParallaxClient(object): |
166 | + |
167 | + def __init__(self): |
168 | + self.address = FLAGS.glance_parallax_address |
169 | + self.port = FLAGS.glance_parallax_port |
170 | + url = urlparse.urlparse(self.address) |
171 | + self.netloc = url.netloc |
172 | + self.connection_type = {'http': httplib.HTTPConnection, |
173 | + 'https': httplib.HTTPSConnection}[url.scheme] |
174 | + |
175 | + def get_images(self): |
176 | + """ |
177 | + Returns a list of image data mappings from Parallax |
178 | + """ |
179 | + try: |
180 | + c = self.connection_type(self.netloc, self.port) |
181 | + c.request("GET", "images") |
182 | + res = c.getresponse() |
183 | + if res.status == 200: |
184 | + # Parallax returns a JSONified dict(images=image_list) |
185 | + data = json.loads(res.read())['images'] |
186 | + return data |
187 | + else: |
188 | + logging.warn("Parallax returned HTTP error %d from " |
189 | + "request for /images", res.status_int) |
190 | + return [] |
191 | + finally: |
192 | + c.close() |
193 | + |
194 | + def get_image_metadata(self, image_id): |
195 | + """ |
196 | + Returns a mapping of image metadata from Parallax |
197 | + """ |
198 | + try: |
199 | + c = self.connection_type(self.netloc, self.port) |
200 | + c.request("GET", "images/%s" % image_id) |
201 | + res = c.getresponse() |
202 | + if res.status == 200: |
203 | + # Parallax returns a JSONified dict(image=image_info) |
204 | + data = json.loads(res.read())['image'] |
205 | + return data |
206 | + else: |
207 | + # TODO(jaypipes): log the error? |
208 | + return None |
209 | + finally: |
210 | + c.close() |
211 | + |
212 | + def add_image_metadata(self, image_metadata): |
213 | + """ |
214 | + Tells parallax about an image's metadata |
215 | + """ |
216 | + pass |
217 | + |
218 | + def update_image_metadata(self, image_id, image_metadata): |
219 | + """ |
220 | + Updates Parallax's information about an image |
221 | + """ |
222 | + pass |
223 | + |
224 | + def delete_image_metadata(self, image_id): |
225 | + """ |
226 | + Deletes Parallax's information about an image |
227 | + """ |
228 | + pass |
229 | + |
230 | + |
231 | +class GlanceImageService(BaseImageService): |
232 | + |
233 | """Provides storage and retrieval of disk image objects within Glance.""" |
234 | - # TODO(gundlach): once Glance has an API, build this. |
235 | - pass |
236 | - |
237 | - |
238 | -class LocalImageService(ImageService): |
239 | + |
240 | + def __init__(self): |
241 | + self.teller = TellerClient() |
242 | + self.parallax = ParallaxClient() |
243 | + |
244 | + def index(self): |
245 | + """ |
246 | + Calls out to Parallax for a list of images available |
247 | + """ |
248 | + images = self.parallax.get_images() |
249 | + return images |
250 | + |
251 | + def show(self, id): |
252 | + """ |
253 | + Returns a dict containing image data for the given opaque image id. |
254 | + """ |
255 | + image = self.parallax.get_image_metadata(id) |
256 | + if image: |
257 | + return image |
258 | + raise exception.NotFound |
259 | + |
260 | + def create(self, data): |
261 | + """ |
262 | + Store the image data and return the new image id. |
263 | + |
264 | + :raises AlreadyExists if the image already exist. |
265 | + |
266 | + """ |
267 | + return self.parallax.add_image_metadata(data) |
268 | + |
269 | + def update(self, image_id, data): |
270 | + """Replace the contents of the given image with the new data. |
271 | + |
272 | + :raises NotFound if the image does not exist. |
273 | + |
274 | + """ |
275 | + self.parallax.update_image_metadata(image_id, data) |
276 | + |
277 | + def delete(self, image_id): |
278 | + """ |
279 | + Delete the given image. |
280 | + |
281 | + :raises NotFound if the image does not exist. |
282 | + |
283 | + """ |
284 | + self.parallax.delete_image_metadata(image_id) |
285 | + |
286 | + def delete_all(self): |
287 | + """ |
288 | + Clears out all images |
289 | + """ |
290 | + pass |
291 | + |
292 | + |
293 | +class LocalImageService(BaseImageService): |
294 | + |
295 | """Image service storing images to local disk.""" |
296 | |
297 | def __init__(self): |
298 | @@ -68,7 +245,10 @@ |
299 | return [ self.show(id) for id in self._ids() ] |
300 | |
301 | def show(self, id): |
302 | - return pickle.load(open(self._path_to(id))) |
303 | + try: |
304 | + return pickle.load(open(self._path_to(id))) |
305 | + except IOError: |
306 | + raise exception.NotFound |
307 | |
308 | def create(self, data): |
309 | """ |
310 | @@ -81,10 +261,23 @@ |
311 | |
312 | def update(self, image_id, data): |
313 | """Replace the contents of the given image with the new data.""" |
314 | - pickle.dump(data, open(self._path_to(image_id), 'w')) |
315 | + try: |
316 | + pickle.dump(data, open(self._path_to(image_id), 'w')) |
317 | + except IOError: |
318 | + raise exception.NotFound |
319 | |
320 | def delete(self, image_id): |
321 | """ |
322 | Delete the given image. Raises OSError if the image does not exist. |
323 | """ |
324 | - os.unlink(self._path_to(image_id)) |
325 | + try: |
326 | + os.unlink(self._path_to(image_id)) |
327 | + except IOError: |
328 | + raise exception.NotFound |
329 | + |
330 | + def delete_all(self): |
331 | + """ |
332 | + Clears out all images in local directory |
333 | + """ |
334 | + for f in os.listdir(self._path): |
335 | + os.unlink(self._path_to(f)) |
336 | |
337 | === modified file 'nova/tests/api/rackspace/fakes.py' |
338 | --- nova/tests/api/rackspace/fakes.py 2010-10-01 18:02:51 +0000 |
339 | +++ nova/tests/api/rackspace/fakes.py 2010-10-05 20:21:00 +0000 |
340 | @@ -1,5 +1,24 @@ |
341 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
342 | + |
343 | +# Copyright 2010 OpenStack LLC. |
344 | +# All Rights Reserved. |
345 | +# |
346 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
347 | +# not use this file except in compliance with the License. You may obtain |
348 | +# a copy of the License at |
349 | +# |
350 | +# http://www.apache.org/licenses/LICENSE-2.0 |
351 | +# |
352 | +# Unless required by applicable law or agreed to in writing, software |
353 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
354 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
355 | +# License for the specific language governing permissions and limitations |
356 | +# under the License. |
357 | + |
358 | import datetime |
359 | import json |
360 | +import random |
361 | +import string |
362 | |
363 | import webob |
364 | import webob.dec |
365 | @@ -7,6 +26,7 @@ |
366 | from nova import auth |
367 | from nova import utils |
368 | from nova import flags |
369 | +from nova import exception as exc |
370 | import nova.api.rackspace.auth |
371 | import nova.api.rackspace._id_translator |
372 | from nova.image import service |
373 | @@ -105,6 +125,60 @@ |
374 | FLAGS.FAKE_subdomain = 'rs' |
375 | |
376 | |
377 | +def stub_out_glance(stubs): |
378 | + |
379 | + class FakeParallaxClient: |
380 | + |
381 | + def __init__(self): |
382 | + self.fixtures = {} |
383 | + |
384 | + def fake_get_images(self): |
385 | + return self.fixtures |
386 | + |
387 | + def fake_get_image_metadata(self, image_id): |
388 | + for k, f in self.fixtures.iteritems(): |
389 | + if k == image_id: |
390 | + return f |
391 | + return None |
392 | + |
393 | + def fake_add_image_metadata(self, image_data): |
394 | + id = ''.join(random.choice(string.letters) for _ in range(20)) |
395 | + image_data['id'] = id |
396 | + self.fixtures[id] = image_data |
397 | + return id |
398 | + |
399 | + def fake_update_image_metadata(self, image_id, image_data): |
400 | + |
401 | + if image_id not in self.fixtures.keys(): |
402 | + raise exc.NotFound |
403 | + |
404 | + self.fixtures[image_id].update(image_data) |
405 | + |
406 | + def fake_delete_image_metadata(self, image_id): |
407 | + |
408 | + if image_id not in self.fixtures.keys(): |
409 | + raise exc.NotFound |
410 | + |
411 | + del self.fixtures[image_id] |
412 | + |
413 | + def fake_delete_all(self): |
414 | + self.fixtures = {} |
415 | + |
416 | + fake_parallax_client = FakeParallaxClient() |
417 | + stubs.Set(nova.image.service.ParallaxClient, 'get_images', |
418 | + fake_parallax_client.fake_get_images) |
419 | + stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata', |
420 | + fake_parallax_client.fake_get_image_metadata) |
421 | + stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata', |
422 | + fake_parallax_client.fake_add_image_metadata) |
423 | + stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata', |
424 | + fake_parallax_client.fake_update_image_metadata) |
425 | + stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata', |
426 | + fake_parallax_client.fake_delete_image_metadata) |
427 | + stubs.Set(nova.image.service.GlanceImageService, 'delete_all', |
428 | + fake_parallax_client.fake_delete_all) |
429 | + |
430 | + |
431 | class FakeAuthDatabase(object): |
432 | data = {} |
433 | |
434 | |
435 | === modified file 'nova/tests/api/rackspace/test_images.py' |
436 | --- nova/tests/api/rackspace/test_images.py 2010-10-01 18:02:51 +0000 |
437 | +++ nova/tests/api/rackspace/test_images.py 2010-10-05 20:21:00 +0000 |
438 | @@ -15,25 +15,127 @@ |
439 | # License for the specific language governing permissions and limitations |
440 | # under the License. |
441 | |
442 | +import logging |
443 | import unittest |
444 | |
445 | import stubout |
446 | |
447 | +from nova import exception |
448 | +from nova import utils |
449 | from nova.api.rackspace import images |
450 | - |
451 | - |
452 | -class ImagesTest(unittest.TestCase): |
453 | - def setUp(self): |
454 | - self.stubs = stubout.StubOutForTesting() |
455 | - |
456 | - def tearDown(self): |
457 | - self.stubs.UnsetAll() |
458 | - |
459 | - def test_get_image_list(self): |
460 | - pass |
461 | - |
462 | - def test_delete_image(self): |
463 | - pass |
464 | - |
465 | - def test_create_image(self): |
466 | - pass |
467 | +from nova.tests.api.rackspace import fakes |
468 | + |
469 | + |
470 | +class BaseImageServiceTests(): |
471 | + |
472 | + """Tasks to test for all image services""" |
473 | + |
474 | + def test_create(self): |
475 | + |
476 | + fixture = {'name': 'test image', |
477 | + 'updated': None, |
478 | + 'created': None, |
479 | + 'status': None, |
480 | + 'serverId': None, |
481 | + 'progress': None} |
482 | + |
483 | + num_images = len(self.service.index()) |
484 | + |
485 | + id = self.service.create(fixture) |
486 | + |
487 | + self.assertNotEquals(None, id) |
488 | + self.assertEquals(num_images + 1, len(self.service.index())) |
489 | + |
490 | + def test_create_and_show_non_existing_image(self): |
491 | + |
492 | + fixture = {'name': 'test image', |
493 | + 'updated': None, |
494 | + 'created': None, |
495 | + 'status': None, |
496 | + 'serverId': None, |
497 | + 'progress': None} |
498 | + |
499 | + num_images = len(self.service.index()) |
500 | + |
501 | + id = self.service.create(fixture) |
502 | + |
503 | + self.assertNotEquals(None, id) |
504 | + |
505 | + self.assertRaises(exception.NotFound, |
506 | + self.service.show, |
507 | + 'bad image id') |
508 | + |
509 | + def test_update(self): |
510 | + |
511 | + fixture = {'name': 'test image', |
512 | + 'updated': None, |
513 | + 'created': None, |
514 | + 'status': None, |
515 | + 'serverId': None, |
516 | + 'progress': None} |
517 | + |
518 | + id = self.service.create(fixture) |
519 | + |
520 | + fixture['status'] = 'in progress' |
521 | + |
522 | + self.service.update(id, fixture) |
523 | + new_image_data = self.service.show(id) |
524 | + self.assertEquals('in progress', new_image_data['status']) |
525 | + |
526 | + def test_delete(self): |
527 | + |
528 | + fixtures = [ |
529 | + {'name': 'test image 1', |
530 | + 'updated': None, |
531 | + 'created': None, |
532 | + 'status': None, |
533 | + 'serverId': None, |
534 | + 'progress': None}, |
535 | + {'name': 'test image 2', |
536 | + 'updated': None, |
537 | + 'created': None, |
538 | + 'status': None, |
539 | + 'serverId': None, |
540 | + 'progress': None}] |
541 | + |
542 | + ids = [] |
543 | + for fixture in fixtures: |
544 | + new_id = self.service.create(fixture) |
545 | + ids.append(new_id) |
546 | + |
547 | + num_images = len(self.service.index()) |
548 | + self.assertEquals(2, num_images) |
549 | + |
550 | + self.service.delete(ids[0]) |
551 | + |
552 | + num_images = len(self.service.index()) |
553 | + self.assertEquals(1, num_images) |
554 | + |
555 | + |
556 | +class LocalImageServiceTest(unittest.TestCase, |
557 | + BaseImageServiceTests): |
558 | + |
559 | + """Tests the local image service""" |
560 | + |
561 | + def setUp(self): |
562 | + self.stubs = stubout.StubOutForTesting() |
563 | + self.service = utils.import_object('nova.image.service.LocalImageService') |
564 | + |
565 | + def tearDown(self): |
566 | + self.service.delete_all() |
567 | + self.stubs.UnsetAll() |
568 | + |
569 | + |
570 | +class GlanceImageServiceTest(unittest.TestCase, |
571 | + BaseImageServiceTests): |
572 | + |
573 | + """Tests the local image service""" |
574 | + |
575 | + def setUp(self): |
576 | + self.stubs = stubout.StubOutForTesting() |
577 | + fakes.stub_out_glance(self.stubs) |
578 | + self.service = utils.import_object('nova.image.service.GlanceImageService') |
579 | + |
580 | + def tearDown(self): |
581 | + self.service.delete_all() |
582 | + self.stubs.UnsetAll() |
583 | |
584 | === modified file 'nova/tests/api/rackspace/test_servers.py' |
585 | --- nova/tests/api/rackspace/test_servers.py 2010-10-05 19:37:15 +0000 |
586 | +++ nova/tests/api/rackspace/test_servers.py 2010-10-05 20:21:00 +0000 |
587 | @@ -33,6 +33,7 @@ |
588 | |
589 | FLAGS = flags.FLAGS |
590 | |
591 | +FLAGS.verbose = True |
592 | |
593 | def return_server(context, id): |
594 | return stub_instance(id) |
Looks good to me.