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