Merge lp:~rconradharris/glance/lp615670 into lp:glance/austin

Proposed by Rick Clark
Status: Merged
Approved by: Rick Clark
Approved revision: 2
Merged at revision: 2
Proposed branch: lp:~rconradharris/glance/lp615670
Merge into: lp:glance/austin
Diff against target: 281 lines (+250/-0)
6 files modified
.bzrignore (+1/-0)
parallax/parallax/__init__.py (+16/-0)
teller/teller/backends.py (+80/-0)
teller/teller/server.py (+76/-0)
teller/tests/unit/test_backends.py (+40/-0)
teller/tests/unit/test_server.py (+37/-0)
To merge this branch: bzr merge lp:~rconradharris/glance/lp615670
Reviewer Review Type Date Requested Status
Michael Gundlach (community) Needs Fixing
Rick Clark (community) code Approve
Soren Hansen Pending
Review via email: mp+34921@code.launchpad.net

This proposal supersedes a proposal from 2010-08-25.

Commit message

First cut of backends and image controller.

Description of the change

First cut of backends and image controller. Not much here, just getting the ball rolling.

To post a comment you must log in.
Revision history for this message
Cory Wright (corywright) : Posted in a previous version of this proposal
review: Approve
Revision history for this message
Rick Clark (dendrobates) wrote :

Framework looks good

review: Approve (code)
Revision history for this message
Michael Gundlach (gundlach) wrote :
Download full text (3.2 KiB)

Overall, looks good. Some style stuff and a couple of small but real complaints:

If this project is supposed to adhere to the same style guidelines as Nova, give every class and every method a docstring. E.g. it took me a while to get started understanding the parallax backend code because there was nothing to guide me, and I still don't know what the ImageRegistry docstring means.

In a similar vein, you should have a blank line between class line and docstring and 1st function, like

class Foo(object):

    """docstring"""

    def f1(self):
        """docstring"""
        ...

    ...

Sign your TODOs, like TODO(gundlach): so they aren't unclaimed.

You've got duplicate code in the while loops that yield CHUNKSIZE chunks. I'd move that duplicated code into Backend (which makes sense since Backend defined CHUNKSIZE -- it already understands that things will be read in chunks, so a generator that yields chunks would make sense living on Backend.)

Your try/except KeyError has too much in the try statement. Call _scheme2backend in the try only, then outside the try/except call get(). This way a KeyError in get() won't be misunderstood. It'll make the code look uglier, which hurts (I like how it looks now, visually) but it'll be less prone to bugs.

I thought the **kwargs passed to get_from_backend were YAGNI, but I see they are used for testing -- brilliant :)

If Glance is supposed to adhere to the same pylint rules as Nova, you're going to get flagged for PPRINT_OBJ unless you pylint: disable-msg the relevant rule. I don't personally care since we aren't enforcing pylint yet, but FYI.

Also FYI -- no action required if you don't want -- jaypipes(I think?) is making an openstack.common project for code common to nova/swift/glance/etc. One thing that will live in there is nova.wsgi.Router, which would help tease apart ImageController's two tasks (routing, and handling the requested action). You might copy nova.wsgi.Router into your project until openstack.common is ready, if you like. I'd be happy to help if you'd like handholding. Finally, if you haven't seen the webob.dec.wsgify decorator, check it out -- it is great for making WSGI apps look even simpler. It lets __call__ return Response objects directly, or strings, or webob.exc exceptions; and it eliminates start_response. With wsgi.Router and wsgify combined you'd have something like

class Router(wsgi.Router):
  def __init__(self):
    controller = ImageController()
    mapper = routes.Mapper()
    mapper.connect('/healthcheck', controller=controller, action='healthcheck')
    mapper.connect('/', controller=controller, action='fetch', conditions=dict(method="GET"))
    mapper.connect('/', controller=controller, action='put', conditions=dict(method="PUT"))
    mapper.connect('/', controller=controller, action='delete', conditions=dict(method="DELETE"))
    super(Router, self).__init__(mapper)

class ImageController(object):
  @webob.dec.wsgify
  def __call__(self, req):
    action = req.environ['wsgiorg.routing_args'][1]['action']
    return getattr(self, action)(req)
  def healthcheck(self, req):
    # healthcheck
  def fetch(self, req):
    # as before, but ends with "...

Read more...

review: Needs Fixing
Revision history for this message
Rick Harris (rconradharris) wrote :
Download full text (3.9 KiB)

