Merge lp:~rconradharris/glance/add_registry_adapter into lp:~hudson-openstack/glance/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
Reviewer Review Type Date Requested Status
Christopher MacGown (community) Approve
Jay Pipes (community) Approve
Review via email: mp+37327@code.launchpad.net

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
Jay Pipes (jaypipes) wrote :

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

review: Approve
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&registry=%s" % (image_uri, registry))
1042+
1043
1044 if __name__ == "__main__":
1045 unittest.main()

Subscribers

People subscribed via source and target branches