Merge lp:~jaypipes/glance/teller-api into lp:~hudson-openstack/glance/trunk

Proposed by Jay Pipes
Status: Merged
Approved by: Jay Pipes
Approved revision: 21
Merged at revision: 20
Proposed branch: lp:~jaypipes/glance/teller-api
Merge into: lp:~hudson-openstack/glance/trunk
Diff against target: 779 lines (+322/-159)
7 files modified
glance/client.py (+30/-11)
glance/parallax/controllers.py (+28/-27)
glance/teller/controllers.py (+18/-20)
glance/teller/registries.py (+19/-33)
tests/stubs.py (+115/-25)
tests/unit/test_clients.py (+76/-11)
tests/unit/test_teller_api.py (+36/-32)
To merge this branch: bzr merge lp:~jaypipes/glance/teller-api
Reviewer Review Type Date Requested Status
Michael Gundlach (community) Approve
Rick Harris (community) Approve
Christopher MacGown Pending
Review via email: mp+42392@code.launchpad.net

Description of the change

    * Changes Teller API to use REST with opaque ID sent in
      API calls instead of a "parallax URI". This hides the
      URI stuff behind the API layer in communication between
      Parallax and Teller.
    * Adds unit tests for the only complete Teller API call so
      far: GET images/<ID>, which returns a gzip'd string of
      image data

      I want to get feedback on these new unit tests and the
      changes to the Teller API to remove the parallax URI from
      the API calls.

To post a comment you must log in.
Revision history for this message
Rick Harris (rconradharris) wrote :

lgtm

I'm fine with the idea of using sequential id's for now and one registry per installation. However, down the line (prob beyond cactus), we're going to want to discuss how to federate the registries, whether it's using the registry URI as the ID (which has some neat interoperability implications), or using a UUID with some peer-lookup algorithm.

Tests look good and still run fast.

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

Marking as approved.

