Merge lp:~jaypipes/nova/glance-image-service into lp:~hudson-openstack/nova/trunk

Proposed by Jay Pipes
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
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

To post a comment you must log in.
Revision history for this message
Christopher MacGown (0x44) wrote :

Looks good to me.

review: Approve
Revision history for this message
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=image_info) or dict(images=image_list). So, you probably want to

try:
    return json.loads(resp.read())['images']
except KeyError:
     raise ImageServiceException('Received malformed json from Parallax')

(The reason I return the json as dict(image=image_info) rather than just returning the image_info as a dict directly-- as would make sense-- is that, when we do support XML, we'll need a name for the root tag. By doing it this way, JSON and XML can be derived from the exact same dict-representation.)

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.

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

Got it, thanks Rick. Will fix up shortly.

Revision history for this message
Jay Pipes (jaypipes) wrote :

OK, changes made and a new unit test added...

Revision history for this message
Rick Harris (rconradharris) wrote :

Looks good, thanks :)

review: Approve
323. By Jay Pipes

Merge trunk

324. By Jay Pipes

Merge cerberus and trunk

325. By Jay Pipes

Merge overwrote import_object() load of image service.

Revision history for this message
Michael Gundlach (gundlach) wrote :

lgtm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'nova/api/rackspace/images.py'
--- nova/api/rackspace/images.py 2010-09-29 16:27:56 +0000
+++ nova/api/rackspace/images.py 2010-10-05 20:21:00 +0000
@@ -17,12 +17,17 @@
1717
18from webob import exc18from webob import exc
1919
20from nova import flags
21from nova import utils
20from nova import wsgi22from nova import wsgi
21from nova.api.rackspace import _id_translator23from nova.api.rackspace import _id_translator
22import nova.api.rackspace24import nova.api.rackspace
23import nova.image.service25import nova.image.service
24from nova.api.rackspace import faults26from nova.api.rackspace import faults
2527
28
29FLAGS = flags.FLAGS
30
26class Controller(wsgi.Controller):31class Controller(wsgi.Controller):
2732
28 _serialization_metadata = {33 _serialization_metadata = {
@@ -35,7 +40,7 @@
35 }40 }
3641
37 def __init__(self):42 def __init__(self):
38 self._service = nova.image.service.ImageService.load()43 self._service = utils.import_object(FLAGS.image_service)
39 self._id_translator = _id_translator.RackspaceAPIIdTranslator(44 self._id_translator = _id_translator.RackspaceAPIIdTranslator(
40 "image", self._service.__class__.__name__)45 "image", self._service.__class__.__name__)
4146
4247
=== modified file 'nova/api/rackspace/servers.py'
--- nova/api/rackspace/servers.py 2010-10-05 20:07:11 +0000
+++ nova/api/rackspace/servers.py 2010-10-05 20:21:00 +0000
@@ -42,7 +42,7 @@
4242
43def _image_service():43def _image_service():
44 """ Helper method for initializing the image id translator """44 """ Helper method for initializing the image id translator """
45 service = nova.image.service.ImageService.load()45 service = utils.import_object(FLAGS.image_service)
46 return (service, _id_translator.RackspaceAPIIdTranslator(46 return (service, _id_translator.RackspaceAPIIdTranslator(
47 "image", service.__class__.__name__))47 "image", service.__class__.__name__))
4848
4949
=== modified file 'nova/flags.py'
--- nova/flags.py 2010-09-29 00:53:27 +0000
+++ nova/flags.py 2010-10-05 20:21:00 +0000
@@ -222,6 +222,10 @@
222DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager',222DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager',
223 'Manager for scheduler')223 'Manager for scheduler')
224224
225# The service to use for image search and retrieval
226DEFINE_string('image_service', 'nova.image.service.LocalImageService',
227 'The service to use for retrieving and searching for images.')
228
225DEFINE_string('host', socket.gethostname(),229DEFINE_string('host', socket.gethostname(),
226 'name of this node')230 'name of this node')
227231
228232
=== modified file 'nova/image/service.py'
--- nova/image/service.py 2010-09-13 15:53:53 +0000
+++ nova/image/service.py 2010-10-05 20:21:00 +0000
@@ -16,38 +16,215 @@
16# under the License.16# under the License.
1717
18import cPickle as pickle18import cPickle as pickle
19import httplib
20import json
21import logging
19import os.path22import os.path
20import random23import random
21import string24import string
2225import urlparse
23class ImageService(object):26
24 """Provides storage and retrieval of disk image objects."""27import webob.exc
2528
26 @staticmethod29from nova import utils
27 def load():30from nova import flags
28 """Factory method to return image service."""31from nova import exception
29 #TODO(gundlach): read from config.32
30 class_ = LocalImageService33
31 return class_()34FLAGS = flags.FLAGS
35
36
37flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1',
38 'IP address or URL where Glance\'s Teller service resides')
39flags.DEFINE_string('glance_teller_port', '9191',
40 'Port for Glance\'s Teller service')
41flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1',
42 'IP address or URL where Glance\'s Parallax service resides')
43flags.DEFINE_string('glance_parallax_port', '9292',
44 'Port for Glance\'s Parallax service')
45
46
47class BaseImageService(object):
48
49 """Base class for providing image search and retrieval services"""
3250
33 def index(self):51 def index(self):
34 """52 """
35 Return a dict from opaque image id to image data.53 Return a dict from opaque image id to image data.
36 """54 """
55 raise NotImplementedError
3756
38 def show(self, id):57 def show(self, id):
39 """58 """
40 Returns a dict containing image data for the given opaque image id.59 Returns a dict containing image data for the given opaque image id.
41 """60
4261 :raises NotFound if the image does not exist
4362 """
44class GlanceImageService(ImageService):63 raise NotImplementedError
64
65 def create(self, data):
66 """
67 Store the image data and return the new image id.
68
69 :raises AlreadyExists if the image already exist.
70
71 """
72 raise NotImplementedError
73
74 def update(self, image_id, data):
75 """Replace the contents of the given image with the new data.
76
77 :raises NotFound if the image does not exist.
78
79 """
80 raise NotImplementedError
81
82 def delete(self, image_id):
83 """
84 Delete the given image.
85
86 :raises NotFound if the image does not exist.
87
88 """
89 raise NotImplementedError
90
91
92class TellerClient(object):
93
94 def __init__(self):
95 self.address = FLAGS.glance_teller_address
96 self.port = FLAGS.glance_teller_port
97 url = urlparse.urlparse(self.address)
98 self.netloc = url.netloc
99 self.connection_type = {'http': httplib.HTTPConnection,
100 'https': httplib.HTTPSConnection}[url.scheme]
101
102
103class ParallaxClient(object):
104
105 def __init__(self):
106 self.address = FLAGS.glance_parallax_address
107 self.port = FLAGS.glance_parallax_port
108 url = urlparse.urlparse(self.address)
109 self.netloc = url.netloc
110 self.connection_type = {'http': httplib.HTTPConnection,
111 'https': httplib.HTTPSConnection}[url.scheme]
112
113 def get_images(self):
114 """
115 Returns a list of image data mappings from Parallax
116 """
117 try:
118 c = self.connection_type(self.netloc, self.port)
119 c.request("GET", "images")
120 res = c.getresponse()
121 if res.status == 200:
122 # Parallax returns a JSONified dict(images=image_list)
123 data = json.loads(res.read())['images']
124 return data
125 else:
126 logging.warn("Parallax returned HTTP error %d from "
127 "request for /images", res.status_int)
128 return []
129 finally:
130 c.close()
131
132 def get_image_metadata(self, image_id):
133 """
134 Returns a mapping of image metadata from Parallax
135 """
136 try:
137 c = self.connection_type(self.netloc, self.port)
138 c.request("GET", "images/%s" % image_id)
139 res = c.getresponse()
140 if res.status == 200:
141 # Parallax returns a JSONified dict(image=image_info)
142 data = json.loads(res.read())['image']
143 return data
144 else:
145 # TODO(jaypipes): log the error?
146 return None
147 finally:
148 c.close()
149
150 def add_image_metadata(self, image_metadata):
151 """
152 Tells parallax about an image's metadata
153 """
154 pass
155
156 def update_image_metadata(self, image_id, image_metadata):
157 """
158 Updates Parallax's information about an image
159 """
160 pass
161
162 def delete_image_metadata(self, image_id):
163 """
164 Deletes Parallax's information about an image
165 """
166 pass
167
168
169class GlanceImageService(BaseImageService):
170
45 """Provides storage and retrieval of disk image objects within Glance."""171 """Provides storage and retrieval of disk image objects within Glance."""
46 # TODO(gundlach): once Glance has an API, build this.172
47 pass173 def __init__(self):
48174 self.teller = TellerClient()
49175 self.parallax = ParallaxClient()
50class LocalImageService(ImageService):176
177 def index(self):
178 """
179 Calls out to Parallax for a list of images available
180 """
181 images = self.parallax.get_images()
182 return images
183
184 def show(self, id):
185 """
186 Returns a dict containing image data for the given opaque image id.
187 """
188 image = self.parallax.get_image_metadata(id)
189 if image:
190 return image
191 raise exception.NotFound
192
193 def create(self, data):
194 """
195 Store the image data and return the new image id.
196
197 :raises AlreadyExists if the image already exist.
198
199 """
200 return self.parallax.add_image_metadata(data)
201
202 def update(self, image_id, data):
203 """Replace the contents of the given image with the new data.
204
205 :raises NotFound if the image does not exist.
206
207 """
208 self.parallax.update_image_metadata(image_id, data)
209
210 def delete(self, image_id):
211 """
212 Delete the given image.
213
214 :raises NotFound if the image does not exist.
215
216 """
217 self.parallax.delete_image_metadata(image_id)
218
219 def delete_all(self):
220 """
221 Clears out all images
222 """
223 pass
224
225
226class LocalImageService(BaseImageService):
227
51 """Image service storing images to local disk."""228 """Image service storing images to local disk."""
52229
53 def __init__(self):230 def __init__(self):
@@ -68,7 +245,10 @@
68 return [ self.show(id) for id in self._ids() ]245 return [ self.show(id) for id in self._ids() ]
69246
70 def show(self, id):247 def show(self, id):
71 return pickle.load(open(self._path_to(id))) 248 try:
249 return pickle.load(open(self._path_to(id)))
250 except IOError:
251 raise exception.NotFound
72252
73 def create(self, data):253 def create(self, data):
74 """254 """
@@ -81,10 +261,23 @@
81261
82 def update(self, image_id, data):262 def update(self, image_id, data):
83 """Replace the contents of the given image with the new data."""263 """Replace the contents of the given image with the new data."""
84 pickle.dump(data, open(self._path_to(image_id), 'w'))264 try:
265 pickle.dump(data, open(self._path_to(image_id), 'w'))
266 except IOError:
267 raise exception.NotFound
85268
86 def delete(self, image_id):269 def delete(self, image_id):
87 """270 """
88 Delete the given image. Raises OSError if the image does not exist.271 Delete the given image. Raises OSError if the image does not exist.
89 """272 """
90 os.unlink(self._path_to(image_id))273 try:
274 os.unlink(self._path_to(image_id))
275 except IOError:
276 raise exception.NotFound
277
278 def delete_all(self):
279 """
280 Clears out all images in local directory
281 """
282 for f in os.listdir(self._path):
283 os.unlink(self._path_to(f))
91284
=== modified file 'nova/tests/api/rackspace/fakes.py'
--- nova/tests/api/rackspace/fakes.py 2010-10-01 18:02:51 +0000
+++ nova/tests/api/rackspace/fakes.py 2010-10-05 20:21:00 +0000
@@ -1,5 +1,24 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
1import datetime18import datetime
2import json19import json
20import random
21import string
322
4import webob23import webob
5import webob.dec24import webob.dec
@@ -7,6 +26,7 @@
7from nova import auth26from nova import auth
8from nova import utils27from nova import utils
9from nova import flags28from nova import flags
29from nova import exception as exc
10import nova.api.rackspace.auth30import nova.api.rackspace.auth
11import nova.api.rackspace._id_translator31import nova.api.rackspace._id_translator
12from nova.image import service32from nova.image import service
@@ -105,6 +125,60 @@
105 FLAGS.FAKE_subdomain = 'rs'125 FLAGS.FAKE_subdomain = 'rs'
106126
107127
128def stub_out_glance(stubs):
129
130 class FakeParallaxClient:
131
132 def __init__(self):
133 self.fixtures = {}
134
135 def fake_get_images(self):
136 return self.fixtures
137
138 def fake_get_image_metadata(self, image_id):
139 for k, f in self.fixtures.iteritems():
140 if k == image_id:
141 return f
142 return None
143
144 def fake_add_image_metadata(self, image_data):
145 id = ''.join(random.choice(string.letters) for _ in range(20))
146 image_data['id'] = id
147 self.fixtures[id] = image_data
148 return id
149
150 def fake_update_image_metadata(self, image_id, image_data):
151
152 if image_id not in self.fixtures.keys():
153 raise exc.NotFound
154
155 self.fixtures[image_id].update(image_data)
156
157 def fake_delete_image_metadata(self, image_id):
158
159 if image_id not in self.fixtures.keys():
160 raise exc.NotFound
161
162 del self.fixtures[image_id]
163
164 def fake_delete_all(self):
165 self.fixtures = {}
166
167 fake_parallax_client = FakeParallaxClient()
168 stubs.Set(nova.image.service.ParallaxClient, 'get_images',
169 fake_parallax_client.fake_get_images)
170 stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata',
171 fake_parallax_client.fake_get_image_metadata)
172 stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata',
173 fake_parallax_client.fake_add_image_metadata)
174 stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata',
175 fake_parallax_client.fake_update_image_metadata)
176 stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata',
177 fake_parallax_client.fake_delete_image_metadata)
178 stubs.Set(nova.image.service.GlanceImageService, 'delete_all',
179 fake_parallax_client.fake_delete_all)
180
181
108class FakeAuthDatabase(object):182class FakeAuthDatabase(object):
109 data = {}183 data = {}
110184
111185
=== modified file 'nova/tests/api/rackspace/test_images.py'
--- nova/tests/api/rackspace/test_images.py 2010-10-01 18:02:51 +0000
+++ nova/tests/api/rackspace/test_images.py 2010-10-05 20:21:00 +0000
@@ -15,25 +15,127 @@
15# License for the specific language governing permissions and limitations15# License for the specific language governing permissions and limitations
16# under the License.16# under the License.
1717
18import logging
18import unittest19import unittest
1920
20import stubout21import stubout
2122
23from nova import exception
24from nova import utils
22from nova.api.rackspace import images25from nova.api.rackspace import images
2326from nova.tests.api.rackspace import fakes
2427
25class ImagesTest(unittest.TestCase):28
26 def setUp(self):29class BaseImageServiceTests():
27 self.stubs = stubout.StubOutForTesting()30
2831 """Tasks to test for all image services"""
29 def tearDown(self):32
30 self.stubs.UnsetAll()33 def test_create(self):
3134
32 def test_get_image_list(self):35 fixture = {'name': 'test image',
33 pass36 'updated': None,
3437 'created': None,
35 def test_delete_image(self):38 'status': None,
36 pass39 'serverId': None,
37 40 'progress': None}
38 def test_create_image(self):41
39 pass42 num_images = len(self.service.index())
43
44 id = self.service.create(fixture)
45
46 self.assertNotEquals(None, id)
47 self.assertEquals(num_images + 1, len(self.service.index()))
48
49 def test_create_and_show_non_existing_image(self):
50
51 fixture = {'name': 'test image',
52 'updated': None,
53 'created': None,
54 'status': None,
55 'serverId': None,
56 'progress': None}
57
58 num_images = len(self.service.index())
59
60 id = self.service.create(fixture)
61
62 self.assertNotEquals(None, id)
63
64 self.assertRaises(exception.NotFound,
65 self.service.show,
66 'bad image id')
67
68 def test_update(self):
69
70 fixture = {'name': 'test image',
71 'updated': None,
72 'created': None,
73 'status': None,
74 'serverId': None,
75 'progress': None}
76
77 id = self.service.create(fixture)
78
79 fixture['status'] = 'in progress'
80
81 self.service.update(id, fixture)
82 new_image_data = self.service.show(id)
83 self.assertEquals('in progress', new_image_data['status'])
84
85 def test_delete(self):
86
87 fixtures = [
88 {'name': 'test image 1',
89 'updated': None,
90 'created': None,
91 'status': None,
92 'serverId': None,
93 'progress': None},
94 {'name': 'test image 2',
95 'updated': None,
96 'created': None,
97 'status': None,
98 'serverId': None,
99 'progress': None}]
100
101 ids = []
102 for fixture in fixtures:
103 new_id = self.service.create(fixture)
104 ids.append(new_id)
105
106 num_images = len(self.service.index())
107 self.assertEquals(2, num_images)
108
109 self.service.delete(ids[0])
110
111 num_images = len(self.service.index())
112 self.assertEquals(1, num_images)
113
114
115class LocalImageServiceTest(unittest.TestCase,
116 BaseImageServiceTests):
117
118 """Tests the local image service"""
119
120 def setUp(self):
121 self.stubs = stubout.StubOutForTesting()
122 self.service = utils.import_object('nova.image.service.LocalImageService')
123
124 def tearDown(self):
125 self.service.delete_all()
126 self.stubs.UnsetAll()
127
128
129class GlanceImageServiceTest(unittest.TestCase,
130 BaseImageServiceTests):
131
132 """Tests the local image service"""
133
134 def setUp(self):
135 self.stubs = stubout.StubOutForTesting()
136 fakes.stub_out_glance(self.stubs)
137 self.service = utils.import_object('nova.image.service.GlanceImageService')
138
139 def tearDown(self):
140 self.service.delete_all()
141 self.stubs.UnsetAll()
40142
=== modified file 'nova/tests/api/rackspace/test_servers.py'
--- nova/tests/api/rackspace/test_servers.py 2010-10-05 19:37:15 +0000
+++ nova/tests/api/rackspace/test_servers.py 2010-10-05 20:21:00 +0000
@@ -33,6 +33,7 @@
3333
34FLAGS = flags.FLAGS34FLAGS = flags.FLAGS
3535
36FLAGS.verbose = True
3637
37def return_server(context, id):38def return_server(context, id):
38 return stub_instance(id)39 return stub_instance(id)