Merge lp:~jaypipes/glance/swift-backend into lp:glance/austin
- swift-backend
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Glance Core security contacts | Pending | ||
Review via email: mp+36597@code.launchpad.net |
Commit message
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_
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() |