Merge lp:~rconradharris/glance/add_registry_adapter into lp:~hudson-openstack/glance/trunk
- add_registry_adapter
- Merge into trunk
Proposed by
Rick Harris
Status: | Merged |
---|---|
Approved by: | Christopher MacGown |
Approved revision: | 15 |
Merged at revision: | 10 |
Proposed branch: | lp:~rconradharris/glance/add_registry_adapter |
Merge into: | lp:~hudson-openstack/glance/trunk |
Diff against target: |
1045 lines (+472/-310) 19 files modified
bin/parallax-server.py (+2/-2) bin/teller-server.py (+51/-0) glance/common/db/__init__.py (+0/-1) glance/common/db/sqlalchemy/__init__.py (+0/-24) glance/common/wsgi.py (+7/-1) glance/parallax/api/__init__.py (+0/-49) glance/parallax/api/images.py (+0/-82) glance/parallax/controllers.py (+114/-0) glance/parallax/db/__init__.py (+23/-0) glance/parallax/db/api.py (+6/-2) glance/parallax/db/sqlalchemy/__init__.py (+27/-0) glance/parallax/db/sqlalchemy/api.py (+15/-7) glance/parallax/db/sqlalchemy/models.py (+1/-1) glance/teller/api/images.py (+0/-75) glance/teller/backends.py (+29/-26) glance/teller/controllers.py (+109/-0) glance/teller/registries.py (+47/-7) tests/test_data.py (+13/-13) tests/unit/test_teller_api.py (+28/-20) |
To merge this branch: | bzr merge lp:~rconradharris/glance/add_registry_adapter |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher MacGown (community) | Approve | ||
Jay Pipes (community) | Approve | ||
Review via email: mp+37327@code.launchpad.net |
Commit message
Description of the change
This patch:
* Decouples Controller for ParallaxAdapter implementation by adding generic RegistryAdapter and providing a lookup function
* Adds base model attributes to Parallax's JSON (created_at, etc)
To post a comment you must log in.
Revision history for this message
Christopher MacGown (0x44) wrote : | # |
This is great, thanks Rick.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'bin/parallax-server.py' | |||
2 | --- bin/parallax-server.py 2010-09-29 05:14:03 +0000 | |||
3 | +++ bin/parallax-server.py 2010-10-01 23:24:45 +0000 | |||
4 | @@ -36,7 +36,7 @@ | |||
5 | 36 | from glance.common import utils | 36 | from glance.common import utils |
6 | 37 | from glance.common import server | 37 | from glance.common import server |
7 | 38 | from glance.common import wsgi | 38 | from glance.common import wsgi |
9 | 39 | from glance.parallax import api | 39 | from glance.parallax import controllers |
10 | 40 | 40 | ||
11 | 41 | 41 | ||
12 | 42 | FLAGS = flags.FLAGS | 42 | FLAGS = flags.FLAGS |
13 | @@ -44,7 +44,7 @@ | |||
14 | 44 | flags.DEFINE_integer('parallax_port', 9191, 'Parallax port') | 44 | flags.DEFINE_integer('parallax_port', 9191, 'Parallax port') |
15 | 45 | 45 | ||
16 | 46 | def main(_args): | 46 | def main(_args): |
18 | 47 | wsgi.run_server(api.API(), FLAGS.parallax_port) | 47 | wsgi.run_server(controllers.API(), FLAGS.parallax_port) |
19 | 48 | 48 | ||
20 | 49 | if __name__ == '__main__': | 49 | if __name__ == '__main__': |
21 | 50 | utils.default_flagfile() | 50 | utils.default_flagfile() |
22 | 51 | 51 | ||
23 | === added file 'bin/teller-server.py' | |||
24 | --- bin/teller-server.py 1970-01-01 00:00:00 +0000 | |||
25 | +++ bin/teller-server.py 2010-10-01 23:24:45 +0000 | |||
26 | @@ -0,0 +1,51 @@ | |||
27 | 1 | #!/usr/bin/env python | ||
28 | 2 | # pylint: disable-msg=C0103 | ||
29 | 3 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
30 | 4 | |||
31 | 5 | # Copyright 2010 United States Government as represented by the | ||
32 | 6 | # Administrator of the National Aeronautics and Space Administration. | ||
33 | 7 | # All Rights Reserved. | ||
34 | 8 | # | ||
35 | 9 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
36 | 10 | # you may not use this file except in compliance with the License. | ||
37 | 11 | # You may obtain a copy of the License at | ||
38 | 12 | # | ||
39 | 13 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
40 | 14 | # | ||
41 | 15 | # Unless required by applicable law or agreed to in writing, software | ||
42 | 16 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
43 | 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
44 | 18 | # See the License for the specific language governing permissions and | ||
45 | 19 | # limitations under the License. | ||
46 | 20 | """ | ||
47 | 21 | Teller API daemon. | ||
48 | 22 | """ | ||
49 | 23 | |||
50 | 24 | import os | ||
51 | 25 | import sys | ||
52 | 26 | |||
53 | 27 | # If ../parallax/__init__.py exists, add ../ to Python search path, so that | ||
54 | 28 | # it will override what happens to be installed in /usr/(local/)lib/python... | ||
55 | 29 | possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), | ||
56 | 30 | os.pardir, | ||
57 | 31 | os.pardir)) | ||
58 | 32 | if os.path.exists(os.path.join(possible_topdir, 'teller', '__init__.py')): | ||
59 | 33 | sys.path.insert(0, possible_topdir) | ||
60 | 34 | |||
61 | 35 | from glance.common import flags | ||
62 | 36 | from glance.common import utils | ||
63 | 37 | from glance.common import server | ||
64 | 38 | from glance.common import wsgi | ||
65 | 39 | from glance.teller import controllers | ||
66 | 40 | |||
67 | 41 | |||
68 | 42 | FLAGS = flags.FLAGS | ||
69 | 43 | # TODO(sirp): ensure no conflicts in port selection | ||
70 | 44 | flags.DEFINE_integer('teller_port', 9292, 'Teller port') | ||
71 | 45 | |||
72 | 46 | def main(_args): | ||
73 | 47 | wsgi.run_server(controllers.API(), FLAGS.teller_port) | ||
74 | 48 | |||
75 | 49 | if __name__ == '__main__': | ||
76 | 50 | utils.default_flagfile() | ||
77 | 51 | server.serve('teller-server', main) | ||
78 | 0 | 52 | ||
79 | === modified file 'glance/common/db/__init__.py' | |||
80 | --- glance/common/db/__init__.py 2010-09-29 05:14:03 +0000 | |||
81 | +++ glance/common/db/__init__.py 2010-10-01 23:24:45 +0000 | |||
82 | @@ -20,4 +20,3 @@ | |||
83 | 20 | DB abstraction for Nova and Glance | 20 | DB abstraction for Nova and Glance |
84 | 21 | """ | 21 | """ |
85 | 22 | 22 | ||
86 | 23 | from glance.common.db.api import * | ||
87 | 24 | 23 | ||
88 | === modified file 'glance/common/db/sqlalchemy/__init__.py' | |||
89 | --- glance/common/db/sqlalchemy/__init__.py 2010-09-29 05:14:03 +0000 | |||
90 | +++ glance/common/db/sqlalchemy/__init__.py 2010-10-01 23:24:45 +0000 | |||
91 | @@ -1,24 +0,0 @@ | |||
92 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
93 | 2 | |||
94 | 3 | # Copyright 2010 United States Government as represented by the | ||
95 | 4 | # Administrator of the National Aeronautics and Space Administration. | ||
96 | 5 | # All Rights Reserved. | ||
97 | 6 | # | ||
98 | 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
99 | 8 | # not use this file except in compliance with the License. You may obtain | ||
100 | 9 | # a copy of the License at | ||
101 | 10 | # | ||
102 | 11 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
103 | 12 | # | ||
104 | 13 | # Unless required by applicable law or agreed to in writing, software | ||
105 | 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
106 | 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
107 | 16 | # License for the specific language governing permissions and limitations | ||
108 | 17 | # under the License. | ||
109 | 18 | |||
110 | 19 | """ | ||
111 | 20 | SQLAlchemy database backend | ||
112 | 21 | """ | ||
113 | 22 | from glance.common.db.sqlalchemy import models | ||
114 | 23 | |||
115 | 24 | models.register_models() | ||
116 | 25 | 0 | ||
117 | === modified file 'glance/common/wsgi.py' | |||
118 | --- glance/common/wsgi.py 2010-09-29 04:54:27 +0000 | |||
119 | +++ glance/common/wsgi.py 2010-10-01 23:24:45 +0000 | |||
120 | @@ -23,6 +23,7 @@ | |||
121 | 23 | 23 | ||
122 | 24 | import logging | 24 | import logging |
123 | 25 | import sys | 25 | import sys |
124 | 26 | import datetime | ||
125 | 26 | 27 | ||
126 | 27 | import eventlet | 28 | import eventlet |
127 | 28 | import eventlet.wsgi | 29 | import eventlet.wsgi |
128 | @@ -260,7 +261,12 @@ | |||
129 | 260 | 261 | ||
130 | 261 | def _to_json(self, data): | 262 | def _to_json(self, data): |
131 | 262 | import json | 263 | import json |
133 | 263 | return json.dumps(data) | 264 | def sanitizer(obj): |
134 | 265 | if isinstance(obj, datetime.datetime): | ||
135 | 266 | return obj.isoformat() | ||
136 | 267 | return obj | ||
137 | 268 | |||
138 | 269 | return json.dumps(data, default=sanitizer) | ||
139 | 264 | 270 | ||
140 | 265 | def _to_xml(self, data): | 271 | def _to_xml(self, data): |
141 | 266 | metadata = self.metadata.get('application/xml', {}) | 272 | metadata = self.metadata.get('application/xml', {}) |
142 | 267 | 273 | ||
143 | === removed directory 'glance/parallax/api' | |||
144 | === removed file 'glance/parallax/api/__init__.py' | |||
145 | --- glance/parallax/api/__init__.py 2010-09-29 20:51:21 +0000 | |||
146 | +++ glance/parallax/api/__init__.py 1970-01-01 00:00:00 +0000 | |||
147 | @@ -1,49 +0,0 @@ | |||
148 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
149 | 2 | |||
150 | 3 | # Copyright 2010 United States Government as represented by the | ||
151 | 4 | # Administrator of the National Aeronautics and Space Administration. | ||
152 | 5 | # All Rights Reserved. | ||
153 | 6 | # | ||
154 | 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
155 | 8 | # not use this file except in compliance with the License. You may obtain | ||
156 | 9 | # a copy of the License at | ||
157 | 10 | # | ||
158 | 11 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
159 | 12 | # | ||
160 | 13 | # Unless required by applicable law or agreed to in writing, software | ||
161 | 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
162 | 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
163 | 16 | # License for the specific language governing permissions and limitations | ||
164 | 17 | # under the License. | ||
165 | 18 | |||
166 | 19 | """ | ||
167 | 20 | Parallax API controllers. | ||
168 | 21 | """ | ||
169 | 22 | |||
170 | 23 | import json | ||
171 | 24 | import time | ||
172 | 25 | |||
173 | 26 | import routes | ||
174 | 27 | import webob.dec | ||
175 | 28 | import webob.exc | ||
176 | 29 | import webob | ||
177 | 30 | |||
178 | 31 | from glance.common import flags | ||
179 | 32 | from glance.common import utils | ||
180 | 33 | from glance.common import wsgi | ||
181 | 34 | from glance.parallax.api import images | ||
182 | 35 | |||
183 | 36 | |||
184 | 37 | FLAGS = flags.FLAGS | ||
185 | 38 | |||
186 | 39 | |||
187 | 40 | class API(wsgi.Router): | ||
188 | 41 | """WSGI entry point for all Parallax requests.""" | ||
189 | 42 | |||
190 | 43 | def __init__(self): | ||
191 | 44 | # TODO(sirp): should we add back the middleware for parallax | ||
192 | 45 | mapper = routes.Mapper() | ||
193 | 46 | mapper.resource("image", "images", controller=images.Controller(), | ||
194 | 47 | collection={'detail': 'GET'}) | ||
195 | 48 | super(API, self).__init__(mapper) | ||
196 | 49 | |||
197 | 50 | 0 | ||
198 | === removed file 'glance/parallax/api/images.py' | |||
199 | --- glance/parallax/api/images.py 2010-09-29 20:51:21 +0000 | |||
200 | +++ glance/parallax/api/images.py 1970-01-01 00:00:00 +0000 | |||
201 | @@ -1,82 +0,0 @@ | |||
202 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
203 | 2 | |||
204 | 3 | # Copyright 2010 OpenStack LLC. | ||
205 | 4 | # All Rights Reserved. | ||
206 | 5 | # | ||
207 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
208 | 7 | # not use this file except in compliance with the License. You may obtain | ||
209 | 8 | # a copy of the License at | ||
210 | 9 | # | ||
211 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
212 | 11 | # | ||
213 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
214 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
215 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
216 | 15 | # License for the specific language governing permissions and limitations | ||
217 | 16 | # under the License. | ||
218 | 17 | |||
219 | 18 | """ | ||
220 | 19 | Parllax Image controller | ||
221 | 20 | """ | ||
222 | 21 | |||
223 | 22 | |||
224 | 23 | from glance.common import wsgi | ||
225 | 24 | from glance.common import db | ||
226 | 25 | from glance.common import exception | ||
227 | 26 | from webob import exc | ||
228 | 27 | |||
229 | 28 | |||
230 | 29 | class Controller(wsgi.Controller): | ||
231 | 30 | """Image Controller """ | ||
232 | 31 | |||
233 | 32 | # TODO(sirp): this is not currently used, but should eventually | ||
234 | 33 | # incorporate it | ||
235 | 34 | _serialization_metadata = { | ||
236 | 35 | 'application/xml': { | ||
237 | 36 | "attributes": { | ||
238 | 37 | "image": [ "id", "name", "updated", "created", "status", | ||
239 | 38 | "serverId", "progress" ] | ||
240 | 39 | } | ||
241 | 40 | } | ||
242 | 41 | } | ||
243 | 42 | |||
244 | 43 | def index(self, req): | ||
245 | 44 | """Index is not currently supported """ | ||
246 | 45 | raise exc.HTTPNotImplemented() | ||
247 | 46 | |||
248 | 47 | def detail(self, req): | ||
249 | 48 | """Detail is not currently supported """ | ||
250 | 49 | raise exc.HTTPNotImplemented() | ||
251 | 50 | |||
252 | 51 | def show(self, req, id): | ||
253 | 52 | """Return data about the given image id.""" | ||
254 | 53 | try: | ||
255 | 54 | image = db.image_get(None, id) | ||
256 | 55 | except exception.NotFound: | ||
257 | 56 | raise exc.HTTPNotFound() | ||
258 | 57 | |||
259 | 58 | file_dicts = [dict(location=f.location, size=f.size) | ||
260 | 59 | for f in image.files] | ||
261 | 60 | |||
262 | 61 | metadata_dicts = [dict(key=m.key, value=m.value) | ||
263 | 62 | for m in image.metadata] | ||
264 | 63 | |||
265 | 64 | return dict(id=image.id, | ||
266 | 65 | name=image.name, | ||
267 | 66 | state=image.state, | ||
268 | 67 | public=image.public, | ||
269 | 68 | files=file_dicts, | ||
270 | 69 | metadata=metadata_dicts) | ||
271 | 70 | |||
272 | 71 | def delete(self, req, id): | ||
273 | 72 | """Delete is not currently supported """ | ||
274 | 73 | raise exc.HTTPNotImplemented() | ||
275 | 74 | |||
276 | 75 | def create(self, req): | ||
277 | 76 | """Create is not currently supported """ | ||
278 | 77 | raise exc.HTTPNotImplemented() | ||
279 | 78 | |||
280 | 79 | def update(self, req, id): | ||
281 | 80 | """Update is not currently supported """ | ||
282 | 81 | raise exc.HTTPNotImplemented() | ||
283 | 82 | |||
284 | 83 | 0 | ||
285 | === added file 'glance/parallax/controllers.py' | |||
286 | --- glance/parallax/controllers.py 1970-01-01 00:00:00 +0000 | |||
287 | +++ glance/parallax/controllers.py 2010-10-01 23:24:45 +0000 | |||
288 | @@ -0,0 +1,114 @@ | |||
289 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
290 | 2 | |||
291 | 3 | # Copyright 2010 OpenStack LLC. | ||
292 | 4 | # All Rights Reserved. | ||
293 | 5 | # | ||
294 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
295 | 7 | # not use this file except in compliance with the License. You may obtain | ||
296 | 8 | # a copy of the License at | ||
297 | 9 | # | ||
298 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
299 | 11 | # | ||
300 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
301 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
302 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
303 | 15 | # License for the specific language governing permissions and limitations | ||
304 | 16 | # under the License. | ||
305 | 17 | """ | ||
306 | 18 | Parllax Image controller | ||
307 | 19 | """ | ||
308 | 20 | |||
309 | 21 | import routes | ||
310 | 22 | from glance.common import wsgi | ||
311 | 23 | from glance.common import exception | ||
312 | 24 | from glance.parallax import db | ||
313 | 25 | from webob import exc | ||
314 | 26 | |||
315 | 27 | |||
316 | 28 | class ImageController(wsgi.Controller): | ||
317 | 29 | """Image Controller """ | ||
318 | 30 | |||
319 | 31 | def index(self, req): | ||
320 | 32 | """Return data for all public, non-deleted images """ | ||
321 | 33 | images = db.image_get_all_public(None) | ||
322 | 34 | image_dicts = [self._make_image_dict(i) for i in images] | ||
323 | 35 | return dict(images=image_dicts) | ||
324 | 36 | |||
325 | 37 | def detail(self, req): | ||
326 | 38 | """Detail is not currently supported """ | ||
327 | 39 | raise exc.HTTPNotImplemented() | ||
328 | 40 | |||
329 | 41 | def show(self, req, id): | ||
330 | 42 | """Return data about the given image id.""" | ||
331 | 43 | try: | ||
332 | 44 | image = db.image_get(None, id) | ||
333 | 45 | except exception.NotFound: | ||
334 | 46 | raise exc.HTTPNotFound() | ||
335 | 47 | |||
336 | 48 | return dict(image=self._make_image_dict(image)) | ||
337 | 49 | |||
338 | 50 | def delete(self, req, id): | ||
339 | 51 | """Delete is not currently supported """ | ||
340 | 52 | raise exc.HTTPNotImplemented() | ||
341 | 53 | |||
342 | 54 | def create(self, req): | ||
343 | 55 | """Create is not currently supported """ | ||
344 | 56 | raise exc.HTTPNotImplemented() | ||
345 | 57 | |||
346 | 58 | def update(self, req, id): | ||
347 | 59 | """Update is not currently supported """ | ||
348 | 60 | raise exc.HTTPNotImplemented() | ||
349 | 61 | |||
350 | 62 | @staticmethod | ||
351 | 63 | def _make_image_dict(image): | ||
352 | 64 | """ Create a dict represenation of an image which we can use to | ||
353 | 65 | serialize the image. | ||
354 | 66 | """ | ||
355 | 67 | def _fetch_attrs(obj, attrs): | ||
356 | 68 | return dict([(a, getattr(obj, a)) for a in attrs]) | ||
357 | 69 | |||
358 | 70 | # attributes common to all models | ||
359 | 71 | base_attrs = set(['id', 'created_at', 'updated_at', 'deleted_at', | ||
360 | 72 | 'deleted']) | ||
361 | 73 | |||
362 | 74 | file_attrs = base_attrs | set(['location', 'size']) | ||
363 | 75 | files = [_fetch_attrs(f, file_attrs) for f in image.files] | ||
364 | 76 | |||
365 | 77 | # TODO(sirp): should this be a dict, or a list of dicts? | ||
366 | 78 | # A plain dict is more convenient, but list of dicts would provide | ||
367 | 79 | # access to created_at, etc | ||
368 | 80 | metadata = dict((m.key, m.value) for m in image.metadata | ||
369 | 81 | if not m.deleted) | ||
370 | 82 | |||
371 | 83 | image_attrs = base_attrs | set(['name', 'image_type', 'state', 'public']) | ||
372 | 84 | |||
373 | 85 | image_dict = _fetch_attrs(image, image_attrs) | ||
374 | 86 | image_dict['files'] = files | ||
375 | 87 | image_dict['metadata'] = metadata | ||
376 | 88 | return image_dict | ||
377 | 89 | |||
378 | 90 | return dict(id=image.id, | ||
379 | 91 | name=image.name, | ||
380 | 92 | state=image.state, | ||
381 | 93 | public=image.public, | ||
382 | 94 | files=files, | ||
383 | 95 | metadata=metadata) | ||
384 | 96 | |||
385 | 97 | |||
386 | 98 | class API(wsgi.Router): | ||
387 | 99 | """WSGI entry point for all Parallax requests.""" | ||
388 | 100 | |||
389 | 101 | def __init__(self): | ||
390 | 102 | # TODO(sirp): should we add back the middleware for parallax? | ||
391 | 103 | mapper = routes.Mapper() | ||
392 | 104 | mapper.resource("image", "images", controller=ImageController(), | ||
393 | 105 | collection={'detail': 'GET'}) | ||
394 | 106 | super(API, self).__init__(mapper) | ||
395 | 107 | |||
396 | 108 | |||
397 | 109 | |||
398 | 110 | |||
399 | 111 | |||
400 | 112 | |||
401 | 113 | |||
402 | 114 | |||
403 | 0 | 115 | ||
404 | === added directory 'glance/parallax/db' | |||
405 | === added file 'glance/parallax/db/__init__.py' | |||
406 | --- glance/parallax/db/__init__.py 1970-01-01 00:00:00 +0000 | |||
407 | +++ glance/parallax/db/__init__.py 2010-10-01 23:24:45 +0000 | |||
408 | @@ -0,0 +1,23 @@ | |||
409 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
410 | 2 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
411 | 3 | |||
412 | 4 | # Copyright 2010 United States Government as represented by the | ||
413 | 5 | # Administrator of the National Aeronautics and Space Administration. | ||
414 | 6 | # All Rights Reserved. | ||
415 | 7 | # | ||
416 | 8 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
417 | 9 | # not use this file except in compliance with the License. You may obtain | ||
418 | 10 | # a copy of the License at | ||
419 | 11 | # | ||
420 | 12 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
421 | 13 | # | ||
422 | 14 | # Unless required by applicable law or agreed to in writing, software | ||
423 | 15 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
424 | 16 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
425 | 17 | # License for the specific language governing permissions and limitations | ||
426 | 18 | # under the License. | ||
427 | 19 | """ | ||
428 | 20 | DB abstraction for Nova and Glance | ||
429 | 21 | """ | ||
430 | 22 | |||
431 | 23 | from glance.parallax.db.api import * | ||
432 | 0 | 24 | ||
433 | === renamed file 'glance/common/db/api.py' => 'glance/parallax/db/api.py' | |||
434 | --- glance/common/db/api.py 2010-09-29 20:51:21 +0000 | |||
435 | +++ glance/parallax/db/api.py 2010-10-01 23:24:45 +0000 | |||
436 | @@ -30,7 +30,7 @@ | |||
437 | 30 | 30 | ||
438 | 31 | 31 | ||
439 | 32 | IMPL = utils.LazyPluggable(FLAGS['db_backend'], | 32 | IMPL = utils.LazyPluggable(FLAGS['db_backend'], |
441 | 33 | sqlalchemy='glance.common.db.sqlalchemy.api') | 33 | sqlalchemy='glance.parallax.db.sqlalchemy.api') |
442 | 34 | 34 | ||
443 | 35 | 35 | ||
444 | 36 | ################### | 36 | ################### |
445 | @@ -41,7 +41,6 @@ | |||
446 | 41 | return IMPL.image_create(context, values) | 41 | return IMPL.image_create(context, values) |
447 | 42 | 42 | ||
448 | 43 | 43 | ||
449 | 44 | |||
450 | 45 | def image_destroy(context, image_id): | 44 | def image_destroy(context, image_id): |
451 | 46 | """Destroy the image or raise if it does not exist.""" | 45 | """Destroy the image or raise if it does not exist.""" |
452 | 47 | return IMPL.image_destroy(context, image_id) | 46 | return IMPL.image_destroy(context, image_id) |
453 | @@ -57,6 +56,11 @@ | |||
454 | 57 | return IMPL.image_get_all(context) | 56 | return IMPL.image_get_all(context) |
455 | 58 | 57 | ||
456 | 59 | 58 | ||
457 | 59 | def image_get_all_public(context, public=True): | ||
458 | 60 | """Get all public images.""" | ||
459 | 61 | return IMPL.image_get_all_public(context, public=public) | ||
460 | 62 | |||
461 | 63 | |||
462 | 60 | def image_get_by_str(context, str_id): | 64 | def image_get_by_str(context, str_id): |
463 | 61 | """Get an image by string id.""" | 65 | """Get an image by string id.""" |
464 | 62 | return IMPL.image_get_by_str(context, str_id) | 66 | return IMPL.image_get_by_str(context, str_id) |
465 | 63 | 67 | ||
466 | === added directory 'glance/parallax/db/sqlalchemy' | |||
467 | === added file 'glance/parallax/db/sqlalchemy/__init__.py' | |||
468 | --- glance/parallax/db/sqlalchemy/__init__.py 1970-01-01 00:00:00 +0000 | |||
469 | +++ glance/parallax/db/sqlalchemy/__init__.py 2010-10-01 23:24:45 +0000 | |||
470 | @@ -0,0 +1,27 @@ | |||
471 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
472 | 2 | |||
473 | 3 | # Copyright 2010 United States Government as represented by the | ||
474 | 4 | # Administrator of the National Aeronautics and Space Administration. | ||
475 | 5 | # All Rights Reserved. | ||
476 | 6 | # | ||
477 | 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
478 | 8 | # not use this file except in compliance with the License. You may obtain | ||
479 | 9 | # a copy of the License at | ||
480 | 10 | # | ||
481 | 11 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
482 | 12 | # | ||
483 | 13 | # Unless required by applicable law or agreed to in writing, software | ||
484 | 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
485 | 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
486 | 16 | # License for the specific language governing permissions and limitations | ||
487 | 17 | # under the License. | ||
488 | 18 | |||
489 | 19 | """ | ||
490 | 20 | SQLAlchemy models for glance data | ||
491 | 21 | """ | ||
492 | 22 | |||
493 | 23 | from glance.parallax.db.sqlalchemy import models | ||
494 | 24 | |||
495 | 25 | |||
496 | 26 | models.register_models() | ||
497 | 27 | |||
498 | 0 | 28 | ||
499 | === renamed file 'glance/common/db/sqlalchemy/api.py' => 'glance/parallax/db/sqlalchemy/api.py' | |||
500 | --- glance/common/db/sqlalchemy/api.py 2010-09-29 20:51:21 +0000 | |||
501 | +++ glance/parallax/db/sqlalchemy/api.py 2010-10-01 23:24:45 +0000 | |||
502 | @@ -23,8 +23,8 @@ | |||
503 | 23 | from glance.common import db | 23 | from glance.common import db |
504 | 24 | from glance.common import exception | 24 | from glance.common import exception |
505 | 25 | from glance.common import flags | 25 | from glance.common import flags |
506 | 26 | from glance.common.db.sqlalchemy import models | ||
507 | 27 | from glance.common.db.sqlalchemy.session import get_session | 26 | from glance.common.db.sqlalchemy.session import get_session |
508 | 27 | from glance.parallax.db.sqlalchemy import models | ||
509 | 28 | from sqlalchemy.orm import exc | 28 | from sqlalchemy.orm import exc |
510 | 29 | 29 | ||
511 | 30 | #from sqlalchemy.orm import joinedload_all | 30 | #from sqlalchemy.orm import joinedload_all |
512 | @@ -78,16 +78,24 @@ | |||
513 | 78 | except exc.NoResultFound: | 78 | except exc.NoResultFound: |
514 | 79 | new_exc = exception.NotFound("No model for id %s" % image_id) | 79 | new_exc = exception.NotFound("No model for id %s" % image_id) |
515 | 80 | raise new_exc.__class__, new_exc, sys.exc_info()[2] | 80 | raise new_exc.__class__, new_exc, sys.exc_info()[2] |
516 | 81 | return models.Image.find(image_id, deleted=_deleted(context)) | ||
517 | 82 | 81 | ||
518 | 83 | 82 | ||
519 | 84 | def image_get_all(context): | 83 | def image_get_all(context): |
520 | 85 | session = get_session() | 84 | session = get_session() |
526 | 86 | # TODO(sirp): add back eager loading | 85 | return session.query(models.Image |
527 | 87 | return session.query(models.Image | 86 | ).options(joinedload(models.Image.files) |
528 | 88 | #).options(joinedload(models.Image.files) | 87 | ).options(joinedload(models.Image.metadata) |
529 | 89 | #).options(joinedload(models.Image.metadata) | 88 | ).filter_by(deleted=_deleted(context) |
530 | 90 | ).filter_by(deleted=_deleted(context) | 89 | ).all() |
531 | 90 | |||
532 | 91 | |||
533 | 92 | def image_get_all_public(context, public): | ||
534 | 93 | session = get_session() | ||
535 | 94 | return session.query(models.Image | ||
536 | 95 | ).options(joinedload(models.Image.files) | ||
537 | 96 | ).options(joinedload(models.Image.metadata) | ||
538 | 97 | ).filter_by(deleted=_deleted(context) | ||
539 | 98 | ).filter_by(public=public | ||
540 | 91 | ).all() | 99 | ).all() |
541 | 92 | 100 | ||
542 | 93 | 101 | ||
543 | 94 | 102 | ||
544 | === renamed file 'glance/common/db/sqlalchemy/models.py' => 'glance/parallax/db/sqlalchemy/models.py' | |||
545 | --- glance/common/db/sqlalchemy/models.py 2010-09-29 20:51:21 +0000 | |||
546 | +++ glance/parallax/db/sqlalchemy/models.py 2010-10-01 23:24:45 +0000 | |||
547 | @@ -173,7 +173,7 @@ | |||
548 | 173 | class ImageMetadatum(BASE, ModelBase): | 173 | class ImageMetadatum(BASE, ModelBase): |
549 | 174 | """Represents an image metadata in the datastore""" | 174 | """Represents an image metadata in the datastore""" |
550 | 175 | __tablename__ = 'image_metadata' | 175 | __tablename__ = 'image_metadata' |
552 | 176 | __prefix__ = 'mdata' | 176 | __prefix__ = 'img-meta' |
553 | 177 | id = Column(Integer, primary_key=True) | 177 | id = Column(Integer, primary_key=True) |
554 | 178 | image_id = Column(Integer, ForeignKey('images.id'), nullable=False) | 178 | image_id = Column(Integer, ForeignKey('images.id'), nullable=False) |
555 | 179 | image = relationship(Image, backref=backref('metadata')) | 179 | image = relationship(Image, backref=backref('metadata')) |
556 | 180 | 180 | ||
557 | === removed directory 'glance/teller/api' | |||
558 | === removed file 'glance/teller/api/__init__.py' | |||
559 | === removed file 'glance/teller/api/images.py' | |||
560 | --- glance/teller/api/images.py 2010-10-01 01:16:15 +0000 | |||
561 | +++ glance/teller/api/images.py 1970-01-01 00:00:00 +0000 | |||
562 | @@ -1,75 +0,0 @@ | |||
563 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
564 | 2 | |||
565 | 3 | # Copyright 2010 OpenStack LLC. | ||
566 | 4 | # All Rights Reserved. | ||
567 | 5 | # | ||
568 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
569 | 7 | # not use this file except in compliance with the License. You may obtain | ||
570 | 8 | # a copy of the License at | ||
571 | 9 | # | ||
572 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
573 | 11 | # | ||
574 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
575 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
576 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
577 | 15 | # License for the specific language governing permissions and limitations | ||
578 | 16 | # under the License. | ||
579 | 17 | |||
580 | 18 | from glance.common import wsgi, db, exception | ||
581 | 19 | from glance.teller.backends import get_from_backend | ||
582 | 20 | from glance.teller.parallax import ParallaxAdapter | ||
583 | 21 | from webob import exc, Response | ||
584 | 22 | |||
585 | 23 | |||
586 | 24 | class Controller(wsgi.Controller): | ||
587 | 25 | """Image Controller """ | ||
588 | 26 | |||
589 | 27 | image_lookup_fn = ParallaxAdapter.lookup | ||
590 | 28 | |||
591 | 29 | def index(self, request): | ||
592 | 30 | """ Get a list of images, does this even make sense? """ | ||
593 | 31 | raise exc.HTTPNotImplemented | ||
594 | 32 | |||
595 | 33 | def detail(self, req): | ||
596 | 34 | """Detail is not currently supported """ | ||
597 | 35 | raise exc.HTTPNotImplemented() | ||
598 | 36 | |||
599 | 37 | def show(self, request, uri): | ||
600 | 38 | """ | ||
601 | 39 | Query the parallax service for the image registry for the passed in | ||
602 | 40 | request['uri']. If it exists, we connect to the appropriate backend as | ||
603 | 41 | determined by the URI scheme and yield chunks of data back to the | ||
604 | 42 | client. | ||
605 | 43 | """ | ||
606 | 44 | |||
607 | 45 | #info(twitch) I don't know if this would actually happen in the wild. | ||
608 | 46 | if uri is None: | ||
609 | 47 | return exc.HTTPBadRequest(body="Missing uri", request=request, | ||
610 | 48 | content_type="text/plain") | ||
611 | 49 | |||
612 | 50 | image = self.image_lookup_fn(uri) | ||
613 | 51 | if not image: | ||
614 | 52 | raise exc.HTTPNotFound(body='Image not found', request=request, | ||
615 | 53 | content_type='text/plain') | ||
616 | 54 | |||
617 | 55 | def image_iterator(): | ||
618 | 56 | for file in image['files']: | ||
619 | 57 | for chunk in get_from_backend(file['location'], | ||
620 | 58 | expected_size=file['size']): | ||
621 | 59 | yield chunk | ||
622 | 60 | |||
623 | 61 | |||
624 | 62 | return request.get_response(Response(app_iter=image_iterator())) | ||
625 | 63 | |||
626 | 64 | def delete(self, req, id): | ||
627 | 65 | """Delete is not currently supported """ | ||
628 | 66 | raise exc.HTTPNotImplemented() | ||
629 | 67 | |||
630 | 68 | def create(self, req): | ||
631 | 69 | """Create is not currently supported """ | ||
632 | 70 | raise exc.HTTPNotImplemented() | ||
633 | 71 | |||
634 | 72 | def update(self, req, id): | ||
635 | 73 | """Update is not currently supported """ | ||
636 | 74 | raise exc.HTTPNotImplemented() | ||
637 | 75 | |||
638 | 76 | 0 | ||
639 | === modified file 'glance/teller/backends.py' | |||
640 | --- glance/teller/backends.py 2010-09-28 20:18:30 +0000 | |||
641 | +++ glance/teller/backends.py 2010-10-01 23:24:45 +0000 | |||
642 | @@ -21,6 +21,14 @@ | |||
643 | 21 | import urlparse | 21 | import urlparse |
644 | 22 | 22 | ||
645 | 23 | 23 | ||
646 | 24 | def _file_iter(f, size): | ||
647 | 25 | """ Return an iterator for a file-like object """ | ||
648 | 26 | chunk = f.read(size) | ||
649 | 27 | while chunk: | ||
650 | 28 | yield chunk | ||
651 | 29 | chunk = f.read(size) | ||
652 | 30 | |||
653 | 31 | |||
654 | 24 | class BackendException(Exception): | 32 | class BackendException(Exception): |
655 | 25 | pass | 33 | pass |
656 | 26 | 34 | ||
657 | @@ -53,19 +61,18 @@ | |||
658 | 53 | return p | 61 | return p |
659 | 54 | 62 | ||
660 | 55 | with opener(sanitize_path(parsed_uri.path)) as f: | 63 | with opener(sanitize_path(parsed_uri.path)) as f: |
665 | 56 | chunk = f.read(cls.CHUNKSIZE) | 64 | return _file_iter(f, cls.CHUNKSIZE) |
662 | 57 | while chunk: | ||
663 | 58 | yield chunk | ||
664 | 59 | chunk = f.read(cls.CHUNKSIZE) | ||
666 | 60 | 65 | ||
667 | 61 | 66 | ||
668 | 62 | class HTTPBackend(Backend): | 67 | class HTTPBackend(Backend): |
669 | 68 | """ An implementation of the HTTP Backend Adapter """ | ||
670 | 69 | |||
671 | 63 | @classmethod | 70 | @classmethod |
672 | 64 | def get(cls, parsed_uri, expected_size, conn_class=None): | 71 | def get(cls, parsed_uri, expected_size, conn_class=None): |
677 | 65 | """ | 72 | """Takes a parsed uri for an HTTP resource, fetches it, ane yields the |
678 | 66 | http://netloc/path/to/file.tar.gz.0 | 73 | data. |
679 | 67 | https://netloc/path/to/file.tar.gz.0 | 74 | """ |
680 | 68 | """ | 75 | |
681 | 69 | if conn_class: | 76 | if conn_class: |
682 | 70 | pass # use the conn_class passed in | 77 | pass # use the conn_class passed in |
683 | 71 | elif parsed_uri.scheme == "http": | 78 | elif parsed_uri.scheme == "http": |
684 | @@ -74,14 +81,12 @@ | |||
685 | 74 | conn_class = httplib.HTTPSConnection | 81 | conn_class = httplib.HTTPSConnection |
686 | 75 | else: | 82 | else: |
687 | 76 | raise BackendException("scheme '%s' not supported for HTTPBackend") | 83 | raise BackendException("scheme '%s' not supported for HTTPBackend") |
688 | 84 | |||
689 | 77 | conn = conn_class(parsed_uri.netloc) | 85 | conn = conn_class(parsed_uri.netloc) |
690 | 78 | conn.request("GET", parsed_uri.path, "", {}) | 86 | conn.request("GET", parsed_uri.path, "", {}) |
691 | 87 | |||
692 | 79 | try: | 88 | try: |
698 | 80 | response = conn.getresponse() | 89 | return _file_iter(conn.getresponse(), cls.CHUNKSIZE) |
694 | 81 | chunk = response.read(cls.CHUNKSIZE) | ||
695 | 82 | while chunk: | ||
696 | 83 | yield chunk | ||
697 | 84 | chunk = response.read(cls.CHUNKSIZE) | ||
699 | 85 | finally: | 90 | finally: |
700 | 86 | conn.close() | 91 | conn.close() |
701 | 87 | 92 | ||
702 | @@ -130,24 +135,22 @@ | |||
703 | 130 | % (expected_size, obj.size)) | 135 | % (expected_size, obj.size)) |
704 | 131 | 136 | ||
705 | 132 | 137 | ||
715 | 133 | def _scheme2backend(scheme): | 138 | BACKENDS = { |
716 | 134 | return { | 139 | "file": FilesystemBackend, |
717 | 135 | "file": FilesystemBackend, | 140 | "http": HTTPBackend, |
718 | 136 | "http": HTTPBackend, | 141 | "https": HTTPBackend, |
719 | 137 | "https": HTTPBackend, | 142 | "swift": SwiftBackend, |
720 | 138 | "swift": SwiftBackend, | 143 | "teststr": TestStrBackend |
721 | 139 | "teststr": TestStrBackend | 144 | } |
713 | 140 | }[scheme] | ||
714 | 141 | |||
722 | 142 | 145 | ||
723 | 143 | def get_from_backend(uri, **kwargs): | 146 | def get_from_backend(uri, **kwargs): |
727 | 144 | """ | 147 | """ Yields chunks of data from backend specified by uri """ |
725 | 145 | Yields chunks of data from backend specified by uri | ||
726 | 146 | """ | ||
728 | 147 | parsed_uri = urlparse.urlparse(uri) | 148 | parsed_uri = urlparse.urlparse(uri) |
729 | 149 | |||
730 | 148 | try: | 150 | try: |
732 | 149 | return _scheme2backend(parsed_uri.scheme).get(parsed_uri, **kwargs) | 151 | backend = BACKENDS[parsed_uri.scheme] |
733 | 150 | except KeyError: | 152 | except KeyError: |
734 | 151 | raise UnsupportedBackend("No backend found for '%s'" % parsed_uri.scheme) | 153 | raise UnsupportedBackend("No backend found for '%s'" % parsed_uri.scheme) |
735 | 152 | 154 | ||
736 | 155 | return backend.get(parsed_uri, **kwargs) | ||
737 | 153 | 156 | ||
738 | 154 | 157 | ||
739 | === added file 'glance/teller/controllers.py' | |||
740 | --- glance/teller/controllers.py 1970-01-01 00:00:00 +0000 | |||
741 | +++ glance/teller/controllers.py 2010-10-01 23:24:45 +0000 | |||
742 | @@ -0,0 +1,109 @@ | |||
743 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
744 | 2 | |||
745 | 3 | # Copyright 2010 OpenStack LLC. | ||
746 | 4 | # All Rights Reserved. | ||
747 | 5 | # | ||
748 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
749 | 7 | # not use this file except in compliance with the License. You may obtain | ||
750 | 8 | # a copy of the License at | ||
751 | 9 | # | ||
752 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
753 | 11 | # | ||
754 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
755 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
756 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
757 | 15 | # License for the specific language governing permissions and limitations | ||
758 | 16 | # under the License. | ||
759 | 17 | """ | ||
760 | 18 | Teller Image controller | ||
761 | 19 | """ | ||
762 | 20 | |||
763 | 21 | import routes | ||
764 | 22 | from webob import exc, Response | ||
765 | 23 | from glance.common import wsgi | ||
766 | 24 | from glance.common import exception | ||
767 | 25 | from glance.parallax import db | ||
768 | 26 | from glance.teller import backends | ||
769 | 27 | from glance.teller import registries | ||
770 | 28 | |||
771 | 29 | |||
772 | 30 | class ImageController(wsgi.Controller): | ||
773 | 31 | """Image Controller """ | ||
774 | 32 | |||
775 | 33 | def index(self, req): | ||
776 | 34 | """ | ||
777 | 35 | Query the parallax service for the image registry for the passed in | ||
778 | 36 | req['uri']. If it exists, we connect to the appropriate backend as | ||
779 | 37 | determined by the URI scheme and yield chunks of data back to the | ||
780 | 38 | client. | ||
781 | 39 | |||
782 | 40 | Optionally, we can pass in 'registry' which will use a given | ||
783 | 41 | RegistryAdapter for the request. This is useful for testing. | ||
784 | 42 | """ | ||
785 | 43 | try: | ||
786 | 44 | uri = req.str_GET['uri'] | ||
787 | 45 | except KeyError: | ||
788 | 46 | return exc.HTTPBadRequest(body="Missing uri", request=req, | ||
789 | 47 | content_type="text/plain") | ||
790 | 48 | |||
791 | 49 | registry = req.str_GET.get('registry', 'parallax') | ||
792 | 50 | |||
793 | 51 | try: | ||
794 | 52 | image = registries.lookup_by_registry(registry, uri) | ||
795 | 53 | except registries.UnknownRegistryAdapter: | ||
796 | 54 | return exc.HTTPBadRequest(body="Uknown registry '%s'" % registry, | ||
797 | 55 | request=req, | ||
798 | 56 | content_type="text/plain") | ||
799 | 57 | |||
800 | 58 | if not image: | ||
801 | 59 | raise exc.HTTPNotFound(body='Image not found', request=req, | ||
802 | 60 | content_type='text/plain') | ||
803 | 61 | |||
804 | 62 | def image_iterator(): | ||
805 | 63 | for file in image['files']: | ||
806 | 64 | chunks = backends.get_from_backend( | ||
807 | 65 | file['location'], expected_size=file['size']) | ||
808 | 66 | |||
809 | 67 | for chunk in chunks: | ||
810 | 68 | yield chunk | ||
811 | 69 | |||
812 | 70 | |||
813 | 71 | return req.get_response(Response(app_iter=image_iterator())) | ||
814 | 72 | |||
815 | 73 | def detail(self, req): | ||
816 | 74 | """Detail is not currently supported """ | ||
817 | 75 | raise exc.HTTPNotImplemented() | ||
818 | 76 | |||
819 | 77 | def show(self, req): | ||
820 | 78 | """Show is not currently supported """ | ||
821 | 79 | raise exc.HTTPNotImplemented() | ||
822 | 80 | |||
823 | 81 | def delete(self, req, id): | ||
824 | 82 | """Delete is not currently supported """ | ||
825 | 83 | raise exc.HTTPNotImplemented() | ||
826 | 84 | |||
827 | 85 | def create(self, req): | ||
828 | 86 | """Create is not currently supported """ | ||
829 | 87 | raise exc.HTTPNotImplemented() | ||
830 | 88 | |||
831 | 89 | def update(self, req, id): | ||
832 | 90 | """Update is not currently supported """ | ||
833 | 91 | raise exc.HTTPNotImplemented() | ||
834 | 92 | |||
835 | 93 | |||
836 | 94 | class API(wsgi.Router): | ||
837 | 95 | """WSGI entry point for all Parallax requests.""" | ||
838 | 96 | |||
839 | 97 | def __init__(self): | ||
840 | 98 | mapper = routes.Mapper() | ||
841 | 99 | mapper.resource("image", "image", controller=ImageController(), | ||
842 | 100 | collection={'detail': 'GET'}) | ||
843 | 101 | super(API, self).__init__(mapper) | ||
844 | 102 | |||
845 | 103 | |||
846 | 104 | |||
847 | 105 | |||
848 | 106 | |||
849 | 107 | |||
850 | 108 | |||
851 | 109 | |||
852 | 0 | 110 | ||
853 | === renamed file 'glance/teller/parallax.py' => 'glance/teller/registries.py' | |||
854 | --- glance/teller/parallax.py 2010-10-01 01:16:15 +0000 | |||
855 | +++ glance/teller/registries.py 2010-10-01 23:24:45 +0000 | |||
856 | @@ -20,7 +20,28 @@ | |||
857 | 20 | import urlparse | 20 | import urlparse |
858 | 21 | 21 | ||
859 | 22 | 22 | ||
861 | 23 | class ParallaxAdapter(object): | 23 | class RegistryAdapterException(Exception): |
862 | 24 | """ Base class for all RegistryAdapter exceptions """ | ||
863 | 25 | pass | ||
864 | 26 | |||
865 | 27 | |||
866 | 28 | class UnknownRegistryAdapter(RegistryAdapterException): | ||
867 | 29 | """ Raised if we don't recognize the requested Registry protocol """ | ||
868 | 30 | pass | ||
869 | 31 | |||
870 | 32 | |||
871 | 33 | class RegistryAdapter(object): | ||
872 | 34 | """ Base class for all image endpoints """ | ||
873 | 35 | |||
874 | 36 | @classmethod | ||
875 | 37 | def lookup(cls, image_uri): | ||
876 | 38 | """ Subclasses must define a lookup method which returns an dictionary | ||
877 | 39 | representing the image. | ||
878 | 40 | """ | ||
879 | 41 | raise NotImplementedError | ||
880 | 42 | |||
881 | 43 | |||
882 | 44 | class ParallaxAdapter(RegistryAdapter): | ||
883 | 24 | """ | 45 | """ |
884 | 25 | ParallaxAdapter stuff | 46 | ParallaxAdapter stuff |
885 | 26 | """ | 47 | """ |
886 | @@ -47,9 +68,13 @@ | |||
887 | 47 | # The image exists | 68 | # The image exists |
888 | 48 | if response.status == 200: | 69 | if response.status == 200: |
889 | 49 | result = response.read() | 70 | result = response.read() |
890 | 71 | image_json = json.loads(result) | ||
891 | 72 | |||
892 | 73 | try: | ||
893 | 74 | return image_json["image"] | ||
894 | 75 | except KeyError: | ||
895 | 76 | raise RegistryAdapterException("Missing 'image' key") | ||
896 | 50 | 77 | ||
897 | 51 | json = json.loads(result) | ||
898 | 52 | return json | ||
899 | 53 | finally: | 78 | finally: |
900 | 54 | conn.close() | 79 | conn.close() |
901 | 55 | 80 | ||
902 | @@ -63,10 +88,25 @@ | |||
903 | 63 | def lookup(cls, image_uri): | 88 | def lookup(cls, image_uri): |
904 | 64 | if image_uri.count("success"): | 89 | if image_uri.count("success"): |
905 | 65 | # A successful attempt | 90 | # A successful attempt |
910 | 66 | mock_res = { "files":[{"location":"teststr://chunk0", | 91 | files = [dict(location="teststr://chunk0", size=1235), |
911 | 67 | "size":1235}, | 92 | dict(location="teststr://chunk1", size=12345)] |
912 | 68 | {"location": "teststr://chunk1", | 93 | |
913 | 69 | "size":12345}]} | 94 | mock_res = dict(files=files) |
914 | 95 | |||
915 | 70 | return mock_res | 96 | return mock_res |
916 | 71 | 97 | ||
917 | 98 | REGISTRY_ADAPTERS = { | ||
918 | 99 | 'parallax': ParallaxAdapter, | ||
919 | 100 | 'fake_parallax': FakeParallaxAdapter | ||
920 | 101 | } | ||
921 | 102 | |||
922 | 103 | def lookup_by_registry(registry, image_uri): | ||
923 | 104 | """ Convenience function to lookup based on a registry protocol """ | ||
924 | 105 | try: | ||
925 | 106 | adapter = REGISTRY_ADAPTERS[registry] | ||
926 | 107 | except KeyError: | ||
927 | 108 | raise UnknownRegistryAdapter("'%s' not found" % registry) | ||
928 | 109 | |||
929 | 110 | return adapter.lookup(image_uri) | ||
930 | 111 | |||
931 | 72 | 112 | ||
932 | 73 | 113 | ||
933 | === modified file 'tests/test_data.py' | |||
934 | --- tests/test_data.py 2010-09-29 20:51:21 +0000 | |||
935 | +++ tests/test_data.py 2010-10-01 23:24:45 +0000 | |||
936 | @@ -15,30 +15,30 @@ | |||
937 | 15 | # License for the specific language governing permissions and limitations | 15 | # License for the specific language governing permissions and limitations |
938 | 16 | # under the License. | 16 | # under the License. |
939 | 17 | 17 | ||
941 | 18 | from glance.common.db import api | 18 | from glance.parallax import db |
942 | 19 | 19 | ||
943 | 20 | 20 | ||
944 | 21 | def make_fake_image(): | 21 | def make_fake_image(): |
945 | 22 | """Create a fake image record """ | 22 | """Create a fake image record """ |
947 | 23 | image = api.image_create( | 23 | image = db.image_create( |
948 | 24 | None, | 24 | None, |
949 | 25 | dict(name="Test Image", | 25 | dict(name="Test Image", |
950 | 26 | state="available", | 26 | state="available", |
951 | 27 | public=True, | 27 | public=True, |
952 | 28 | image_type="tarball")) | 28 | image_type="tarball")) |
953 | 29 | 29 | ||
964 | 30 | api.image_file_create( | 30 | db.image_file_create( |
965 | 31 | None, | 31 | None, |
966 | 32 | dict(image_id=image.id, | 32 | dict(image_id=image.id, |
967 | 33 | location="swift://myacct/mycontainer/obj.tar.gz.0", | 33 | location="teststr://chunk0", |
968 | 34 | size=101)) | 34 | size=6)) |
969 | 35 | api.image_file_create( | 35 | db.image_file_create( |
970 | 36 | None, | 36 | None, |
971 | 37 | dict(image_id=image.id, | 37 | dict(image_id=image.id, |
972 | 38 | location="swift://myacct/mycontainer/obj.tar.gz.1", | 38 | location="teststr://chunk1", |
973 | 39 | size=101)) | 39 | size=6)) |
974 | 40 | 40 | ||
976 | 41 | api.image_metadatum_create( | 41 | db.image_metadatum_create( |
977 | 42 | None, | 42 | None, |
978 | 43 | dict(image_id=image.id, | 43 | dict(image_id=image.id, |
979 | 44 | key="testkey", | 44 | key="testkey", |
980 | 45 | 45 | ||
981 | === modified file 'tests/unit/test_teller_api.py' | |||
982 | --- tests/unit/test_teller_api.py 2010-10-01 01:16:15 +0000 | |||
983 | +++ tests/unit/test_teller_api.py 2010-10-01 23:24:45 +0000 | |||
984 | @@ -1,33 +1,41 @@ | |||
985 | 1 | import unittest | 1 | import unittest |
986 | 2 | from webob import Request, exc | 2 | from webob import Request, exc |
989 | 3 | from glance.teller import parallax | 3 | from glance.teller import controllers |
988 | 4 | from glance.teller.api import images as image_server | ||
990 | 5 | 4 | ||
991 | 6 | class TestImageController(unittest.TestCase): | 5 | class TestImageController(unittest.TestCase): |
992 | 7 | def setUp(self): | 6 | def setUp(self): |
999 | 8 | fake_parallax = parallax.FakeParallaxAdapter | 7 | self.image_controller = controllers.ImageController() |
1000 | 9 | 8 | ||
1001 | 10 | self.image_controller = image_server.Controller() | 9 | def test_index_image_with_no_uri_should_raise_http_bad_request(self): |
996 | 11 | self.image_controller.image_lookup_fn = fake_parallax.lookup | ||
997 | 12 | |||
998 | 13 | def test_show_image_with_no_uri_should_raise_http_bad_request(self): | ||
1002 | 14 | # uri must be specified | 10 | # uri must be specified |
1003 | 15 | request = Request.blank("/image") | 11 | request = Request.blank("/image") |
1012 | 16 | response = self.image_controller.show(request, None) | 12 | response = self.image_controller.index(request) |
1013 | 17 | self.assertEqual(response.status_int, 400) # should be 422? | 13 | self.assertEqual(response.status_int, 400) # should be 422? |
1014 | 18 | 14 | ||
1015 | 19 | def test_show_image_where_image_exists_should_return_the_data(self): | 15 | def test_index_image_unrecognized_registry_adapter(self): |
1016 | 20 | # FIXME: need urllib.quote here? | 16 | # FIXME: need urllib.quote here? |
1017 | 21 | image_uri = "http://parallax-success/myacct/my-image" | 17 | image_uri = "http://parallax-success/myacct/my-image" |
1018 | 22 | request = Request.blank("/image?uri=%s" % image_uri) | 18 | request = self._make_request(image_uri, "unknownregistry") |
1019 | 23 | response = self.image_controller.show(request, image_uri) | 19 | response = self.image_controller.index(request) |
1020 | 20 | self.assertEqual(response.status_int, 400) # should be 422? | ||
1021 | 21 | |||
1022 | 22 | def test_index_image_where_image_exists_should_return_the_data(self): | ||
1023 | 23 | # FIXME: need urllib.quote here? | ||
1024 | 24 | image_uri = "http://parallax-success/myacct/my-image" | ||
1025 | 25 | request = self._make_request(image_uri) | ||
1026 | 26 | response = self.image_controller.index(request) | ||
1027 | 24 | self.assertEqual("//chunk0//chunk1", response.body) | 27 | self.assertEqual("//chunk0//chunk1", response.body) |
1028 | 25 | 28 | ||
1030 | 26 | def test_show_image_where_image_doesnt_exist_should_raise_not_found(self): | 29 | def test_index_image_where_image_doesnt_exist_should_raise_not_found(self): |
1031 | 27 | image_uri = "http://parallax-failure/myacct/does-not-exist" | 30 | image_uri = "http://parallax-failure/myacct/does-not-exist" |
1035 | 28 | request = Request.blank("/image?uri=%s" % image_uri) | 31 | request = self._make_request(image_uri) |
1036 | 29 | self.assertRaises(exc.HTTPNotFound, self.image_controller.show, request, | 32 | self.assertRaises(exc.HTTPNotFound, self.image_controller.index, |
1037 | 30 | image_uri) | 33 | request) |
1038 | 34 | |||
1039 | 35 | def _make_request(self, image_uri, registry="fake_parallax"): | ||
1040 | 36 | return Request.blank( | ||
1041 | 37 | "/image?uri=%s®istry=%s" % (image_uri, registry)) | ||
1042 | 38 | |||
1043 | 31 | 39 | ||
1044 | 32 | if __name__ == "__main__": | 40 | if __name__ == "__main__": |
1045 | 33 | unittest.main() | 41 | unittest.main() |
Excellent work overall, Rick.
A couple tiny issues with double-up vim headers and whitespace, but that is entirely style-related and shouldn't hold up this work. Nice work making the registry layer adaptable.
-jay