Thanks for the comments!
> Overall, looks good. Some style stuff and a couple of small but real
> complaints:
>
>
> If this project is supposed to adhere to the same style guidelines as Nova,
> give every class and every method a docstring. E.g. it took me a while to get
> started understanding the parallax backend code because there was nothing to
> guide me, and I still don't know what the ImageRegistry docstring means.
>
> In a similar vein, you should have a blank line between class line and
> docstring and 1st function, like
>
> class Foo(object):
>
> """docstring"""
>
> def f1(self):
> """docstring"""
> ...
>
> ...
>
>
> Sign your TODOs, like TODO(gundlach): so they aren't unclaimed.

I think glance should follow the nova coding standards (for better or worse), so I agree with all of these suggestions.

> You've got duplicate code in the while loops that yield CHUNKSIZE chunks. I'd
> move that duplicated code into Backend (which makes sense since Backend
> defined CHUNKSIZE -- it already understands that things will be read in
> chunks, so a generator that yields chunks would make sense living on Backend.)

You're right, the code isn't particularly DRY. Wrapping a file-like object in a generator is a common pattern and merits its own helper-function.

> Your try/except KeyError has too much in the try statement. Call
> _scheme2backend in the try only, then outside the try/except call get(). This
> way a KeyError in get() won't be misunderstood. It'll make the code look
> uglier, which hurts (I like how it looks now, visually) but it'll be less
> prone to bugs.

You are 100% right here, this is bad form.

> I thought the **kwargs passed to get_from_backend were YAGNI, but I see they
> are used for testing -- brilliant :)
>
> If Glance is supposed to adhere to the same pylint rules as Nova, you're going
> to get flagged for PPRINT_OBJ unless you pylint: disable-msg the relevant
> rule. I don't personally care since we aren't enforcing pylint yet, but FYI.

Agree, should probably make this pylint compliant (or ax it entirely).

