Merge lp:~jaypipes/glance/swift-backend into lp:glance/austin

Proposed by Jay Pipes
Status: Merged
Approved by: Jay Pipes
Approved revision: 4
Merge reported by: Jay Pipes
Merged at revision: not available
Proposed branch: lp:~jaypipes/glance/swift-backend
Merge into: lp:glance/austin
Diff against target: 555 lines (+445/-5)
5 files modified
.bzrignore (+1/-1)
teller/teller/backends.py (+71/-2)
teller/teller/server.py (+20/-0)
teller/tests/unit/swiftfakehttp.py (+292/-0)
teller/tests/unit/test_backends.py (+61/-2)
To merge this branch: bzr merge lp:~jaypipes/glance/swift-backend
Reviewer Review Type Date Requested Status
Glance Core security contacts Pending
Review via email: mp+36597@code.launchpad.net

Description of the change

Chris' work on Swift backend. The timestamps of revisions in his original branch were set to 0 for some reason. This branch adds all his work for revisions 3 and 4 from his implements_swift_backend branch.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2010-08-11 22:54:34 +0000
3+++ .bzrignore 2010-09-24 20:38:43 +0000
4@@ -1,1 +1,1 @@
5-backends.pyc
6+*.pyc
7
8=== modified file 'teller/teller/backends.py'
9--- teller/teller/backends.py 2010-08-12 01:17:56 +0000
10+++ teller/teller/backends.py 2010-09-24 20:38:43 +0000
11@@ -1,13 +1,38 @@
12+# vim: tabstop=4 shiftwidth=4 softtabstop=4
13+
14+# Copyright 2010 OpenStack, LLC
15+# All Rights Reserved.
16+#
17+# Licensed under the Apache License, Version 2.0 (the "License"); you may
18+# not use this file except in compliance with the License. You may obtain
19+# a copy of the License at
20+#
21+# http://www.apache.org/licenses/LICENSE-2.0
22+#
23+# Unless required by applicable law or agreed to in writing, software
24+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
25+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
26+# License for the specific language governing permissions and limitations
27+# under the License.
28+
29+import cloudfiles
30+import httplib
31+import re
32 import urlparse
33
34+
35 class BackendException(Exception):
36 pass
37+
38+
39 class UnsupportedBackend(BackendException):
40 pass
41
42+
43 class Backend(object):
44 CHUNKSIZE = 4096
45
46+
47 class TestStrBackend(Backend):
48 @classmethod
49 def get(cls, parsed_uri):
50@@ -16,6 +41,7 @@
51 """
52 yield parsed_uri.netloc
53
54+
55 class FilesystemBackend(Backend):
56 @classmethod
57 def get(cls, parsed_uri, opener=lambda p: open(p, "b")):
58@@ -32,6 +58,7 @@
59 yield chunk
60 chunk = f.read(cls.CHUNKSIZE)
61
62+
63 class HTTPBackend(Backend):
64 @classmethod
65 def get(cls, parsed_uri, conn_class=None):
66@@ -39,7 +66,6 @@
67 http://netloc/path/to/file.tar.gz.0
68 https://netloc/path/to/file.tar.gz.0
69 """
70- import httplib
71 if conn_class:
72 pass # use the conn_class passed in
73 elif parsed_uri.scheme == "http":
74@@ -47,7 +73,7 @@
75 elif parsed_uri.scheme == "https":
76 conn_class = httplib.HTTPSConnection
77 else:
78- raise BackendException("scheme '%s' not support for HTTPBackend")
79+ raise BackendException("scheme '%s' not supported for HTTPBackend")
80 conn = conn_class(parsed_uri.netloc)
81 conn.request("GET", parsed_uri.path, "", {})
82 try:
83@@ -59,14 +85,57 @@
84 finally:
85 conn.close()
86
87+class SwiftBackend(Backend):
88+ """
89+ An implementation of the swift backend adapter.
90+ """
91+
92+ RE_SWIFT_TOKENS = re.compile(r":|@|/")
93+ EXAMPLE_URL="swift://user:password@auth_url/container/file.gz.0"
94+
95+ @classmethod
96+ def get(cls, parsed_uri, conn_class=None):
97+ """
98+ Takes a parsed_uri in the format of:
99+ swift://user:password@auth_url/container/file.gz.0, connects to the
100+ swift instance at auth_url and downloads the file. Returns the generator
101+ provided by stream() on the swift object representing the file.
102+ """
103+ if conn_class:
104+ pass # Use the provided conn_class
105+ else:
106+ conn_class = cloudfiles
107+
108+ try:
109+ split_url = parsed_uri.path[2:]
110+ swift_tokens = cls.RE_SWIFT_TOKENS.split(split_url)
111+ user, api_key, authurl, container, file = swift_tokens
112+ except ValueError:
113+ raise BackendException(
114+ "Expected four values to unpack in: swift:%s. "
115+ "Should have received something like: %s."
116+ % (parsed_uri.path, cls.EXAMPLE_URL))
117+
118+ swift_conn = conn_class.get_connection(username=user, api_key=api_key,
119+ authurl=authurl)
120+
121+ container = swift_conn.get_container(container)
122+ obj = container.get_object(file)
123+
124+ # Return the generator provided from obj.stream()
125+ return obj.stream(chunksize=cls.CHUNKSIZE)
126+
127+
128 def _scheme2backend(scheme):
129 return {
130 "file": FilesystemBackend,
131 "http": HTTPBackend,
132 "https": HTTPBackend,
133+ "swift": SwiftBackend,
134 "teststr": TestStrBackend
135 }[scheme]
136
137+
138 def get_from_backend(uri, **kwargs):
139 """
140 Yields chunks of data from backend specified by uri
141
142=== modified file 'teller/teller/server.py'
143--- teller/teller/server.py 2010-08-24 04:32:57 +0000
144+++ teller/teller/server.py 2010-09-24 20:38:43 +0000
145@@ -1,15 +1,35 @@
146+# vim: tabstop=4 shiftwidth=4 softtabstop=4
147+
148+# Copyright 2010 OpenStack, LLC
149+# All Rights Reserved.
150+#
151+# Licensed under the Apache License, Version 2.0 (the "License"); you may
152+# not use this file except in compliance with the License. You may obtain
153+# a copy of the License at
154+#
155+# http://www.apache.org/licenses/LICENSE-2.0
156+#
157+# Unless required by applicable law or agreed to in writing, software
158+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
159+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
160+# License for the specific language governing permissions and limitations
161+# under the License.
162+
163 from webob import Request, Response, UTC
164 from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPCreated, \
165 HTTPInternalServerError, HTTPNoContent, HTTPNotFound, \
166 HTTPNotModified, HTTPPreconditionFailed, \
167 HTTPRequestTimeout, HTTPUnprocessableEntity, HTTPMethodNotAllowed
168+
169 from teller.backends import get_from_backend
170
171+
172 def PPRINT_OBJ(obj):
173 from pprint import pprint
174 pprint(obj.__dict__)
175 print dir(obj)
176
177+
178 class ImageController(object):
179 """Implements the WSGI application for the Teller Image Server."""
180 def __init__(self, conf):
181
182=== added file 'teller/tests/unit/swiftfakehttp.py'
183--- teller/tests/unit/swiftfakehttp.py 1970-01-01 00:00:00 +0000
184+++ teller/tests/unit/swiftfakehttp.py 2010-09-24 20:38:43 +0000
185@@ -0,0 +1,292 @@
186+# vim: tabstop=4 shiftwidth=4 softtabstop=4
187+
188+# Copyright 2010 OpenStack, LLC
189+# All Rights Reserved.
190+#
191+# Licensed under the Apache License, Version 2.0 (the "License"); you may
192+# not use this file except in compliance with the License. You may obtain
193+# a copy of the License at
194+#
195+# http://www.apache.org/licenses/LICENSE-2.0
196+#
197+# Unless required by applicable law or agreed to in writing, software
198+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
199+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
200+# License for the specific language governing permissions and limitations
201+# under the License.
202+
203+"""
204+fakehttp/socket implementation
205+
206+- TrackerSocket: an object which masquerades as a socket and responds to
207+ requests in a manner consistent with a *very* stupid CloudFS tracker.
208+
209+- CustomHTTPConnection: an object which subclasses httplib.HTTPConnection
210+ in order to replace it's socket with a TrackerSocket instance.
211+
212+The unittests each have setup methods which create freerange connection
213+instances that have had their HTTPConnection instances replaced by
214+intances of CustomHTTPConnection.
215+"""
216+
217+from httplib import HTTPConnection as connbase
218+import StringIO
219+
220+
221+class FakeSocket(object):
222+ def __init__(self):
223+ self._rbuffer = StringIO.StringIO()
224+ self._wbuffer = StringIO.StringIO()
225+
226+ def close(self):
227+ pass
228+
229+ def send(self, data, flags=0):
230+ self._rbuffer.write(data)
231+ sendall = send
232+
233+ def recv(self, len=1024, flags=0):
234+ return self._wbuffer(len)
235+
236+ def connect(self):
237+ pass
238+
239+ def makefile(self, mode, flags):
240+ return self._wbuffer
241+
242+class TrackerSocket(FakeSocket):
243+ def write(self, data):
244+ self._wbuffer.write(data)
245+ def read(self, length=-1):
246+ return self._rbuffer.read(length)
247+
248+ def _create_GET_account_content(self, path, args):
249+ if args.has_key('format') and args['format'] == 'json':
250+ containers = []
251+ containers.append('[\n');
252+ containers.append('{"name":"container1","count":2,"bytes":78},\n')
253+ containers.append('{"name":"container2","count":1,"bytes":39},\n')
254+ containers.append('{"name":"container3","count":3,"bytes":117}\n')
255+ containers.append(']\n')
256+ elif args.has_key('format') and args['format'] == 'xml':
257+ containers = []
258+ containers.append('<?xml version="1.0" encoding="UTF-8"?>\n')
259+ containers.append('<account name="FakeAccount">\n')
260+ containers.append('<container><name>container1</name>'
261+ '<count>2</count>'
262+ '<bytes>78</bytes></container>\n')
263+ containers.append('<container><name>container2</name>'
264+ '<count>1</count>'
265+ '<bytes>39</bytes></container>\n')
266+ containers.append('<container><name>container3</name>'
267+ '<count>3</count>'
268+ '<bytes>117</bytes></container>\n')
269+ containers.append('</account>\n')
270+ else:
271+ containers = ['container%s\n' % i for i in range(1,4)]
272+ return ''.join(containers)
273+
274+ def _create_GET_container_content(self, path, args):
275+ left = 0
276+ right = 9
277+ if args.has_key('offset'):
278+ left = int(args['offset'])
279+ if args.has_key('limit'):
280+ right = left + int(args['limit'])
281+
282+ if args.has_key('format') and args['format'] == 'json':
283+ objects = []
284+ objects.append('{"name":"object1",'
285+ '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
286+ '"bytes":14,'
287+ '"content_type":"application\/octet-stream",'
288+ '"last_modified":"2007-03-04 20:32:17"}')
289+ objects.append('{"name":"object2",'
290+ '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
291+ '"bytes":64,'
292+ '"content_type":"application\/octet-stream",'
293+ '"last_modified":"2007-03-04 20:32:17"}')
294+ objects.append('{"name":"object3",'
295+ '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
296+ '"bytes":14,'
297+ '"content_type":"application\/octet-stream",'
298+ '"last_modified":"2007-03-04 20:32:17"}')
299+ objects.append('{"name":"object4",'
300+ '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
301+ '"bytes":64,'
302+ '"content_type":"application\/octet-stream",'
303+ '"last_modified":"2007-03-04 20:32:17"}')
304+ objects.append('{"name":"object5",'
305+ '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
306+ '"bytes":14,'
307+ '"content_type":"application\/octet-stream",'
308+ '"last_modified":"2007-03-04 20:32:17"}')
309+ objects.append('{"name":"object6",'
310+ '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
311+ '"bytes":64,'
312+ '"content_type":"application\/octet-stream",'
313+ '"last_modified":"2007-03-04 20:32:17"}')
314+ objects.append('{"name":"object7",'
315+ '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
316+ '"bytes":14,'
317+ '"content_type":"application\/octet-stream",'
318+ '"last_modified":"2007-03-04 20:32:17"}')
319+ objects.append('{"name":"object8",'
320+ '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
321+ '"bytes":64,'
322+ '"content_type":"application\/octet-stream",'
323+ '"last_modified":"2007-03-04 20:32:17"}')
324+ output = '[\n%s\n]\n' % (',\n'.join(objects[left:right]))
325+ elif args.has_key('format') and args['format'] == 'xml':
326+ objects = []
327+ objects.append('<object><name>object1</name>'
328+ '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
329+ '<bytes>14</bytes>'
330+ '<content_type>application/octet-stream</content_type>'
331+ '<last_modified>2007-03-04 20:32:17</last_modified>'
332+ '</object>\n')
333+ objects.append('<object><name>object2</name>'
334+ '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
335+ '<bytes>64</bytes>'
336+ '<content_type>application/octet-stream</content_type>'
337+ '<last_modified>2007-03-04 20:32:17</last_modified>'
338+ '</object>\n')
339+ objects.append('<object><name>object3</name>'
340+ '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
341+ '<bytes>14</bytes>'
342+ '<content_type>application/octet-stream</content_type>'
343+ '<last_modified>2007-03-04 20:32:17</last_modified>'
344+ '</object>\n')
345+ objects.append('<object><name>object4</name>'
346+ '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
347+ '<bytes>64</bytes>'
348+ '<content_type>application/octet-stream</content_type>'
349+ '<last_modified>2007-03-04 20:32:17</last_modified>'
350+ '</object>\n')
351+ objects.append('<object><name>object5</name>'
352+ '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
353+ '<bytes>14</bytes>'
354+ '<content_type>application/octet-stream</content_type>'
355+ '<last_modified>2007-03-04 20:32:17</last_modified>'
356+ '</object>\n')
357+ objects.append('<object><name>object6</name>'
358+ '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
359+ '<bytes>64</bytes>'
360+ '<content_type>application/octet-stream</content_type>'
361+ '<last_modified>2007-03-04 20:32:17</last_modified>'
362+ '</object>\n')
363+ objects.append('<object><name>object7</name>'
364+ '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
365+ '<bytes>14</bytes>'
366+ '<content_type>application/octet-stream</content_type>'
367+ '<last_modified>2007-03-04 20:32:17</last_modified>'
368+ '</object>\n')
369+ objects.append('<object><name>object8</name>'
370+ '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
371+ '<bytes>64</bytes>'
372+ '<content_type>application/octet-stream</content_type>'
373+ '<last_modified>2007-03-04 20:32:17</last_modified>'
374+ '</object>\n')
375+ objects = objects[left:right]
376+ objects.insert(0, '<?xml version="1.0" encoding="UTF-8"?>\n')
377+ objects.insert(1, '<container name="test_container_1"\n')
378+ objects.append('</container>\n')
379+ output = ''.join(objects)
380+ else:
381+ objects = ['object%s\n' % i for i in range(1,9)]
382+ objects = objects[left:right]
383+ output = ''.join(objects)
384+
385+ # prefix/path don't make much sense given our test data
386+ if args.has_key('prefix') or args.has_key('path'):
387+ pass
388+ return output
389+
390+ def render_GET(self, path, args):
391+ # Special path that returns 404 Not Found
392+ if (len(path) == 4) and (path[3] == 'bogus'):
393+ self.write('HTTP/1.1 404 Not Found\n')
394+ self.write('Content-Type: text/plain\n')
395+ self.write('Content-Length: 0\n')
396+ self.write('Connection: close\n\n')
397+ return
398+
399+ self.write('HTTP/1.1 200 Ok\n')
400+ self.write('Content-Type: text/plain\n')
401+ if len(path) == 2:
402+ content = self._create_GET_account_content(path, args)
403+ elif len(path) == 3:
404+ content = self._create_GET_container_content(path, args)
405+ # Object
406+ elif len(path) == 4:
407+ content = 'I am a teapot, short and stout\n'
408+ self.write('Content-Length: %d\n' % len(content))
409+ self.write('Connection: close\n\n')
410+ self.write(content)
411+
412+ def render_HEAD(self, path, args):
413+ # Account
414+ if len(path) == 2:
415+ self.write('HTTP/1.1 204 No Content\n')
416+ self.write('Content-Type: text/plain\n')
417+ self.write('Connection: close\n')
418+ self.write('X-Account-Container-Count: 3\n')
419+ self.write('X-Account-Bytes-Used: 234\n\n')
420+ else:
421+ self.write('HTTP/1.1 200 Ok\n')
422+ self.write('Content-Type: text/plain\n')
423+ self.write('ETag: d5c7f3babf6c602a8da902fb301a9f27\n')
424+ self.write('Content-Length: 21\n')
425+ self.write('Connection: close\n\n')
426+
427+ def render_POST(self, path, args):
428+ self.write('HTTP/1.1 202 Ok\n')
429+ self.write('Connection: close\n\n')
430+
431+ def render_PUT(self, path, args):
432+ self.write('HTTP/1.1 200 Ok\n')
433+ self.write('Content-Type: text/plain\n')
434+ self.write('Connection: close\n\n')
435+ render_DELETE = render_PUT
436+
437+ def render(self, method, uri):
438+ if '?' in uri:
439+ parts = uri.split('?')
440+ query = parts[1].strip('&').split('&')
441+ args = dict([tuple(i.split('=', 1)) for i in query])
442+ path = parts[0].strip('/').split('/')
443+ else:
444+ args = {}
445+ path = uri.strip('/').split('/')
446+
447+ if hasattr(self, 'render_%s' % method):
448+ getattr(self, 'render_%s' % method)(path, args)
449+ else:
450+ self.write('HTTP/1.1 406 Not Acceptable\n')
451+ self.write('Content-Type: text/plain\n')
452+ self.write('Connection: close\n')
453+
454+ def makefile(self, mode, flags):
455+ self._rbuffer.seek(0)
456+ lines = self.read().splitlines()
457+ (method, uri, version) = lines[0].split()
458+
459+ self.render(method, uri)
460+
461+ self._wbuffer.seek(0)
462+ return self._wbuffer
463+
464+
465+class CustomHTTPConnection(connbase):
466+ def connect(self):
467+ self.sock = TrackerSocket()
468+
469+
470+if __name__ == '__main__':
471+ conn = CustomHTTPConnection('localhost', 8000)
472+ conn.request('HEAD', '/v1/account/container/object')
473+ response = conn.getresponse()
474+ print "Status:", response.status, response.reason
475+ for (key, value) in response.getheaders():
476+ print "%s: %s" % (key, value)
477+ print response.read()
478
479=== modified file 'teller/tests/unit/test_backends.py'
480--- teller/tests/unit/test_backends.py 2010-08-12 01:17:56 +0000
481+++ teller/tests/unit/test_backends.py 2010-09-24 20:38:43 +0000
482@@ -1,6 +1,29 @@
483+# vim: tabstop=4 shiftwidth=4 softtabstop=4
484+
485+# Copyright 2010 OpenStack, LLC
486+# All Rights Reserved.
487+#
488+# Licensed under the Apache License, Version 2.0 (the "License"); you may
489+# not use this file except in compliance with the License. You may obtain
490+# a copy of the License at
491+#
492+# http://www.apache.org/licenses/LICENSE-2.0
493+#
494+# Unless required by applicable law or agreed to in writing, software
495+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
496+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
497+# License for the specific language governing permissions and limitations
498+# under the License.
499+
500+from StringIO import StringIO
501 import unittest
502-from StringIO import StringIO
503-from teller.backends import Backend, get_from_backend
504+
505+from cloudfiles import Connection
506+from cloudfiles.authentication import MockAuthentication as Auth
507+
508+from swiftfakehttp import CustomHTTPConnection
509+from teller.backends import Backend, BackendException, get_from_backend
510+
511
512 class TestBackends(unittest.TestCase):
513 def setUp(self):
514@@ -36,5 +59,41 @@
515 chunks = [c for c in fetcher]
516 self.assertEqual(chunks, ["fa", "ke", "da", "ta"])
517
518+ def test_swift_get_from_backend(self):
519+ class FakeSwift(object):
520+ def __init__(self, *args, **kwargs):
521+ pass
522+ @classmethod
523+ def get_connection(self, *args, **kwargs):
524+ auth = Auth("user", "password")
525+ conn = Connection(auth=auth)
526+ conn.connection = CustomHTTPConnection("localhost", 8000)
527+ return conn
528+
529+ swift_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s', 'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n']
530+
531+ fetcher = get_from_backend("swift://user:password@localhost/container1/file.tar.gz",
532+ conn_class=FakeSwift)
533+
534+ chunks = [c for c in fetcher]
535+
536+ self.assertEqual(chunks, swift_returns)
537+
538+ def test_swift_get_from_backend_with_bad_uri(self):
539+ class FakeSwift(object):
540+ def __init__(self, *args, **kwargs):
541+ pass
542+ @classmethod
543+ def get_connection(self, *args, **kwargs):
544+ auth = Auth("user", "password")
545+ conn = Connection(auth=auth)
546+ conn.connection = CustomHTTPConnection("localhost", 8000)
547+ return conn
548+
549+ swift_url="swift://localhost/container1/file.tar.gz"
550+
551+ self.assertRaises(BackendException, get_from_backend, swift_url)
552+
553+
554 if __name__ == "__main__":
555 unittest.main()

Subscribers

People subscribed via source and target branches