review: Approve
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 'glance/client.py'
--- glance/client.py 2010-11-23 18:12:11 +0000
+++ glance/client.py 2010-12-01 17:19:12 +0000
@@ -73,6 +73,20 @@
73 self.protocol = url.scheme73 self.protocol = url.scheme
74 self.connection = None74 self.connection = None
7575
76 def get_connection_type(self):
77 """
78 Returns the proper connection type
79 """
80 try:
81 connection_type = {'http': httplib.HTTPConnection,
82 'https': httplib.HTTPSConnection}\
83 [self.protocol]
84 return connection_type
85 except KeyError:
86 raise UnsupportedProtocolError("Unsupported protocol %s. Unable "
87 " to connect to server."
88 % self.protocol)
89
76 def do_request(self, method, action, body=None):90 def do_request(self, method, action, body=None):
77 """91 """
78 Connects to the server and issues a request. Handles converting92 Connects to the server and issues a request. Handles converting
@@ -86,14 +100,7 @@
86 :param data: string of data to send, or None (default)100 :param data: string of data to send, or None (default)
87 """101 """
88 try:102 try:
89 connection_type = {'http': httplib.HTTPConnection,103 connection_type = self.get_connection_type()
90 'https': httplib.HTTPSConnection}\
91 [self.protocol]
92 except KeyError:
93 raise UnsupportedProtocolError("Unsupported protocol %s. Unable "
94 " to connect to server."
95 % self.protocol)
96 try:
97 c = connection_type(self.netloc, self.port)104 c = connection_type(self.netloc, self.port)
98 c.request(method, action, body)105 c.request(method, action, body)
99 res = c.getresponse()106 res = c.getresponse()
@@ -116,8 +123,6 @@
116 except (socket.error, IOError), e:123 except (socket.error, IOError), e:
117 raise ClientConnectionError("Unable to connect to "124 raise ClientConnectionError("Unable to connect to "
118 "server. Got error: %s" % e)125 "server. Got error: %s" % e)
119 finally:
120 c.close()
121126
122 def get_status_code(self, response):127 def get_status_code(self, response):
123 """128 """
@@ -132,7 +137,7 @@
132137
133class TellerClient(BaseClient):138class TellerClient(BaseClient):
134139
135 """A client for the Teller image caching service"""140 """A client for the Teller image caching and delivery service"""
136141
137 DEFAULT_ADDRESS = 'http://127.0.0.1'142 DEFAULT_ADDRESS = 'http://127.0.0.1'
138 DEFAULT_PORT = 9191143 DEFAULT_PORT = 9191
@@ -148,6 +153,20 @@
148 """153 """
149 super(TellerClient, self).__init__(**kwargs)154 super(TellerClient, self).__init__(**kwargs)
150155
156 def get_image(self, image_id):
157 """
158 Returns the raw disk image as a mime-encoded blob stream for the
159 supplied opaque image identifier.
160
161 :param image_id: The opaque image identifier
162
163 :raises exception.NotFound if image is not found
164 """
165 # TODO(jaypipes): Handle other registries than Parallax...
166
167 res = self.do_request("GET", "images/%s" % image_id)
168 return res.read()
169
151170
152class ParallaxClient(BaseClient):171class ParallaxClient(BaseClient):
153172
154173
=== modified file 'glance/parallax/controllers.py'
--- glance/parallax/controllers.py 2010-11-23 18:12:11 +0000
+++ glance/parallax/controllers.py 2010-12-01 17:19:12 +0000
@@ -61,7 +61,7 @@
61 61
62 """62 """
63 images = db.image_get_all_public(None)63 images = db.image_get_all_public(None)
64 image_dicts = [self._make_image_dict(i) for i in images]64 image_dicts = [make_image_dict(i) for i in images]
65 return dict(images=image_dicts)65 return dict(images=image_dicts)
6666
67 def show(self, req, id):67 def show(self, req, id):
@@ -71,7 +71,7 @@
71 except exception.NotFound:71 except exception.NotFound:
72 raise exc.HTTPNotFound()72 raise exc.HTTPNotFound()
73 73
74 return dict(image=self._make_image_dict(image))74 return dict(image=make_image_dict(image))
7575
76 def delete(self, req, id):76 def delete(self, req, id):
77 """Deletes an existing image with the registry.77 """Deletes an existing image with the registry.
@@ -133,31 +133,6 @@
133 except exception.NotFound:133 except exception.NotFound:
134 return exc.HTTPNotFound()134 return exc.HTTPNotFound()
135135
136 @staticmethod
137 def _make_image_dict(image):
138 """Create a dict representation of an image which we can use to
139 serialize the image.
140
141 """
142
143 def _fetch_attrs(d, attrs):
144 return dict([(a, d[a]) for a in attrs])
145
146 files = [_fetch_attrs(f, db.IMAGE_FILE_ATTRS) for f in image['files']]
147
148 # TODO(sirp): should this be a dict, or a list of dicts?
149 # A plain dict is more convenient, but list of dicts would provide
150 # access to created_at, etc
151 properties = dict((p['key'], p['value'])
152 for p in image['properties']
153 if not p['deleted'])
154
155 image_dict = _fetch_attrs(image, db.IMAGE_ATTRS)
156
157 image_dict['files'] = files
158 image_dict['properties'] = properties
159 return image_dict
160
161136
162class API(wsgi.Router):137class API(wsgi.Router):
163 """WSGI entry point for all Parallax requests."""138 """WSGI entry point for all Parallax requests."""
@@ -169,3 +144,29 @@
169 collection={'detail': 'GET'})144 collection={'detail': 'GET'})
170 mapper.connect("/", controller=ImageController(), action="index")145 mapper.connect("/", controller=ImageController(), action="index")
171 super(API, self).__init__(mapper)146 super(API, self).__init__(mapper)
147
148
149def make_image_dict(image):
150 """
151 Create a dict representation of an image which we can use to
152 serialize the image.
153 """
154
155 def _fetch_attrs(d, attrs):
156 return dict([(a, d[a]) for a in attrs
157 if a in d.keys()])
158
159 files = [_fetch_attrs(f, db.IMAGE_FILE_ATTRS) for f in image['files']]
160
161 # TODO(sirp): should this be a dict, or a list of dicts?
162 # A plain dict is more convenient, but list of dicts would provide
163 # access to created_at, etc
164 properties = dict((p['key'], p['value'])
165 for p in image['properties']
166 if 'deleted' in p.keys() and not p['deleted'])
167
168 image_dict = _fetch_attrs(image, db.IMAGE_ATTRS)
169
170 image_dict['files'] = files
171 image_dict['properties'] = properties
172 return image_dict
172173
=== modified file 'glance/teller/controllers.py'
--- glance/teller/controllers.py 2010-10-12 18:01:12 +0000
+++ glance/teller/controllers.py 2010-12-01 17:19:12 +0000
@@ -31,9 +31,9 @@
3131
3232
33class ImageController(wsgi.Controller):33class ImageController(wsgi.Controller):
34 """Image Controller """34 """Image Controller"""
3535
36 def index(self, req):36 def show(self, req, id):
37 """37 """
38 Query the parallax service for the image registry for the passed in 38 Query the parallax service for the image registry for the passed in
39 req['uri']. If it exists, we connect to the appropriate backend as39 req['uri']. If it exists, we connect to the appropriate backend as
@@ -43,44 +43,40 @@
43 Optionally, we can pass in 'registry' which will use a given43 Optionally, we can pass in 'registry' which will use a given
44 RegistryAdapter for the request. This is useful for testing.44 RegistryAdapter for the request. This is useful for testing.
45 """45 """
46 try:
47 uri = req.str_GET['uri']
48 except KeyError:
49 return exc.HTTPBadRequest(body="Missing uri", request=req,
50 content_type="text/plain")
5146
52 registry = req.str_GET.get('registry', 'parallax')47 registry = req.str_GET.get('registry', 'parallax')
5348
54 try:49 try:
55 image = registries.lookup_by_registry(registry, uri)50 image = registries.lookup_by_registry(registry, id)
56 logging.debug("Found image registry for URI: %s. Got: %s", uri, image)
57 except registries.UnknownImageRegistry:51 except registries.UnknownImageRegistry:
52 logging.debug("Could not find image registry: %s.", registry)
58 return exc.HTTPBadRequest(body="Unknown registry '%s'" % registry,53 return exc.HTTPBadRequest(body="Unknown registry '%s'" % registry,
59 request=req,54 request=req,
60 content_type="text/plain")55 content_type="text/plain")
6156 except exception.NotFound:
62 if not image:
63 raise exc.HTTPNotFound(body='Image not found', request=req,57 raise exc.HTTPNotFound(body='Image not found', request=req,
64 content_type='text/plain')58 content_type='text/plain')
6559
66 def image_iterator():60 def image_iterator():
67 for file in image['files']:61 for file in image['files']:
68 chunks = backends.get_from_backend(62 chunks = backends.get_from_backend(file['location'],
69 file['location'], expected_size=file['size'])63 expected_size=file['size'])
7064
71 for chunk in chunks:65 for chunk in chunks:
72 yield chunk66 yield chunk
7367
74 return req.get_response(Response(app_iter=image_iterator()))68 res = Response(app_iter=image_iterator(),
69 content_type="text/plain")
70 return req.get_response(res)
75 71
72 def index(self, req):
73 """Index is not currently supported """
74 raise exc.HTTPNotImplemented()
75
76 def detail(self, req):76 def detail(self, req):
77 """Detail is not currently supported """77 """Detail is not currently supported """
78 raise exc.HTTPNotImplemented()78 raise exc.HTTPNotImplemented()
7979
80 def show(self, req):
81 """Show is not currently supported """
82 raise exc.HTTPNotImplemented()
83
84 def delete(self, req, id):80 def delete(self, req, id):
85 """Delete is not currently supported """81 """Delete is not currently supported """
86 raise exc.HTTPNotImplemented()82 raise exc.HTTPNotImplemented()
@@ -95,10 +91,12 @@
9591
9692
97class API(wsgi.Router):93class API(wsgi.Router):
94
98 """WSGI entry point for all Teller requests."""95 """WSGI entry point for all Teller requests."""
9996
100 def __init__(self):97 def __init__(self):
101 mapper = routes.Mapper()98 mapper = routes.Mapper()
102 mapper.resource("image", "image", controller=ImageController(),99 mapper.resource("image", "images", controller=ImageController(),
103 collection={'detail': 'GET'})100 collection={'detail': 'GET'})
101 mapper.connect("/", controller=ImageController(), action="index")
104 super(API, self).__init__(mapper)102 super(API, self).__init__(mapper)
105103
=== modified file 'glance/teller/registries.py'
--- glance/teller/registries.py 2010-10-11 18:28:35 +0000
+++ glance/teller/registries.py 2010-12-01 17:19:12 +0000
@@ -19,6 +19,8 @@
19import json19import json
20import urlparse20import urlparse
2121
22from glance import client
23
2224
23class ImageRegistryException(Exception):25class ImageRegistryException(Exception):
24 """ Base class for all RegistryAdapter exceptions """26 """ Base class for all RegistryAdapter exceptions """
@@ -47,38 +49,17 @@
47 """49 """
4850
49 @classmethod51 @classmethod
50 def lookup(cls, parsed_uri):52 def lookup(cls, image_id):
51 """53 """
52 Takes a parsed_uri, checks if that image is registered in Parallax,54 Takes an image ID and checks if that image is registered in Parallax,
53 and if so, returns the image metadata. If the image does not exist,55 and if so, returns the image metadata. If the image does not exist,
54 we return None.56 we raise NotFound
55 """57 """
56 scheme = parsed_uri.scheme58 # TODO(jaypipes): Make parallax client configurable via options.
57 if scheme == 'http':59 # Unfortunately, the decision to make all adapters have no state
58 conn_class = httplib.HTTPConnection60 # hinders this...
59 elif scheme == 'https':61 c = client.ParallaxClient()
60 conn_class = httplib.HTTPSConnection62 return c.get_image(image_id)
61 else:
62 raise ImageRegistryException(
63 "Unrecognized scheme '%s'" % scheme)
64
65 conn = conn_class(parsed_uri.netloc)
66 try:
67 conn.request('GET', parsed_uri.path, "", {})
68 response = conn.getresponse()
69
70 # The image exists
71 if response.status == 200:
72 result = response.read()
73 image_json = json.loads(result)
74 try:
75 return image_json["image"]
76 except KeyError:
77 raise ImageRegistryException("Missing 'image' key")
78 except Exception: # gaierror
79 return None
80 finally:
81 conn.close()
8263
8364
84REGISTRY_ADAPTERS = {65REGISTRY_ADAPTERS = {
@@ -86,12 +67,17 @@
86}67}
8768
8869
89def lookup_by_registry(registry, image_uri):70def lookup_by_registry(registry, image_id):
90 """ Convenience function to lookup based on a registry protocol """71 """
72 Convenience function to lookup image metadata for the given
73 opaque image identifier and registry.
74
75 :param registry: String name of registry to use for lookups
76 :param image_id: Opaque image identifier
77 """
91 try:78 try:
92 adapter = REGISTRY_ADAPTERS[registry]79 adapter = REGISTRY_ADAPTERS[registry]
93 except KeyError:80 except KeyError:
94 raise UnknownImageRegistry("'%s' not found" % registry)81 raise UnknownImageRegistry("'%s' not found" % registry)
95 82
96 parsed_uri = urlparse.urlparse(image_uri)83 return adapter.lookup(image_id)
97 return adapter.lookup(parsed_uri)
9884
=== modified file 'tests/stubs.py'
--- tests/stubs.py 2010-11-23 18:12:11 +0000
+++ tests/stubs.py 2010-12-01 17:19:12 +0000
@@ -19,18 +19,26 @@
1919
20import datetime20import datetime
21import httplib21import httplib
22import os
23import shutil
22import StringIO24import StringIO
23import sys25import sys
26import gzip
2427
25import stubout28import stubout
26import webob29import webob
2730
28from glance.common import exception31from glance.common import exception
29from glance.parallax import controllers as parallax_controllers32from glance.parallax import controllers as parallax_controllers
33from glance.teller import controllers as teller_controllers
34import glance.teller.backends
30import glance.teller.backends.swift35import glance.teller.backends.swift
31import glance.parallax.db.sqlalchemy.api36import glance.parallax.db.sqlalchemy.api
3237
3338
39FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests')
40
41
34def stub_out_http_backend(stubs):42def stub_out_http_backend(stubs):
35 """Stubs out the httplib.HTTPRequest.getresponse to return43 """Stubs out the httplib.HTTPRequest.getresponse to return
36 faked-out data instead of grabbing actual contents of a resource44 faked-out data instead of grabbing actual contents of a resource
@@ -63,24 +71,61 @@
63 fake_http_conn.getresponse)71 fake_http_conn.getresponse)
6472
6573
74def clean_out_fake_filesystem_backend():
75 """
76 Removes any leftover directories used in fake filesystem
77 backend
78 """
79 if os.path.exists(FAKE_FILESYSTEM_ROOTDIR):
80 shutil.rmtree(FAKE_FILESYSTEM_ROOTDIR, ignore_errors=True)
81
82
66def stub_out_filesystem_backend(stubs):83def stub_out_filesystem_backend(stubs):
67 """Stubs out the Filesystem Teller service to return fake84 """
85 Stubs out the Filesystem Teller service to return fake
68 data from files.86 data from files.
6987
70 The stubbed service always yields the following fixture::88 We establish a few fake images in a directory under /tmp/glance-tests
7189 and ensure that this directory contains the following files:
72 //chunk090
73 //chunk191 /acct/2.gz.0 <-- zipped tarfile containing "chunk0"
92 /acct/2.gz.1 <-- zipped tarfile containing "chunk42"
93
94 The stubbed service yields the data in the above files.
7495
75 :param stubs: Set of stubout stubs96 :param stubs: Set of stubout stubs
7697
77 """98 """
99
78 class FakeFilesystemBackend(object):100 class FakeFilesystemBackend(object):
79101
102 CHUNKSIZE = 100
103
80 @classmethod104 @classmethod
81 def get(cls, parsed_uri, expected_size, conn_class=None):105 def get(cls, parsed_uri, expected_size, opener=None):
82106 filepath = os.path.join('/',
83 return StringIO.StringIO(parsed_uri.path)107 parsed_uri.netloc,
108 parsed_uri.path.strip(os.path.sep))
109 f = gzip.open(filepath, 'rb')
110 data = f.read()
111 f.close()
112 return data
113
114 # Establish a clean faked filesystem with dummy images
115 if os.path.exists(FAKE_FILESYSTEM_ROOTDIR):
116 shutil.rmtree(FAKE_FILESYSTEM_ROOTDIR, ignore_errors=True)
117 os.mkdir(FAKE_FILESYSTEM_ROOTDIR)
118 os.mkdir(os.path.join(FAKE_FILESYSTEM_ROOTDIR, 'acct'))
119
120 f = gzip.open(os.path.join(FAKE_FILESYSTEM_ROOTDIR, 'acct', '2.gz.0'),
121 "wb")
122 f.write("chunk0")
123 f.close()
124
125 f = gzip.open(os.path.join(FAKE_FILESYSTEM_ROOTDIR, 'acct', '2.gz.1'),
126 "wb")
127 f.write("chunk42")
128 f.close()
84129
85 fake_filesystem_backend = FakeFilesystemBackend()130 fake_filesystem_backend = FakeFilesystemBackend()
86 stubs.Set(glance.teller.backends.FilesystemBackend, 'get',131 stubs.Set(glance.teller.backends.FilesystemBackend, 'get',
@@ -156,25 +201,16 @@
156 fake_parallax_registry.lookup)201 fake_parallax_registry.lookup)
157202
158203
159def stub_out_parallax_server(stubs):204def stub_out_parallax_and_teller_server(stubs):
160 """205 """
161 Mocks httplib calls to 127.0.0.1:9292 for testing so206 Mocks calls to 127.0.0.1 on 9191 and 9292 for testing so
162 that a real Parallax server does not need to be up and207 that a real Teller server does not need to be up and
163 running208 running
164 """209 """
165210
166 def fake_http_connection_constructor(address, port):
167 """
168 Returns either a faked connection or a real
169 one depending on if the connection is to a parallax
170 server or not...
171 """
172 return FakeParallaxConnection()
173
174
175 class FakeParallaxConnection(object):211 class FakeParallaxConnection(object):
176212
177 def __init__(self):213 def __init__(self, *args, **kwargs):
178 pass214 pass
179215
180 def connect(self):216 def connect(self):
@@ -199,8 +235,54 @@
199 setattr(res, 'read', fake_reader)235 setattr(res, 'read', fake_reader)
200 return res236 return res
201237
202 stubs.Set(httplib, 'HTTPConnection',238 class FakeTellerConnection(object):
203 fake_http_connection_constructor)239
240 def __init__(self, *args, **kwargs):
241 pass
242
243 def connect(self):
244 return True
245
246 def close(self):
247 return True
248
249 def request(self, method, url, body=None):
250 self.req = webob.Request.blank("/" + url.lstrip("/"))
251 self.req.method = method
252 if body:
253 self.req.body = body
254
255 def getresponse(self):
256 res = self.req.get_response(teller_controllers.API())
257
258 # httplib.Response has a read() method...fake it out
259 def fake_reader():
260 return res.body
261
262 setattr(res, 'read', fake_reader)
263 return res
264
265 def fake_get_connection_type(client):
266 """
267 Returns the proper connection type
268 """
269 if client.port == 9191 and client.netloc == '127.0.0.1':
270 return FakeTellerConnection
271 if client.port == 9292 and client.netloc == '127.0.0.1':
272 return FakeParallaxConnection
273 else:
274 try:
275 connection_type = {'http': httplib.HTTPConnection,
276 'https': httplib.HTTPSConnection}\
277 [client.protocol]
278 return connection_type
279 except KeyError:
280 raise UnsupportedProtocolError("Unsupported protocol %s. Unable "
281 " to connect to server."
282 % self.protocol)
283
284 stubs.Set(glance.client.BaseClient, 'get_connection_type',
285 fake_get_connection_type)
204286
205287
206def stub_out_parallax_db_image_api(stubs):288def stub_out_parallax_db_image_api(stubs):
@@ -225,7 +307,11 @@
225 'updated_at': datetime.datetime.utcnow(),307 'updated_at': datetime.datetime.utcnow(),
226 'deleted_at': None,308 'deleted_at': None,
227 'deleted': False,309 'deleted': False,
228 'files': [],310 'files': [
311 {"location": "swift://user:passwd@acct/container/obj.tar.gz.0",
312 "size": 6},
313 {"location": "swift://user:passwd@acct/container/obj.tar.gz.1",
314 "size": 7}],
229 'properties': []},315 'properties': []},
230 {'id': 2,316 {'id': 2,
231 'name': 'fake image #2',317 'name': 'fake image #2',
@@ -236,7 +322,11 @@
236 'updated_at': datetime.datetime.utcnow(),322 'updated_at': datetime.datetime.utcnow(),
237 'deleted_at': None,323 'deleted_at': None,
238 'deleted': False,324 'deleted': False,
239 'files': [],325 'files': [
326 {"location": "file://tmp/glance-tests/acct/2.gz.0",
327 "size": 6},
328 {"location": "file://tmp/glance-tests/acct/2.gz.1",
329 "size": 7}],
240 'properties': []}]330 'properties': []}]
241331
242 VALID_STATUSES = ('available', 'disabled', 'pending')332 VALID_STATUSES = ('available', 'disabled', 'pending')
243333
=== modified file 'tests/unit/test_clients.py'
--- tests/unit/test_clients.py 2010-11-23 18:12:11 +0000
+++ tests/unit/test_clients.py 2010-12-01 17:19:12 +0000
@@ -47,13 +47,16 @@
4747
48class TestParallaxClient(unittest.TestCase):48class TestParallaxClient(unittest.TestCase):
4949
50 """Test proper actions made for both valid and invalid requests"""50 """
51 Test proper actions made for both valid and invalid requests
52 against a Parallax service
53 """
5154
52 def setUp(self):55 def setUp(self):
53 """Establish a clean test environment"""56 """Establish a clean test environment"""
54 self.stubs = stubout.StubOutForTesting()57 self.stubs = stubout.StubOutForTesting()
55 stubs.stub_out_parallax_db_image_api(self.stubs)58 stubs.stub_out_parallax_db_image_api(self.stubs)
56 stubs.stub_out_parallax_server(self.stubs)59 stubs.stub_out_parallax_and_teller_server(self.stubs)
57 self.client = client.ParallaxClient()60 self.client = client.ParallaxClient()
5861
59 def tearDown(self):62 def tearDown(self):
@@ -76,27 +79,61 @@
76 'name': 'fake image #2',79 'name': 'fake image #2',
77 'is_public': True,80 'is_public': True,
78 'image_type': 'kernel',81 'image_type': 'kernel',
79 'status': 'available'82 'status': 'available',
80 }83 'files': [
84 {"location": "file://tmp/glance-tests/acct/2.gz.0",
85 "size": 6},
86 {"location": "file://tmp/glance-tests/acct/2.gz.1",
87 "size": 7}],
88 'properties': []}
89
90 expected = {'id': 2,
91 'name': 'fake image #2',
92 'is_public': True,
93 'image_type': 'kernel',
94 'status': 'available',
95 'files': [
96 {"location": "file://tmp/glance-tests/acct/2.gz.0",
97 "size": 6},
98 {"location": "file://tmp/glance-tests/acct/2.gz.1",
99 "size": 7}],
100 'properties': {}}
81101
82 images = self.client.get_images_detailed()102 images = self.client.get_images_detailed()
83 self.assertEquals(len(images), 1)103 self.assertEquals(len(images), 1)
84104
85 for k,v in fixture.iteritems():105 for k,v in expected.iteritems():
86 self.assertEquals(v, images[0][k])106 self.assertEquals(v, images[0][k])
87107
88 def test_get_image_metadata(self):108 def test_get_image(self):
89 """Tests that the detailed info about an image returned"""109 """Tests that the detailed info about an image returned"""
90 fixture = {'id': 2,110 fixture = {'id': 2,
91 'name': 'fake image #2',111 'name': 'fake image #2',
92 'is_public': True,112 'is_public': True,
93 'image_type': 'kernel',113 'image_type': 'kernel',
94 'status': 'available'114 'status': 'available',
95 }115 'files': [
116 {"location": "file://tmp/glance-tests/acct/2.gz.0",
117 "size": 6},
118 {"location": "file://tmp/glance-tests/acct/2.gz.1",
119 "size": 7}],
120 'properties': []}
121
122 expected = {'id': 2,
123 'name': 'fake image #2',
124 'is_public': True,
125 'image_type': 'kernel',
126 'status': 'available',
127 'files': [
128 {"location": "file://tmp/glance-tests/acct/2.gz.0",
129 "size": 6},
130 {"location": "file://tmp/glance-tests/acct/2.gz.1",
131 "size": 7}],
132 'properties': {}}
96133
97 data = self.client.get_image(2)134 data = self.client.get_image(2)
98135
99 for k,v in fixture.iteritems():136 for k,v in expected.iteritems():
100 self.assertEquals(v, data[k])137 self.assertEquals(v, data[k])
101138
102 def test_get_image_non_existing(self):139 def test_get_image_non_existing(self):
@@ -106,7 +143,7 @@
106 self.client.get_image,143 self.client.get_image,
107 42)144 42)
108145
109 def test_add_image_metadata_basic(self):146 def test_add_image_basic(self):
110 """Tests that we can add image metadata and returns the new id"""147 """Tests that we can add image metadata and returns the new id"""
111 fixture = {'name': 'fake public image',148 fixture = {'name': 'fake public image',
112 'is_public': True,149 'is_public': True,
@@ -128,7 +165,7 @@
128 self.assertTrue('status' in data.keys())165 self.assertTrue('status' in data.keys())
129 self.assertEquals('available', data['status'])166 self.assertEquals('available', data['status'])
130167
131 def test_add_image_metadata_with_properties(self):168 def test_add_image_with_properties(self):
132 """Tests that we can add image metadata with properties"""169 """Tests that we can add image metadata with properties"""
133 fixture = {'name': 'fake public image',170 fixture = {'name': 'fake public image',
134 'is_public': True,171 'is_public': True,
@@ -231,3 +268,31 @@
231 self.assertRaises(exception.NotFound,268 self.assertRaises(exception.NotFound,
232 self.client.delete_image,269 self.client.delete_image,
233 3)270 3)
271
272
273class TestTellerClient(unittest.TestCase):
274
275 """
276 Test proper actions made for both valid and invalid requests
277 against a Teller service
278 """
279
280 def setUp(self):
281 """Establish a clean test environment"""
282 self.stubs = stubout.StubOutForTesting()
283 stubs.stub_out_parallax_db_image_api(self.stubs)
284 stubs.stub_out_parallax_and_teller_server(self.stubs)
285 stubs.stub_out_filesystem_backend(self.stubs)
286 self.client = client.TellerClient()
287
288 def tearDown(self):
289 """Clear the test environment"""
290 stubs.clean_out_fake_filesystem_backend()
291 self.stubs.UnsetAll()
292
293 def test_get_image(self):
294 """Test a simple file backend retrieval works as expected"""
295 expected = 'chunk0chunk42'
296 image = self.client.get_image(2)
297
298 self.assertEquals(expected, image)
234299
=== modified file 'tests/unit/test_teller_api.py'
--- tests/unit/test_teller_api.py 2010-10-11 19:34:10 +0000
+++ tests/unit/test_teller_api.py 2010-12-01 17:19:12 +0000
@@ -15,9 +15,10 @@
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 unittest
19
18import stubout20import stubout
19import unittest21import webob
20from webob import Request, exc
2122
22from glance.teller import controllers23from glance.teller import controllers
23from tests import stubs24from tests import stubs
@@ -27,39 +28,42 @@
27 def setUp(self):28 def setUp(self):
28 """Establish a clean test environment"""29 """Establish a clean test environment"""
29 self.stubs = stubout.StubOutForTesting()30 self.stubs = stubout.StubOutForTesting()
31 stubs.stub_out_parallax_and_teller_server(self.stubs)
32 stubs.stub_out_parallax_db_image_api(self.stubs)
33 stubs.stub_out_filesystem_backend(self.stubs)
30 self.image_controller = controllers.ImageController()34 self.image_controller = controllers.ImageController()
3135
32 def tearDown(self):36 def tearDown(self):
33 """Clear the test environment"""37 """Clear the test environment"""
38 stubs.clean_out_fake_filesystem_backend()
34 self.stubs.UnsetAll()39 self.stubs.UnsetAll()
3540
36 def test_index_image_with_no_uri_should_raise_http_bad_request(self):41 def test_index_raises_not_implemented(self):
37 # uri must be specified42 req = webob.Request.blank("/images")
38 request = Request.blank("/image")43 res = req.get_response(controllers.API())
39 response = self.image_controller.index(request)44 self.assertEquals(res.status_int, webob.exc.HTTPNotImplemented.code)
40 self.assertEqual(response.status_int, 400) # should be 422?45
4146 def test_blank_raises_not_implemented(self):
42 def test_index_image_unrecognized_registry_adapter(self):47 req = webob.Request.blank("/")
43 # FIXME: need urllib.quote here?48 res = req.get_response(controllers.API())
44 image_uri = "http://parallax-success/myacct/my-image"49 self.assertEquals(res.status_int, webob.exc.HTTPNotImplemented.code)
45 request = self._make_request(image_uri, "unknownregistry")50
46 response = self.image_controller.index(request)51 def test_detail_raises_not_implemented(self):
47 self.assertEqual(response.status_int, 400) # should be 422?52 req = webob.Request.blank("/images/detail")
4853 res = req.get_response(controllers.API())
49 def test_index_image_where_image_exists_should_return_the_data(self):54 self.assertEquals(res.status_int, webob.exc.HTTPNotImplemented.code)
50 # FIXME: need urllib.quote here?55
51 stubs.stub_out_parallax(self.stubs)56 def test_show_image_unrecognized_registry_adapter(self):
52 stubs.stub_out_filesystem_backend(self.stubs)57 req = webob.Request.blank("/images/1?registry=unknown")
53 image_uri = "http://parallax/myacct/my-image"58 res = req.get_response(controllers.API())
54 request = self._make_request(image_uri)59 self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
55 response = self.image_controller.index(request)60
56 self.assertEqual("/chunk0/chunk1", response.body)61 def test_show_image_basic(self):
5762 req = webob.Request.blank("/images/2")
58 def test_index_image_where_image_doesnt_exist_should_raise_not_found(self):63 res = req.get_response(controllers.API())
59 image_uri = "http://bad-parallax-uri/myacct/does-not-exist"64 self.assertEqual('chunk0chunk42', res.body)
60 request = self._make_request(image_uri)65
61 self.assertRaises(exc.HTTPNotFound, self.image_controller.index,66 def test_show_non_exists_image(self):
62 request)67 req = webob.Request.blank("/images/42")
6368 res = req.get_response(controllers.API())
64 def _make_request(self, image_uri, registry="parallax"):69 self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code)
65 return Request.blank("/image?uri=%s&registry=%s" % (image_uri, registry))

Subscribers

People subscribed via source and target branches