> Also FYI -- no action required if you don't want -- jaypipes(I think?) is
> making an openstack.common project for code common to nova/swift/glance/etc.
> One thing that will live in there is nova.wsgi.Router, which would help tease
> apart ImageController's two tasks (routing, and handling the requested
> action). You might copy nova.wsgi.Router into your project until
> openstack.common is ready, if you like. I'd be happy to help if you'd like
> handholding. Finally, if you haven't seen the webob.dec.wsgify decorator,
> check it out -- it is great for making WSGI apps look even simpler. It lets
> __call__ return Response objects directly, or strings, or webob.exc
> exceptions; and it eliminates start_response. With wsgi.Router and wsgify
> combined you'd have something like
>
> class Router(wsgi.Router):
> def __init__(self):
> controller = ImageController()
> mapper = routes.Mapper()
> mapper.connect('/healthcheck', controller=controller,
> action='healthcheck')
> mapper.connect('/', controller=controller, action='fetch',
> conditions=dict(met...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2010-09-08 21:07:40 +0000
4@@ -0,0 +1,1 @@
5+backends.pyc
6
7=== modified file 'parallax/parallax/__init__.py'
8--- parallax/parallax/__init__.py 2010-08-06 03:06:42 +0000
9+++ parallax/parallax/__init__.py 2010-09-08 21:07:40 +0000
10@@ -0,0 +1,16 @@
11+class ImageRegistry(object):
12+ """
13+ {
14+ "uuid": uuid,
15+ "slug": slug,
16+ "name": name,
17+ "objects": [
18+ { "uri": obj1_uri },
19+ { "uri": obj2_uri }
20+ ]
21+ }
22+ """
23+ def __init__(self):
24+ pass
25+
26+
27
28=== added file 'teller/teller/backends.py'
29--- teller/teller/backends.py 1970-01-01 00:00:00 +0000
30+++ teller/teller/backends.py 2010-09-08 21:07:40 +0000
31@@ -0,0 +1,80 @@
32+import urlparse
33+
34+class BackendException(Exception):
35+ pass
36+class UnsupportedBackend(BackendException):
37+ pass
38+
39+class Backend(object):
40+ CHUNKSIZE = 4096
41+
42+class TestStrBackend(Backend):
43+ @classmethod
44+ def get(cls, parsed_uri):
45+ """
46+ teststr://data
47+ """
48+ yield parsed_uri.netloc
49+
50+class FilesystemBackend(Backend):
51+ @classmethod
52+ def get(cls, parsed_uri, opener=lambda p: open(p, "b")):
53+ """
54+ file:///path/to/file.tar.gz.0
55+ """
56+ def sanitize_path(p):
57+ #FIXME: must prevent attacks using ".." and "." paths
58+ return p
59+
60+ with opener(sanitize_path(parsed_uri.path)) as f:
61+ chunk = f.read(cls.CHUNKSIZE)
62+ while chunk:
63+ yield chunk
64+ chunk = f.read(cls.CHUNKSIZE)
65+
66+class HTTPBackend(Backend):
67+ @classmethod
68+ def get(cls, parsed_uri, conn_class=None):
69+ """
70+ http://netloc/path/to/file.tar.gz.0
71+ https://netloc/path/to/file.tar.gz.0
72+ """
73+ import httplib
74+ if conn_class:
75+ pass # use the conn_class passed in
76+ elif parsed_uri.scheme == "http":
77+ conn_class = httplib.HTTPConnection
78+ elif parsed_uri.scheme == "https":
79+ conn_class = httplib.HTTPSConnection
80+ else:
81+ raise BackendException("scheme '%s' not support for HTTPBackend")
82+ conn = conn_class(parsed_uri.netloc)
83+ conn.request("GET", parsed_uri.path, "", {})
84+ try:
85+ response = conn.getresponse()
86+ chunk = response.read(cls.CHUNKSIZE)
87+ while chunk:
88+ yield chunk
89+ chunk = response.read(cls.CHUNKSIZE)
90+ finally:
91+ conn.close()
92+
93+def _scheme2backend(scheme):
94+ return {
95+ "file": FilesystemBackend,
96+ "http": HTTPBackend,
97+ "https": HTTPBackend,
98+ "teststr": TestStrBackend
99+ }[scheme]
100+
101+def get_from_backend(uri, **kwargs):
102+ """
103+ Yields chunks of data from backend specified by uri
104+ """
105+ parsed_uri = urlparse.urlparse(uri)
106+ try:
107+ return _scheme2backend(parsed_uri.scheme).get(parsed_uri, **kwargs)
108+ except KeyError:
109+ raise UnsupportedBackend("No backend found for '%s'" % parsed_uri.scheme)
110+
111+
112
113=== added file 'teller/teller/server.py'
114--- teller/teller/server.py 1970-01-01 00:00:00 +0000
115+++ teller/teller/server.py 2010-09-08 21:07:40 +0000
116@@ -0,0 +1,76 @@
117+from webob import Request, Response, UTC
118+from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPCreated, \
119+ HTTPInternalServerError, HTTPNoContent, HTTPNotFound, \
120+ HTTPNotModified, HTTPPreconditionFailed, \
121+ HTTPRequestTimeout, HTTPUnprocessableEntity, HTTPMethodNotAllowed
122+from teller.backends import get_from_backend
123+
124+def PPRINT_OBJ(obj):
125+ from pprint import pprint
126+ pprint(obj.__dict__)
127+ print dir(obj)
128+
129+class ImageController(object):
130+ """Implements the WSGI application for the Teller Image Server."""
131+ def __init__(self, conf):
132+ """
133+ """
134+ #TODO: add real lookup fn
135+ #self.image_lookup_fn = parallax_lookup()
136+ self.log_requests = conf.get('log_requests', 't')[:1].lower() == 't'
137+
138+ def GET(self, request):
139+ try:
140+ uri = request.GET['uri']
141+ except KeyError:
142+ return HTTPBadRequest(body="Missing uri", request=request,
143+ content_type="text/plain")
144+
145+ image = self.image_lookup_fn(uri)
146+ if not image:
147+ return HTTPNotFound(body="Image not found", request=request,
148+ content_type="text/plain")
149+
150+ def image_iter():
151+ for obj in image["objects"]:
152+ for chunk in get_from_backend(obj["uri"]):
153+ yield chunk
154+
155+ return request.get_response(Response(app_iter=image_iter()))
156+
157+ def __call__(self, env, start_response):
158+ """WSGI Application entry point for the Teller Image Server."""
159+ start_time = time.time()
160+ req = Request(env)
161+ if False:
162+ pass
163+ #if req.path_info == '/healthcheck':
164+ # return healthcheck(req)(env, start_response)
165+ #elif not check_xml_encodable(req.path_info):
166+ # res = HTTPPreconditionFailed(body='Invalid UTF8')
167+ else:
168+ try:
169+ if hasattr(self, req.method):
170+ res = getattr(self, req.method)(req)
171+ else:
172+ res = HTTPMethodNotAllowed()
173+ except:
174+ self.logger.exception('ERROR __call__ error with %s %s '
175+ % (env.get('REQUEST_METHOD', '-'), env.get('PATH_INFO', '-')))
176+ res = HTTPInternalServerError(body=traceback.format_exc())
177+ trans_time = time.time() - start_time
178+ if self.log_requests:
179+ log_line = '%s - - [%s] "%s %s" %s %s "%s" "%s" %.4f' % (
180+ req.remote_addr,
181+ time.strftime('%d/%b/%Y:%H:%M:%S +0000',
182+ time.gmtime()),
183+ req.method, req.path, res.status.split()[0],
184+ res.content_length or '-', req.referer or '-',
185+ req.user_agent or '-',
186+ trans_time)
187+ self.logger.info(log_line)
188+ #if req.method in ('PUT', 'DELETE'):
189+ # slow = self.slow - trans_time
190+ # if slow > 0:
191+ # sleep(slow)
192+ return res(env, start_response)
193
194=== added directory 'teller/tests/functional'
195=== added directory 'teller/tests/unit'
196=== added file 'teller/tests/unit/test_backends.py'
197--- teller/tests/unit/test_backends.py 1970-01-01 00:00:00 +0000
198+++ teller/tests/unit/test_backends.py 2010-09-08 21:07:40 +0000
199@@ -0,0 +1,40 @@
200+import unittest
201+from StringIO import StringIO
202+from teller.backends import Backend, get_from_backend
203+
204+class TestBackends(unittest.TestCase):
205+ def setUp(self):
206+ Backend.CHUNKSIZE = 2
207+
208+ def test_filesystem_get_from_backend(self):
209+ class FakeFile(object):
210+ def __enter__(self, *args, **kwargs):
211+ return StringIO('fakedata')
212+ def __exit__(self, *args, **kwargs):
213+ pass
214+
215+ fetcher = get_from_backend("file:///path/to/file.tar.gz",
216+ opener=lambda _: FakeFile())
217+
218+ chunks = [c for c in fetcher]
219+ self.assertEqual(chunks, ["fa", "ke", "da", "ta"])
220+
221+ def test_http_get_from_backend(self):
222+ class FakeHTTPConnection(object):
223+ def __init__(self, *args, **kwargs):
224+ pass
225+ def request(self, *args, **kwargs):
226+ pass
227+ def getresponse(self):
228+ return StringIO('fakedata')
229+ def close(self):
230+ pass
231+
232+ fetcher = get_from_backend("http://netloc/path/to/file.tar.gz",
233+ conn_class=FakeHTTPConnection)
234+
235+ chunks = [c for c in fetcher]
236+ self.assertEqual(chunks, ["fa", "ke", "da", "ta"])
237+
238+if __name__ == "__main__":
239+ unittest.main()
240
241=== added file 'teller/tests/unit/test_server.py'
242--- teller/tests/unit/test_server.py 1970-01-01 00:00:00 +0000
243+++ teller/tests/unit/test_server.py 2010-09-08 21:07:40 +0000
244@@ -0,0 +1,37 @@
245+import unittest
246+from webob import Request
247+#TODO: should this be teller.image ?
248+from teller import server as image_server
249+
250+class TestImageController(unittest.TestCase):
251+ def setUp(self):
252+ conf = {}
253+ self.image_controller = image_server.ImageController(conf)
254+
255+ def test_GET(self):
256+ # uri must be specified
257+ request = Request.blank("/image")
258+ response = self.image_controller.GET(request)
259+ self.assertEqual(response.status_int, 400) # should be 422?
260+
261+ # FIXME: need urllib.quote here?
262+ image_uri = "http://parallax/myacct/my-image"
263+ request = Request.blank("/image?uri=%s" % image_uri)
264+ def mock_parallax_lookup(uri):
265+ return {"objects": [{"uri": "teststr://chunk0"},
266+ {"uri": "teststr://chunk1"}]}
267+
268+ self.image_controller.image_lookup_fn = mock_parallax_lookup
269+ response = self.image_controller.GET(request)
270+ self.assertEqual("chunk0chunk1", response.body)
271+
272+ image_uri = "http://parallax/myacct/does-not-exist"
273+ request = Request.blank("/image?uri=%s" % image_uri)
274+ def mock_parallax_lookup(uri):
275+ return None
276+ self.image_controller.image_lookup_fn = mock_parallax_lookup
277+ response = self.image_controller.GET(request)
278+ self.assertEqual(response.status_int, 404)
279+
280+if __name__ == "__main__":
281+ unittest.main()

Subscribers

People subscribed via source and target branches