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
=== modified file 'bin/parallax-server.py'
--- bin/parallax-server.py 2010-09-29 05:14:03 +0000
+++ bin/parallax-server.py 2010-10-01 23:24:45 +0000
@@ -36,7 +36,7 @@
36from glance.common import utils36from glance.common import utils
37from glance.common import server37from glance.common import server
38from glance.common import wsgi38from glance.common import wsgi
39from glance.parallax import api39from glance.parallax import controllers
4040
4141
42FLAGS = flags.FLAGS42FLAGS = flags.FLAGS
@@ -44,7 +44,7 @@
44flags.DEFINE_integer('parallax_port', 9191, 'Parallax port')44flags.DEFINE_integer('parallax_port', 9191, 'Parallax port')
4545
46def main(_args):46def main(_args):
47 wsgi.run_server(api.API(), FLAGS.parallax_port)47 wsgi.run_server(controllers.API(), FLAGS.parallax_port)
4848
49if __name__ == '__main__':49if __name__ == '__main__':
50 utils.default_flagfile()50 utils.default_flagfile()
5151
=== added file 'bin/teller-server.py'
--- bin/teller-server.py 1970-01-01 00:00:00 +0000
+++ bin/teller-server.py 2010-10-01 23:24:45 +0000
@@ -0,0 +1,51 @@
1#!/usr/bin/env python
2# pylint: disable-msg=C0103
3# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
5# Copyright 2010 United States Government as represented by the
6# Administrator of the National Aeronautics and Space Administration.
7# All Rights Reserved.
8#
9# Licensed under the Apache License, Version 2.0 (the "License");
10# you may not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS,
17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20"""
21Teller API daemon.
22"""
23
24import os
25import sys
26
27# If ../parallax/__init__.py exists, add ../ to Python search path, so that
28# it will override what happens to be installed in /usr/(local/)lib/python...
29possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
30 os.pardir,
31 os.pardir))
32if os.path.exists(os.path.join(possible_topdir, 'teller', '__init__.py')):
33 sys.path.insert(0, possible_topdir)
34
35from glance.common import flags
36from glance.common import utils
37from glance.common import server
38from glance.common import wsgi
39from glance.teller import controllers
40
41
42FLAGS = flags.FLAGS
43# TODO(sirp): ensure no conflicts in port selection
44flags.DEFINE_integer('teller_port', 9292, 'Teller port')
45
46def main(_args):
47 wsgi.run_server(controllers.API(), FLAGS.teller_port)
48
49if __name__ == '__main__':
50 utils.default_flagfile()
51 server.serve('teller-server', main)
052
=== modified file 'glance/common/db/__init__.py'
--- glance/common/db/__init__.py 2010-09-29 05:14:03 +0000
+++ glance/common/db/__init__.py 2010-10-01 23:24:45 +0000
@@ -20,4 +20,3 @@
20DB abstraction for Nova and Glance20DB abstraction for Nova and Glance
21"""21"""
2222
23from glance.common.db.api import *
2423
=== modified file 'glance/common/db/sqlalchemy/__init__.py'
--- glance/common/db/sqlalchemy/__init__.py 2010-09-29 05:14:03 +0000
+++ glance/common/db/sqlalchemy/__init__.py 2010-10-01 23:24:45 +0000
@@ -1,24 +0,0 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20SQLAlchemy database backend
21"""
22from glance.common.db.sqlalchemy import models
23
24models.register_models()
250
=== modified file 'glance/common/wsgi.py'
--- glance/common/wsgi.py 2010-09-29 04:54:27 +0000
+++ glance/common/wsgi.py 2010-10-01 23:24:45 +0000
@@ -23,6 +23,7 @@
2323
24import logging24import logging
25import sys25import sys
26import datetime
2627
27import eventlet28import eventlet
28import eventlet.wsgi29import eventlet.wsgi
@@ -260,7 +261,12 @@
260261
261 def _to_json(self, data):262 def _to_json(self, data):
262 import json263 import json
263 return json.dumps(data)264 def sanitizer(obj):
265 if isinstance(obj, datetime.datetime):
266 return obj.isoformat()
267 return obj
268
269 return json.dumps(data, default=sanitizer)
264270
265 def _to_xml(self, data):271 def _to_xml(self, data):
266 metadata = self.metadata.get('application/xml', {})272 metadata = self.metadata.get('application/xml', {})
267273
=== removed directory 'glance/parallax/api'
=== removed file 'glance/parallax/api/__init__.py'
--- glance/parallax/api/__init__.py 2010-09-29 20:51:21 +0000
+++ glance/parallax/api/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,49 +0,0 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20Parallax API controllers.
21"""
22
23import json
24import time
25
26import routes
27import webob.dec
28import webob.exc
29import webob
30
31from glance.common import flags
32from glance.common import utils
33from glance.common import wsgi
34from glance.parallax.api import images
35
36
37FLAGS = flags.FLAGS
38
39
40class API(wsgi.Router):
41 """WSGI entry point for all Parallax requests."""
42
43 def __init__(self):
44 # TODO(sirp): should we add back the middleware for parallax
45 mapper = routes.Mapper()
46 mapper.resource("image", "images", controller=images.Controller(),
47 collection={'detail': 'GET'})
48 super(API, self).__init__(mapper)
49
500
=== removed file 'glance/parallax/api/images.py'
--- glance/parallax/api/images.py 2010-09-29 20:51:21 +0000
+++ glance/parallax/api/images.py 1970-01-01 00:00:00 +0000
@@ -1,82 +0,0 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18"""
19Parllax Image controller
20"""
21
22
23from glance.common import wsgi
24from glance.common import db
25from glance.common import exception
26from webob import exc
27
28
29class Controller(wsgi.Controller):
30 """Image Controller """
31
32 # TODO(sirp): this is not currently used, but should eventually
33 # incorporate it
34 _serialization_metadata = {
35 'application/xml': {
36 "attributes": {
37 "image": [ "id", "name", "updated", "created", "status",
38 "serverId", "progress" ]
39 }
40 }
41 }
42
43 def index(self, req):
44 """Index is not currently supported """
45 raise exc.HTTPNotImplemented()
46
47 def detail(self, req):
48 """Detail is not currently supported """
49 raise exc.HTTPNotImplemented()
50
51 def show(self, req, id):
52 """Return data about the given image id."""
53 try:
54 image = db.image_get(None, id)
55 except exception.NotFound:
56 raise exc.HTTPNotFound()
57
58 file_dicts = [dict(location=f.location, size=f.size)
59 for f in image.files]
60
61 metadata_dicts = [dict(key=m.key, value=m.value)
62 for m in image.metadata]
63
64 return dict(id=image.id,
65 name=image.name,
66 state=image.state,
67 public=image.public,
68 files=file_dicts,
69 metadata=metadata_dicts)
70
71 def delete(self, req, id):
72 """Delete is not currently supported """
73 raise exc.HTTPNotImplemented()
74
75 def create(self, req):
76 """Create is not currently supported """
77 raise exc.HTTPNotImplemented()
78
79 def update(self, req, id):
80 """Update is not currently supported """
81 raise exc.HTTPNotImplemented()
82
830
=== added file 'glance/parallax/controllers.py'
--- glance/parallax/controllers.py 1970-01-01 00:00:00 +0000
+++ glance/parallax/controllers.py 2010-10-01 23:24:45 +0000
@@ -0,0 +1,114 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17"""
18Parllax Image controller
19"""
20
21import routes
22from glance.common import wsgi
23from glance.common import exception
24from glance.parallax import db
25from webob import exc
26
27
28class ImageController(wsgi.Controller):
29 """Image Controller """
30
31 def index(self, req):
32 """Return data for all public, non-deleted images """
33 images = db.image_get_all_public(None)
34 image_dicts = [self._make_image_dict(i) for i in images]
35 return dict(images=image_dicts)
36
37 def detail(self, req):
38 """Detail is not currently supported """
39 raise exc.HTTPNotImplemented()
40
41 def show(self, req, id):
42 """Return data about the given image id."""
43 try:
44 image = db.image_get(None, id)
45 except exception.NotFound:
46 raise exc.HTTPNotFound()
47
48 return dict(image=self._make_image_dict(image))
49
50 def delete(self, req, id):
51 """Delete is not currently supported """
52 raise exc.HTTPNotImplemented()
53
54 def create(self, req):
55 """Create is not currently supported """
56 raise exc.HTTPNotImplemented()
57
58 def update(self, req, id):
59 """Update is not currently supported """
60 raise exc.HTTPNotImplemented()
61
62 @staticmethod
63 def _make_image_dict(image):
64 """ Create a dict represenation of an image which we can use to
65 serialize the image.
66 """
67 def _fetch_attrs(obj, attrs):
68 return dict([(a, getattr(obj, a)) for a in attrs])
69
70 # attributes common to all models
71 base_attrs = set(['id', 'created_at', 'updated_at', 'deleted_at',
72 'deleted'])
73
74 file_attrs = base_attrs | set(['location', 'size'])
75 files = [_fetch_attrs(f, file_attrs) for f in image.files]
76
77 # TODO(sirp): should this be a dict, or a list of dicts?
78 # A plain dict is more convenient, but list of dicts would provide
79 # access to created_at, etc
80 metadata = dict((m.key, m.value) for m in image.metadata
81 if not m.deleted)
82
83 image_attrs = base_attrs | set(['name', 'image_type', 'state', 'public'])
84
85 image_dict = _fetch_attrs(image, image_attrs)
86 image_dict['files'] = files
87 image_dict['metadata'] = metadata
88 return image_dict
89
90 return dict(id=image.id,
91 name=image.name,
92 state=image.state,
93 public=image.public,
94 files=files,
95 metadata=metadata)
96
97
98class API(wsgi.Router):
99 """WSGI entry point for all Parallax requests."""
100
101 def __init__(self):
102 # TODO(sirp): should we add back the middleware for parallax?
103 mapper = routes.Mapper()
104 mapper.resource("image", "images", controller=ImageController(),
105 collection={'detail': 'GET'})
106 super(API, self).__init__(mapper)
107
108
109
110
111
112
113
114
0115
=== added directory 'glance/parallax/db'
=== added file 'glance/parallax/db/__init__.py'
--- glance/parallax/db/__init__.py 1970-01-01 00:00:00 +0000
+++ glance/parallax/db/__init__.py 2010-10-01 23:24:45 +0000
@@ -0,0 +1,23 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
4# Copyright 2010 United States Government as represented by the
5# Administrator of the National Aeronautics and Space Administration.
6# All Rights Reserved.
7#
8# Licensed under the Apache License, Version 2.0 (the "License"); you may
9# not use this file except in compliance with the License. You may obtain
10# a copy of the License at
11#
12# http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17# License for the specific language governing permissions and limitations
18# under the License.
19"""
20DB abstraction for Nova and Glance
21"""
22
23from glance.parallax.db.api import *
024
=== renamed file 'glance/common/db/api.py' => 'glance/parallax/db/api.py'
--- glance/common/db/api.py 2010-09-29 20:51:21 +0000
+++ glance/parallax/db/api.py 2010-10-01 23:24:45 +0000
@@ -30,7 +30,7 @@
3030
3131
32IMPL = utils.LazyPluggable(FLAGS['db_backend'],32IMPL = utils.LazyPluggable(FLAGS['db_backend'],
33 sqlalchemy='glance.common.db.sqlalchemy.api')33 sqlalchemy='glance.parallax.db.sqlalchemy.api')
3434
3535
36###################36###################
@@ -41,7 +41,6 @@
41 return IMPL.image_create(context, values)41 return IMPL.image_create(context, values)
4242
4343
44
45def image_destroy(context, image_id):44def image_destroy(context, image_id):
46 """Destroy the image or raise if it does not exist."""45 """Destroy the image or raise if it does not exist."""
47 return IMPL.image_destroy(context, image_id)46 return IMPL.image_destroy(context, image_id)
@@ -57,6 +56,11 @@
57 return IMPL.image_get_all(context)56 return IMPL.image_get_all(context)
5857
5958
59def image_get_all_public(context, public=True):
60 """Get all public images."""
61 return IMPL.image_get_all_public(context, public=public)
62
63
60def image_get_by_str(context, str_id):64def image_get_by_str(context, str_id):
61 """Get an image by string id."""65 """Get an image by string id."""
62 return IMPL.image_get_by_str(context, str_id)66 return IMPL.image_get_by_str(context, str_id)
6367
=== added directory 'glance/parallax/db/sqlalchemy'
=== added file 'glance/parallax/db/sqlalchemy/__init__.py'
--- glance/parallax/db/sqlalchemy/__init__.py 1970-01-01 00:00:00 +0000
+++ glance/parallax/db/sqlalchemy/__init__.py 2010-10-01 23:24:45 +0000
@@ -0,0 +1,27 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 United States Government as represented by the
4# Administrator of the National Aeronautics and Space Administration.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""
20SQLAlchemy models for glance data
21"""
22
23from glance.parallax.db.sqlalchemy import models
24
25
26models.register_models()
27
028
=== renamed file 'glance/common/db/sqlalchemy/api.py' => 'glance/parallax/db/sqlalchemy/api.py'
--- glance/common/db/sqlalchemy/api.py 2010-09-29 20:51:21 +0000
+++ glance/parallax/db/sqlalchemy/api.py 2010-10-01 23:24:45 +0000
@@ -23,8 +23,8 @@
23from glance.common import db23from glance.common import db
24from glance.common import exception24from glance.common import exception
25from glance.common import flags25from glance.common import flags
26from glance.common.db.sqlalchemy import models
27from glance.common.db.sqlalchemy.session import get_session26from glance.common.db.sqlalchemy.session import get_session
27from glance.parallax.db.sqlalchemy import models
28from sqlalchemy.orm import exc28from sqlalchemy.orm import exc
2929
30#from sqlalchemy.orm import joinedload_all30#from sqlalchemy.orm import joinedload_all
@@ -78,16 +78,24 @@
78 except exc.NoResultFound:78 except exc.NoResultFound:
79 new_exc = exception.NotFound("No model for id %s" % image_id)79 new_exc = exception.NotFound("No model for id %s" % image_id)
80 raise new_exc.__class__, new_exc, sys.exc_info()[2]80 raise new_exc.__class__, new_exc, sys.exc_info()[2]
81 return models.Image.find(image_id, deleted=_deleted(context))
8281
8382
84def image_get_all(context):83def image_get_all(context):
85 session = get_session()84 session = get_session()
86 # TODO(sirp): add back eager loading85 return session.query(models.Image
87 return session.query(models.Image86 ).options(joinedload(models.Image.files)
88 #).options(joinedload(models.Image.files)87 ).options(joinedload(models.Image.metadata)
89 #).options(joinedload(models.Image.metadata)88 ).filter_by(deleted=_deleted(context)
90 ).filter_by(deleted=_deleted(context)89 ).all()
90
91
92def image_get_all_public(context, public):
93 session = get_session()
94 return session.query(models.Image
95 ).options(joinedload(models.Image.files)
96 ).options(joinedload(models.Image.metadata)
97 ).filter_by(deleted=_deleted(context)
98 ).filter_by(public=public
91 ).all()99 ).all()
92100
93101
94102
=== renamed file 'glance/common/db/sqlalchemy/models.py' => 'glance/parallax/db/sqlalchemy/models.py'
--- glance/common/db/sqlalchemy/models.py 2010-09-29 20:51:21 +0000
+++ glance/parallax/db/sqlalchemy/models.py 2010-10-01 23:24:45 +0000
@@ -173,7 +173,7 @@
173class ImageMetadatum(BASE, ModelBase):173class ImageMetadatum(BASE, ModelBase):
174 """Represents an image metadata in the datastore"""174 """Represents an image metadata in the datastore"""
175 __tablename__ = 'image_metadata'175 __tablename__ = 'image_metadata'
176 __prefix__ = 'mdata'176 __prefix__ = 'img-meta'
177 id = Column(Integer, primary_key=True)177 id = Column(Integer, primary_key=True)
178 image_id = Column(Integer, ForeignKey('images.id'), nullable=False)178 image_id = Column(Integer, ForeignKey('images.id'), nullable=False)
179 image = relationship(Image, backref=backref('metadata'))179 image = relationship(Image, backref=backref('metadata'))
180180
=== removed directory 'glance/teller/api'
=== removed file 'glance/teller/api/__init__.py'
=== removed file 'glance/teller/api/images.py'
--- glance/teller/api/images.py 2010-10-01 01:16:15 +0000
+++ glance/teller/api/images.py 1970-01-01 00:00:00 +0000
@@ -1,75 +0,0 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18from glance.common import wsgi, db, exception
19from glance.teller.backends import get_from_backend
20from glance.teller.parallax import ParallaxAdapter
21from webob import exc, Response
22
23
24class Controller(wsgi.Controller):
25 """Image Controller """
26
27 image_lookup_fn = ParallaxAdapter.lookup
28
29 def index(self, request):
30 """ Get a list of images, does this even make sense? """
31 raise exc.HTTPNotImplemented
32
33 def detail(self, req):
34 """Detail is not currently supported """
35 raise exc.HTTPNotImplemented()
36
37 def show(self, request, uri):
38 """
39 Query the parallax service for the image registry for the passed in
40 request['uri']. If it exists, we connect to the appropriate backend as
41 determined by the URI scheme and yield chunks of data back to the
42 client.
43 """
44
45 #info(twitch) I don't know if this would actually happen in the wild.
46 if uri is None:
47 return exc.HTTPBadRequest(body="Missing uri", request=request,
48 content_type="text/plain")
49
50 image = self.image_lookup_fn(uri)
51 if not image:
52 raise exc.HTTPNotFound(body='Image not found', request=request,
53 content_type='text/plain')
54
55 def image_iterator():
56 for file in image['files']:
57 for chunk in get_from_backend(file['location'],
58 expected_size=file['size']):
59 yield chunk
60
61
62 return request.get_response(Response(app_iter=image_iterator()))
63
64 def delete(self, req, id):
65 """Delete is not currently supported """
66 raise exc.HTTPNotImplemented()
67
68 def create(self, req):
69 """Create is not currently supported """
70 raise exc.HTTPNotImplemented()
71
72 def update(self, req, id):
73 """Update is not currently supported """
74 raise exc.HTTPNotImplemented()
75
760
=== modified file 'glance/teller/backends.py'
--- glance/teller/backends.py 2010-09-28 20:18:30 +0000
+++ glance/teller/backends.py 2010-10-01 23:24:45 +0000
@@ -21,6 +21,14 @@
21import urlparse21import urlparse
2222
2323
24def _file_iter(f, size):
25 """ Return an iterator for a file-like object """
26 chunk = f.read(size)
27 while chunk:
28 yield chunk
29 chunk = f.read(size)
30
31
24class BackendException(Exception):32class BackendException(Exception):
25 pass33 pass
2634
@@ -53,19 +61,18 @@
53 return p61 return p
5462
55 with opener(sanitize_path(parsed_uri.path)) as f:63 with opener(sanitize_path(parsed_uri.path)) as f:
56 chunk = f.read(cls.CHUNKSIZE)64 return _file_iter(f, cls.CHUNKSIZE)
57 while chunk:
58 yield chunk
59 chunk = f.read(cls.CHUNKSIZE)
60 65
6166
62class HTTPBackend(Backend):67class HTTPBackend(Backend):
68 """ An implementation of the HTTP Backend Adapter """
69
63 @classmethod70 @classmethod
64 def get(cls, parsed_uri, expected_size, conn_class=None):71 def get(cls, parsed_uri, expected_size, conn_class=None):
65 """72 """Takes a parsed uri for an HTTP resource, fetches it, ane yields the
66 http://netloc/path/to/file.tar.gz.073 data.
67 https://netloc/path/to/file.tar.gz.074 """
68 """75
69 if conn_class:76 if conn_class:
70 pass # use the conn_class passed in77 pass # use the conn_class passed in
71 elif parsed_uri.scheme == "http":78 elif parsed_uri.scheme == "http":
@@ -74,14 +81,12 @@
74 conn_class = httplib.HTTPSConnection81 conn_class = httplib.HTTPSConnection
75 else:82 else:
76 raise BackendException("scheme '%s' not supported for HTTPBackend")83 raise BackendException("scheme '%s' not supported for HTTPBackend")
84
77 conn = conn_class(parsed_uri.netloc)85 conn = conn_class(parsed_uri.netloc)
78 conn.request("GET", parsed_uri.path, "", {})86 conn.request("GET", parsed_uri.path, "", {})
87
79 try:88 try:
80 response = conn.getresponse()89 return _file_iter(conn.getresponse(), cls.CHUNKSIZE)
81 chunk = response.read(cls.CHUNKSIZE)
82 while chunk:
83 yield chunk
84 chunk = response.read(cls.CHUNKSIZE)
85 finally:90 finally:
86 conn.close()91 conn.close()
8792
@@ -130,24 +135,22 @@
130 % (expected_size, obj.size))135 % (expected_size, obj.size))
131136
132137
133def _scheme2backend(scheme):138BACKENDS = {
134 return {139 "file": FilesystemBackend,
135 "file": FilesystemBackend,140 "http": HTTPBackend,
136 "http": HTTPBackend,141 "https": HTTPBackend,
137 "https": HTTPBackend,142 "swift": SwiftBackend,
138 "swift": SwiftBackend,143 "teststr": TestStrBackend
139 "teststr": TestStrBackend144}
140 }[scheme]
141
142145
143def get_from_backend(uri, **kwargs):146def get_from_backend(uri, **kwargs):
144 """147 """ Yields chunks of data from backend specified by uri """
145 Yields chunks of data from backend specified by uri
146 """
147 parsed_uri = urlparse.urlparse(uri)148 parsed_uri = urlparse.urlparse(uri)
149
148 try:150 try:
149 return _scheme2backend(parsed_uri.scheme).get(parsed_uri, **kwargs)151 backend = BACKENDS[parsed_uri.scheme]
150 except KeyError:152 except KeyError:
151 raise UnsupportedBackend("No backend found for '%s'" % parsed_uri.scheme)153 raise UnsupportedBackend("No backend found for '%s'" % parsed_uri.scheme)
152154
155 return backend.get(parsed_uri, **kwargs)
153156
154157
=== added file 'glance/teller/controllers.py'
--- glance/teller/controllers.py 1970-01-01 00:00:00 +0000
+++ glance/teller/controllers.py 2010-10-01 23:24:45 +0000
@@ -0,0 +1,109 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack LLC.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17"""
18Teller Image controller
19"""
20
21import routes
22from webob import exc, Response
23from glance.common import wsgi
24from glance.common import exception
25from glance.parallax import db
26from glance.teller import backends
27from glance.teller import registries
28
29
30class ImageController(wsgi.Controller):
31 """Image Controller """
32
33 def index(self, req):
34 """
35 Query the parallax service for the image registry for the passed in
36 req['uri']. If it exists, we connect to the appropriate backend as
37 determined by the URI scheme and yield chunks of data back to the
38 client.
39
40 Optionally, we can pass in 'registry' which will use a given
41 RegistryAdapter for the request. This is useful for testing.
42 """
43 try:
44 uri = req.str_GET['uri']
45 except KeyError:
46 return exc.HTTPBadRequest(body="Missing uri", request=req,
47 content_type="text/plain")
48
49 registry = req.str_GET.get('registry', 'parallax')
50
51 try:
52 image = registries.lookup_by_registry(registry, uri)
53 except registries.UnknownRegistryAdapter:
54 return exc.HTTPBadRequest(body="Uknown registry '%s'" % registry,
55 request=req,
56 content_type="text/plain")
57
58 if not image:
59 raise exc.HTTPNotFound(body='Image not found', request=req,
60 content_type='text/plain')
61
62 def image_iterator():
63 for file in image['files']:
64 chunks = backends.get_from_backend(
65 file['location'], expected_size=file['size'])
66
67 for chunk in chunks:
68 yield chunk
69
70
71 return req.get_response(Response(app_iter=image_iterator()))
72
73 def detail(self, req):
74 """Detail is not currently supported """
75 raise exc.HTTPNotImplemented()
76
77 def show(self, req):
78 """Show is not currently supported """
79 raise exc.HTTPNotImplemented()
80
81 def delete(self, req, id):
82 """Delete is not currently supported """
83 raise exc.HTTPNotImplemented()
84
85 def create(self, req):
86 """Create is not currently supported """
87 raise exc.HTTPNotImplemented()
88
89 def update(self, req, id):
90 """Update is not currently supported """
91 raise exc.HTTPNotImplemented()
92
93
94class API(wsgi.Router):
95 """WSGI entry point for all Parallax requests."""
96
97 def __init__(self):
98 mapper = routes.Mapper()
99 mapper.resource("image", "image", controller=ImageController(),
100 collection={'detail': 'GET'})
101 super(API, self).__init__(mapper)
102
103
104
105
106
107
108
109
0110
=== renamed file 'glance/teller/parallax.py' => 'glance/teller/registries.py'
--- glance/teller/parallax.py 2010-10-01 01:16:15 +0000
+++ glance/teller/registries.py 2010-10-01 23:24:45 +0000
@@ -20,7 +20,28 @@
20import urlparse20import urlparse
2121
2222
23class ParallaxAdapter(object):23class RegistryAdapterException(Exception):
24 """ Base class for all RegistryAdapter exceptions """
25 pass
26
27
28class UnknownRegistryAdapter(RegistryAdapterException):
29 """ Raised if we don't recognize the requested Registry protocol """
30 pass
31
32
33class RegistryAdapter(object):
34 """ Base class for all image endpoints """
35
36 @classmethod
37 def lookup(cls, image_uri):
38 """ Subclasses must define a lookup method which returns an dictionary
39 representing the image.
40 """
41 raise NotImplementedError
42
43
44class ParallaxAdapter(RegistryAdapter):
24 """45 """
25 ParallaxAdapter stuff46 ParallaxAdapter stuff
26 """47 """
@@ -47,9 +68,13 @@
47 # The image exists68 # The image exists
48 if response.status == 200: 69 if response.status == 200:
49 result = response.read()70 result = response.read()
71 image_json = json.loads(result)
72
73 try:
74 return image_json["image"]
75 except KeyError:
76 raise RegistryAdapterException("Missing 'image' key")
5077
51 json = json.loads(result)
52 return json
53 finally:78 finally:
54 conn.close()79 conn.close()
5580
@@ -63,10 +88,25 @@
63 def lookup(cls, image_uri):88 def lookup(cls, image_uri):
64 if image_uri.count("success"):89 if image_uri.count("success"):
65 # A successful attempt90 # A successful attempt
66 mock_res = { "files":[{"location":"teststr://chunk0", 91 files = [dict(location="teststr://chunk0", size=1235),
67 "size":1235},92 dict(location="teststr://chunk1", size=12345)]
68 {"location": "teststr://chunk1", 93
69 "size":12345}]}94 mock_res = dict(files=files)
95
70 return mock_res96 return mock_res
7197
98REGISTRY_ADAPTERS = {
99 'parallax': ParallaxAdapter,
100 'fake_parallax': FakeParallaxAdapter
101}
102
103def lookup_by_registry(registry, image_uri):
104 """ Convenience function to lookup based on a registry protocol """
105 try:
106 adapter = REGISTRY_ADAPTERS[registry]
107 except KeyError:
108 raise UnknownRegistryAdapter("'%s' not found" % registry)
109
110 return adapter.lookup(image_uri)
111
72112
73113
=== modified file 'tests/test_data.py'
--- tests/test_data.py 2010-09-29 20:51:21 +0000
+++ tests/test_data.py 2010-10-01 23:24:45 +0000
@@ -15,30 +15,30 @@
15# License for the specific language governing permissions and limitations15# License for the specific language governing permissions and limitations
16# under the License.16# under the License.
1717
18from glance.common.db import api18from glance.parallax import db
1919
2020
21def make_fake_image():21def make_fake_image():
22 """Create a fake image record """22 """Create a fake image record """
23 image = api.image_create(23 image = db.image_create(
24 None,24 None,
25 dict(name="Test Image",25 dict(name="Test Image",
26 state="available",26 state="available",
27 public=True,27 public=True,
28 image_type="tarball"))28 image_type="tarball"))
2929
30 api.image_file_create(30 db.image_file_create(
31 None, 31 None,
32 dict(image_id=image.id,32 dict(image_id=image.id,
33 location="swift://myacct/mycontainer/obj.tar.gz.0",33 location="teststr://chunk0",
34 size=101))34 size=6))
35 api.image_file_create(35 db.image_file_create(
36 None, 36 None,
37 dict(image_id=image.id,37 dict(image_id=image.id,
38 location="swift://myacct/mycontainer/obj.tar.gz.1",38 location="teststr://chunk1",
39 size=101))39 size=6))
4040
41 api.image_metadatum_create(41 db.image_metadatum_create(
42 None,42 None,
43 dict(image_id=image.id,43 dict(image_id=image.id,
44 key="testkey",44 key="testkey",
4545
=== modified file 'tests/unit/test_teller_api.py'
--- tests/unit/test_teller_api.py 2010-10-01 01:16:15 +0000
+++ tests/unit/test_teller_api.py 2010-10-01 23:24:45 +0000
@@ -1,33 +1,41 @@
1import unittest1import unittest
2from webob import Request, exc2from webob import Request, exc
3from glance.teller import parallax3from glance.teller import controllers
4from glance.teller.api import images as image_server
54
6class TestImageController(unittest.TestCase):5class TestImageController(unittest.TestCase):
7 def setUp(self):6 def setUp(self):
8 fake_parallax = parallax.FakeParallaxAdapter7 self.image_controller = controllers.ImageController()
98
10 self.image_controller = image_server.Controller()9 def test_index_image_with_no_uri_should_raise_http_bad_request(self):
11 self.image_controller.image_lookup_fn = fake_parallax.lookup
12
13 def test_show_image_with_no_uri_should_raise_http_bad_request(self):
14 # uri must be specified10 # uri must be specified
15 request = Request.blank("/image")11 request = Request.blank("/image")
16 response = self.image_controller.show(request, None)12 response = self.image_controller.index(request)
17 self.assertEqual(response.status_int, 400) # should be 422?13 self.assertEqual(response.status_int, 400) # should be 422?
1814
19 def test_show_image_where_image_exists_should_return_the_data(self):15 def test_index_image_unrecognized_registry_adapter(self):
20 # FIXME: need urllib.quote here?16 # FIXME: need urllib.quote here?
21 image_uri = "http://parallax-success/myacct/my-image"17 image_uri = "http://parallax-success/myacct/my-image"
22 request = Request.blank("/image?uri=%s" % image_uri)18 request = self._make_request(image_uri, "unknownregistry")
23 response = self.image_controller.show(request, image_uri)19 response = self.image_controller.index(request)
20 self.assertEqual(response.status_int, 400) # should be 422?
21
22 def test_index_image_where_image_exists_should_return_the_data(self):
23 # FIXME: need urllib.quote here?
24 image_uri = "http://parallax-success/myacct/my-image"
25 request = self._make_request(image_uri)
26 response = self.image_controller.index(request)
24 self.assertEqual("//chunk0//chunk1", response.body)27 self.assertEqual("//chunk0//chunk1", response.body)
2528
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):
27 image_uri = "http://parallax-failure/myacct/does-not-exist"30 image_uri = "http://parallax-failure/myacct/does-not-exist"
28 request = Request.blank("/image?uri=%s" % image_uri)31 request = self._make_request(image_uri)
29 self.assertRaises(exc.HTTPNotFound, self.image_controller.show, request,32 self.assertRaises(exc.HTTPNotFound, self.image_controller.index,
30 image_uri)33 request)
34
35 def _make_request(self, image_uri, registry="fake_parallax"):
36 return Request.blank(
37 "/image?uri=%s&registry=%s" % (image_uri, registry))
38
3139
32if __name__ == "__main__":40if __name__ == "__main__":
33 unittest.main()41 unittest.main()

Subscribers

People subscribed via source and target branches