Merge lp:~jaypipes/glance/bug771849 into lp:~hudson-openstack/glance/trunk
- bug771849
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Brian Lamar | ||||
Approved revision: | 136 | ||||
Merged at revision: | 157 | ||||
Proposed branch: | lp:~jaypipes/glance/bug771849 | ||||
Merge into: | lp:~hudson-openstack/glance/trunk | ||||
Diff against target: |
1633 lines (+959/-264) 12 files modified
glance/common/exception.py (+18/-0) glance/store/__init__.py (+15/-54) glance/store/filesystem.py (+50/-12) glance/store/http.py (+82/-9) glance/store/location.py (+182/-0) glance/store/s3.py (+119/-29) glance/store/swift.py (+143/-83) tests/stubs.py (+1/-5) tests/unit/test_filesystem_store.py (+12/-13) tests/unit/test_store_location.py (+243/-0) tests/unit/test_stores.py (+0/-1) tests/unit/test_swift_store.py (+94/-58) |
||||
To merge this branch: | bzr merge lp:~jaypipes/glance/bug771849 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brian Lamar (community) | Approve | ||
Brian Waldon (community) | Approve | ||
Review via email: mp+67888@code.launchpad.net |
Commit message
Description of the change
This patch:
Overhaul the way that the store URI works. We can now support
specifying the authurls for Swift and S3 with either an
http://, an https:// or no prefix at all.
The following specifies an HTTP Swift auth URL:
swift_
The following specifies an HTTPS Swift auth URL:
swift_
swift_
The default scheme for Swift auth URL is HTTPS. For S3,
the default for the s3_store_
since boto uses private key signing over HTTP by default
on the s3.amazonaws.com domain. Note that the new S3 stuff
isn't merged yet...
Future refactoring:
* Instead of storing the store_uri in the database in images.location, split the store_uri out into component pieces in an ImageLocation table, reducing duplicate records in the registry database.
* Never return the store_uri in the metadata. This contains security credentials (see LP Bug #755916)
Brian Waldon (bcwaldon) wrote : | # |
Awesome work, Jay. Great tests, btw.
Brian Lamar (blamar) wrote : | # |
Yeah, these imports need some serious un-twisting. Logic seems sound and tests abound, but tests seem to be failing for me:
http://
Also, is there a blueprint for the refactoring of backends/locations so as to remove the delayed imports?
Jay Pipes (jaypipes) wrote : | # |
> Yeah, these imports need some serious un-twisting. Logic seems sound and tests
> abound, but tests seem to be failing for me:
>
> http://
Grrr, fix for MacOSX broke everything else... gah, back to work I go.
> Also, is there a blueprint for the refactoring of backends/locations so as to
> remove the delayed imports?
Yup: https:/
-jay
Jay Pipes (jaypipes) wrote : | # |
Alright, tests pass on Ubuntu 10.10, 11.04, and MacOSX. Please re-try. Thanks.
-jay
Brian Waldon (bcwaldon) wrote : | # |
Yep, still working for me.
Brian Lamar (blamar) : | # |
Preview Diff
1 | === modified file 'glance/common/exception.py' |
2 | --- glance/common/exception.py 2011-06-26 20:43:41 +0000 |
3 | +++ glance/common/exception.py 2011-07-21 13:59:26 +0000 |
4 | @@ -54,6 +54,24 @@ |
5 | pass |
6 | |
7 | |
8 | +class UnknownScheme(Error): |
9 | + |
10 | + msg = "Unknown scheme '%s' found in URI" |
11 | + |
12 | + def __init__(self, scheme): |
13 | + msg = self.__class__.msg % scheme |
14 | + super(UnknownScheme, self).__init__(msg) |
15 | + |
16 | + |
17 | +class BadStoreUri(Error): |
18 | + |
19 | + msg = "The Store URI %s was malformed. Reason: %s" |
20 | + |
21 | + def __init__(self, uri, reason): |
22 | + msg = self.__class__.msg % (uri, reason) |
23 | + super(BadStoreUri, self).__init__(msg) |
24 | + |
25 | + |
26 | class Duplicate(Error): |
27 | pass |
28 | |
29 | |
30 | === modified file 'glance/store/__init__.py' |
31 | --- glance/store/__init__.py 2011-06-28 14:37:31 +0000 |
32 | +++ glance/store/__init__.py 2011-07-21 13:59:26 +0000 |
33 | @@ -20,6 +20,7 @@ |
34 | import urlparse |
35 | |
36 | from glance.common import exception |
37 | +from glance.store import location |
38 | |
39 | |
40 | # TODO(sirp): should this be moved out to common/utils.py ? |
41 | @@ -74,69 +75,29 @@ |
42 | def get_from_backend(uri, **kwargs): |
43 | """Yields chunks of data from backend specified by uri""" |
44 | |
45 | - parsed_uri = urlparse.urlparse(uri) |
46 | - scheme = parsed_uri.scheme |
47 | - |
48 | - backend_class = get_backend_class(scheme) |
49 | - |
50 | - return backend_class.get(parsed_uri, **kwargs) |
51 | + loc = location.get_location_from_uri(uri) |
52 | + backend_class = get_backend_class(loc.store_name) |
53 | + |
54 | + return backend_class.get(loc, **kwargs) |
55 | |
56 | |
57 | def delete_from_backend(uri, **kwargs): |
58 | """Removes chunks of data from backend specified by uri""" |
59 | |
60 | - parsed_uri = urlparse.urlparse(uri) |
61 | - scheme = parsed_uri.scheme |
62 | - |
63 | - backend_class = get_backend_class(scheme) |
64 | + loc = location.get_location_from_uri(uri) |
65 | + backend_class = get_backend_class(loc.store_name) |
66 | |
67 | if hasattr(backend_class, 'delete'): |
68 | - return backend_class.delete(parsed_uri, **kwargs) |
69 | - |
70 | - |
71 | -def get_store_from_location(location): |
72 | + return backend_class.delete(loc, **kwargs) |
73 | + |
74 | + |
75 | +def get_store_from_location(uri): |
76 | """ |
77 | Given a location (assumed to be a URL), attempt to determine |
78 | the store from the location. We use here a simple guess that |
79 | the scheme of the parsed URL is the store... |
80 | |
81 | - :param location: Location to check for the store |
82 | - """ |
83 | - loc_pieces = urlparse.urlparse(location) |
84 | - return loc_pieces.scheme |
85 | - |
86 | - |
87 | -def parse_uri_tokens(parsed_uri, example_url): |
88 | - """ |
89 | - Given a URI and an example_url, attempt to parse the uri to assemble an |
90 | - authurl. This method returns the user, key, authurl, referenced container, |
91 | - and the object we're looking for in that container. |
92 | - |
93 | - Parsing the uri is three phases: |
94 | - 1) urlparse to split the tokens |
95 | - 2) use RE to split on @ and / |
96 | - 3) reassemble authurl |
97 | - """ |
98 | - path = parsed_uri.path.lstrip('//') |
99 | - netloc = parsed_uri.netloc |
100 | - |
101 | - try: |
102 | - try: |
103 | - creds, netloc = netloc.split('@') |
104 | - except ValueError: |
105 | - # Python 2.6.1 compat |
106 | - # see lp659445 and Python issue7904 |
107 | - creds, path = path.split('@') |
108 | - user, key = creds.split(':') |
109 | - path_parts = path.split('/') |
110 | - obj = path_parts.pop() |
111 | - container = path_parts.pop() |
112 | - except (ValueError, IndexError): |
113 | - raise BackendException( |
114 | - "Expected four values to unpack in: %s:%s. " |
115 | - "Should have received something like: %s." |
116 | - % (parsed_uri.scheme, parsed_uri.path, example_url)) |
117 | - |
118 | - authurl = "https://%s" % '/'.join(path_parts) |
119 | - |
120 | - return user, key, authurl, container, obj |
121 | + :param uri: Location to check for the store |
122 | + """ |
123 | + loc = location.get_location_from_uri(uri) |
124 | + return loc.store_name |
125 | |
126 | === modified file 'glance/store/filesystem.py' |
127 | --- glance/store/filesystem.py 2011-06-28 14:37:31 +0000 |
128 | +++ glance/store/filesystem.py 2011-07-21 13:59:26 +0000 |
129 | @@ -26,9 +26,39 @@ |
130 | |
131 | from glance.common import exception |
132 | import glance.store |
133 | +import glance.store.location |
134 | |
135 | logger = logging.getLogger('glance.store.filesystem') |
136 | |
137 | +glance.store.location.add_scheme_map({'file': 'filesystem'}) |
138 | + |
139 | + |
140 | +class StoreLocation(glance.store.location.StoreLocation): |
141 | + |
142 | + """Class describing a Filesystem URI""" |
143 | + |
144 | + def process_specs(self): |
145 | + self.scheme = self.specs.get('scheme', 'file') |
146 | + self.path = self.specs.get('path') |
147 | + |
148 | + def get_uri(self): |
149 | + return "file://%s" % self.path |
150 | + |
151 | + def parse_uri(self, uri): |
152 | + """ |
153 | + Parse URLs. This method fixes an issue where credentials specified |
154 | + in the URL are interpreted differently in Python 2.6.1+ than prior |
155 | + versions of Python. |
156 | + """ |
157 | + pieces = urlparse.urlparse(uri) |
158 | + assert pieces.scheme == 'file' |
159 | + self.scheme = pieces.scheme |
160 | + path = (pieces.netloc + pieces.path).strip() |
161 | + if path == '': |
162 | + reason = "No path specified" |
163 | + raise exception.BadStoreUri(uri, reason) |
164 | + self.path = path |
165 | + |
166 | |
167 | class ChunkedFile(object): |
168 | |
169 | @@ -64,13 +94,19 @@ |
170 | |
171 | class FilesystemBackend(glance.store.Backend): |
172 | @classmethod |
173 | - def get(cls, parsed_uri, expected_size=None, options=None): |
174 | - """ |
175 | - Filesystem-based backend |
176 | - |
177 | - file:///path/to/file.tar.gz.0 |
178 | - """ |
179 | - filepath = parsed_uri.path |
180 | + def get(cls, location, expected_size=None, options=None): |
181 | + """ |
182 | + Takes a `glance.store.location.Location` object that indicates |
183 | + where to find the image file, and returns a generator to use in |
184 | + reading the image file. |
185 | + |
186 | + :location `glance.store.location.Location` object, supplied |
187 | + from glance.store.location.get_location_from_uri() |
188 | + |
189 | + :raises NotFound if file does not exist |
190 | + """ |
191 | + loc = location.store_location |
192 | + filepath = loc.path |
193 | if not os.path.exists(filepath): |
194 | raise exception.NotFound("Image file %s not found" % filepath) |
195 | else: |
196 | @@ -79,17 +115,19 @@ |
197 | return ChunkedFile(filepath) |
198 | |
199 | @classmethod |
200 | - def delete(cls, parsed_uri): |
201 | + def delete(cls, location): |
202 | """ |
203 | - Removes a file from the filesystem backend. |
204 | + Takes a `glance.store.location.Location` object that indicates |
205 | + where to find the image file to delete |
206 | |
207 | - :param parsed_uri: Parsed pieces of URI in form of:: |
208 | - file:///path/to/filename.ext |
209 | + :location `glance.store.location.Location` object, supplied |
210 | + from glance.store.location.get_location_from_uri() |
211 | |
212 | :raises NotFound if file does not exist |
213 | :raises NotAuthorized if cannot delete because of permissions |
214 | """ |
215 | - fn = parsed_uri.path |
216 | + loc = location.store_location |
217 | + fn = loc.path |
218 | if os.path.exists(fn): |
219 | try: |
220 | logger.debug("Deleting image at %s", fn) |
221 | |
222 | === modified file 'glance/store/http.py' |
223 | --- glance/store/http.py 2011-06-28 14:37:31 +0000 |
224 | +++ glance/store/http.py 2011-07-21 13:59:26 +0000 |
225 | @@ -16,31 +16,104 @@ |
226 | # under the License. |
227 | |
228 | import httplib |
229 | +import urlparse |
230 | |
231 | +from glance.common import exception |
232 | import glance.store |
233 | +import glance.store.location |
234 | + |
235 | +glance.store.location.add_scheme_map({'http': 'http', |
236 | + 'https': 'http'}) |
237 | + |
238 | + |
239 | +class StoreLocation(glance.store.location.StoreLocation): |
240 | + |
241 | + """Class describing an HTTP(S) URI""" |
242 | + |
243 | + def process_specs(self): |
244 | + self.scheme = self.specs.get('scheme', 'http') |
245 | + self.netloc = self.specs['netloc'] |
246 | + self.user = self.specs.get('user') |
247 | + self.password = self.specs.get('password') |
248 | + self.path = self.specs.get('path') |
249 | + |
250 | + def _get_credstring(self): |
251 | + if self.user: |
252 | + return '%s:%s@' % (self.user, self.password) |
253 | + return '' |
254 | + |
255 | + def get_uri(self): |
256 | + return "%s://%s%s%s" % ( |
257 | + self.scheme, |
258 | + self._get_credstring(), |
259 | + self.netloc, |
260 | + self.path) |
261 | + |
262 | + def parse_uri(self, uri): |
263 | + """ |
264 | + Parse URLs. This method fixes an issue where credentials specified |
265 | + in the URL are interpreted differently in Python 2.6.1+ than prior |
266 | + versions of Python. |
267 | + """ |
268 | + pieces = urlparse.urlparse(uri) |
269 | + assert pieces.scheme in ('https', 'http') |
270 | + self.scheme = pieces.scheme |
271 | + netloc = pieces.netloc |
272 | + path = pieces.path |
273 | + try: |
274 | + if '@' in netloc: |
275 | + creds, netloc = netloc.split('@') |
276 | + else: |
277 | + creds = None |
278 | + except ValueError: |
279 | + # Python 2.6.1 compat |
280 | + # see lp659445 and Python issue7904 |
281 | + if '@' in path: |
282 | + creds, path = path.split('@') |
283 | + else: |
284 | + creds = None |
285 | + if creds: |
286 | + try: |
287 | + self.user, self.password = creds.split(':') |
288 | + except ValueError: |
289 | + reason = ("Credentials '%s' not well-formatted." |
290 | + % "".join(creds)) |
291 | + raise exception.BadStoreUri(uri, reason) |
292 | + else: |
293 | + self.user = None |
294 | + if netloc == '': |
295 | + reason = "No address specified in HTTP URL" |
296 | + raise exception.BadStoreUri(uri, reason) |
297 | + self.netloc = netloc |
298 | + self.path = path |
299 | |
300 | |
301 | class HTTPBackend(glance.store.Backend): |
302 | """ An implementation of the HTTP Backend Adapter """ |
303 | |
304 | @classmethod |
305 | - def get(cls, parsed_uri, expected_size, options=None, conn_class=None): |
306 | - """ |
307 | - Takes a parsed uri for an HTTP resource, fetches it, and |
308 | - yields the data. |
309 | - """ |
310 | + def get(cls, location, expected_size, options=None, conn_class=None): |
311 | + """ |
312 | + Takes a `glance.store.location.Location` object that indicates |
313 | + where to find the image file, and returns a generator from Swift |
314 | + provided by Swift client's get_object() method. |
315 | + |
316 | + :location `glance.store.location.Location` object, supplied |
317 | + from glance.store.location.get_location_from_uri() |
318 | + """ |
319 | + loc = location.store_location |
320 | if conn_class: |
321 | pass # use the conn_class passed in |
322 | - elif parsed_uri.scheme == "http": |
323 | + elif loc.scheme == "http": |
324 | conn_class = httplib.HTTPConnection |
325 | - elif parsed_uri.scheme == "https": |
326 | + elif loc.scheme == "https": |
327 | conn_class = httplib.HTTPSConnection |
328 | else: |
329 | raise glance.store.BackendException( |
330 | "scheme '%s' not supported for HTTPBackend") |
331 | |
332 | - conn = conn_class(parsed_uri.netloc) |
333 | - conn.request("GET", parsed_uri.path, "", {}) |
334 | + conn = conn_class(loc.netloc) |
335 | + conn.request("GET", loc.path, "", {}) |
336 | |
337 | try: |
338 | return glance.store._file_iter(conn.getresponse(), cls.CHUNKSIZE) |
339 | |
340 | === added file 'glance/store/location.py' |
341 | --- glance/store/location.py 1970-01-01 00:00:00 +0000 |
342 | +++ glance/store/location.py 2011-07-21 13:59:26 +0000 |
343 | @@ -0,0 +1,182 @@ |
344 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
345 | + |
346 | +# Copyright 2011 OpenStack, LLC |
347 | +# All Rights Reserved. |
348 | +# |
349 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
350 | +# not use this file except in compliance with the License. You may obtain |
351 | +# a copy of the License at |
352 | +# |
353 | +# http://www.apache.org/licenses/LICENSE-2.0 |
354 | +# |
355 | +# Unless required by applicable law or agreed to in writing, software |
356 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
357 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
358 | +# License for the specific language governing permissions and limitations |
359 | +# under the License. |
360 | + |
361 | +""" |
362 | +A class that describes the location of an image in Glance. |
363 | + |
364 | +In Glance, an image can either be **stored** in Glance, or it can be |
365 | +**registered** in Glance but actually be stored somewhere else. |
366 | + |
367 | +We needed a class that could support the various ways that Glance |
368 | +describes where exactly an image is stored. |
369 | + |
370 | +An image in Glance has two location properties: the image URI |
371 | +and the image storage URI. |
372 | + |
373 | +The image URI is essentially the permalink identifier for the image. |
374 | +It is displayed in the output of various Glance API calls and, |
375 | +while read-only, is entirely user-facing. It shall **not** contain any |
376 | +security credential information at all. The Glance image URI shall |
377 | +be the host:port of that Glance API server along with /images/<IMAGE_ID>. |
378 | + |
379 | +The Glance storage URI is an internal URI structure that Glance |
380 | +uses to maintain critical information about how to access the images |
381 | +that it stores in its storage backends. It **does contain** security |
382 | +credentials and is **not** user-facing. |
383 | +""" |
384 | + |
385 | +import logging |
386 | +import urlparse |
387 | + |
388 | +from glance.common import exception |
389 | +from glance.common import utils |
390 | + |
391 | +logger = logging.getLogger('glance.store.location') |
392 | + |
393 | +SCHEME_TO_STORE_MAP = {} |
394 | + |
395 | + |
396 | +def get_location_from_uri(uri): |
397 | + """ |
398 | + Given a URI, return a Location object that has had an appropriate |
399 | + store parse the URI. |
400 | + |
401 | + :param uri: A URI that could come from the end-user in the Location |
402 | + attribute/header |
403 | + |
404 | + Example URIs: |
405 | + https://user:pass@example.com:80/images/some-id |
406 | + http://images.oracle.com/123456 |
407 | + swift://user:account:pass@authurl.com/container/obj-id |
408 | + swift+http://user:account:pass@authurl.com/container/obj-id |
409 | + s3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id |
410 | + s3+https://accesskey:secretkey@s3.amazonaws.com/bucket/key-id |
411 | + file:///var/lib/glance/images/1 |
412 | + """ |
413 | + # Add known stores to mapping... this gets past circular import |
414 | + # issues. There's a better way to do this, but that's for another |
415 | + # patch... |
416 | + # TODO(jaypipes) Clear up these imports in refactor-stores blueprint |
417 | + import glance.store.filesystem |
418 | + import glance.store.http |
419 | + import glance.store.s3 |
420 | + import glance.store.swift |
421 | + pieces = urlparse.urlparse(uri) |
422 | + if pieces.scheme not in SCHEME_TO_STORE_MAP.keys(): |
423 | + raise exception.UnknownScheme(pieces.scheme) |
424 | + loc = Location(pieces.scheme, uri=uri) |
425 | + return loc |
426 | + |
427 | + |
428 | +def add_scheme_map(scheme_map): |
429 | + """ |
430 | + Given a mapping of 'scheme' to store_name, adds the mapping to the |
431 | + known list of schemes. |
432 | + |
433 | + Each store should call this method and let Glance know about which |
434 | + schemes to map to a store name. |
435 | + """ |
436 | + SCHEME_TO_STORE_MAP.update(scheme_map) |
437 | + |
438 | + |
439 | +class Location(object): |
440 | + |
441 | + """ |
442 | + Class describing the location of an image that Glance knows about |
443 | + """ |
444 | + |
445 | + def __init__(self, store_name, uri=None, image_id=None, store_specs=None): |
446 | + """ |
447 | + Create a new Location object. |
448 | + |
449 | + :param store_name: The string identifier of the storage backend |
450 | + :param image_id: The identifier of the image in whatever storage |
451 | + backend is used. |
452 | + :param uri: Optional URI to construct location from |
453 | + :param store_specs: Dictionary of information about the location |
454 | + of the image that is dependent on the backend |
455 | + store |
456 | + """ |
457 | + self.store_name = store_name |
458 | + self.image_id = image_id |
459 | + self.store_specs = store_specs or {} |
460 | + self.store_location = self._get_store_location() |
461 | + if uri: |
462 | + self.store_location.parse_uri(uri) |
463 | + |
464 | + def _get_store_location(self): |
465 | + """ |
466 | + We find the store module and then grab an instance of the store's |
467 | + StoreLocation class which handles store-specific location information |
468 | + """ |
469 | + try: |
470 | + cls = utils.import_class('glance.store.%s.StoreLocation' |
471 | + % SCHEME_TO_STORE_MAP[self.store_name]) |
472 | + return cls(self.store_specs) |
473 | + except exception.NotFound: |
474 | + logger.error("Unable to find StoreLocation class in store %s", |
475 | + self.store_name) |
476 | + return None |
477 | + |
478 | + def get_store_uri(self): |
479 | + """ |
480 | + Returns the Glance image URI, which is the host:port of the API server |
481 | + along with /images/<IMAGE_ID> |
482 | + """ |
483 | + return self.store_location.get_uri() |
484 | + |
485 | + def get_uri(self): |
486 | + return None |
487 | + |
488 | + |
489 | +class StoreLocation(object): |
490 | + |
491 | + """ |
492 | + Base class that must be implemented by each store |
493 | + """ |
494 | + |
495 | + def __init__(self, store_specs): |
496 | + self.specs = store_specs |
497 | + if self.specs: |
498 | + self.process_specs() |
499 | + |
500 | + def process_specs(self): |
501 | + """ |
502 | + Subclasses should implement any processing of the self.specs collection |
503 | + such as storing credentials and possibly establishing connections. |
504 | + """ |
505 | + pass |
506 | + |
507 | + def get_uri(self): |
508 | + """ |
509 | + Subclasses should implement a method that returns an internal URI that, |
510 | + when supplied to the StoreLocation instance, can be interpreted by the |
511 | + StoreLocation's parse_uri() method. The URI returned from this method |
512 | + shall never be public and only used internally within Glance, so it is |
513 | + fine to encode credentials in this URI. |
514 | + """ |
515 | + raise NotImplementedError("StoreLocation subclass must implement " |
516 | + "get_uri()") |
517 | + |
518 | + def parse_uri(self, uri): |
519 | + """ |
520 | + Subclasses should implement a method that accepts a string URI and |
521 | + sets appropriate internal fields such that a call to get_uri() will |
522 | + return a proper internal URI |
523 | + """ |
524 | + raise NotImplementedError("StoreLocation subclass must implement " |
525 | + "parse_uri()") |
526 | |
527 | === modified file 'glance/store/s3.py' |
528 | --- glance/store/s3.py 2011-01-27 04:19:13 +0000 |
529 | +++ glance/store/s3.py 2011-07-21 13:59:26 +0000 |
530 | @@ -17,7 +17,101 @@ |
531 | |
532 | """The s3 backend adapter""" |
533 | |
534 | +import urlparse |
535 | + |
536 | +from glance.common import exception |
537 | import glance.store |
538 | +import glance.store.location |
539 | + |
540 | +glance.store.location.add_scheme_map({'s3': 's3', |
541 | + 's3+http': 's3', |
542 | + 's3+https': 's3'}) |
543 | + |
544 | + |
545 | +class StoreLocation(glance.store.location.StoreLocation): |
546 | + |
547 | + """ |
548 | + Class describing an S3 URI. An S3 URI can look like any of |
549 | + the following: |
550 | + |
551 | + s3://accesskey:secretkey@s3service.com/bucket/key-id |
552 | + s3+http://accesskey:secretkey@s3service.com/bucket/key-id |
553 | + s3+https://accesskey:secretkey@s3service.com/bucket/key-id |
554 | + |
555 | + The s3+https:// URIs indicate there is an HTTPS s3service URL |
556 | + """ |
557 | + |
558 | + def process_specs(self): |
559 | + self.scheme = self.specs.get('scheme', 's3') |
560 | + self.accesskey = self.specs.get('accesskey') |
561 | + self.secretkey = self.specs.get('secretkey') |
562 | + self.s3serviceurl = self.specs.get('s3serviceurl') |
563 | + self.bucket = self.specs.get('bucket') |
564 | + self.key = self.specs.get('key') |
565 | + |
566 | + def _get_credstring(self): |
567 | + if self.accesskey: |
568 | + return '%s:%s@' % (self.accesskey, self.secretkey) |
569 | + return '' |
570 | + |
571 | + def get_uri(self): |
572 | + return "%s://%s%s/%s/%s" % ( |
573 | + self.scheme, |
574 | + self._get_credstring(), |
575 | + self.s3serviceurl, |
576 | + self.bucket, |
577 | + self.key) |
578 | + |
579 | + def parse_uri(self, uri): |
580 | + """ |
581 | + Parse URLs. This method fixes an issue where credentials specified |
582 | + in the URL are interpreted differently in Python 2.6.1+ than prior |
583 | + versions of Python. |
584 | + |
585 | + Note that an Amazon AWS secret key can contain the forward slash, |
586 | + which is entirely retarded, and breaks urlparse miserably. |
587 | + This function works around that issue. |
588 | + """ |
589 | + pieces = urlparse.urlparse(uri) |
590 | + assert pieces.scheme in ('s3', 's3+http', 's3+https') |
591 | + self.scheme = pieces.scheme |
592 | + path = pieces.path.strip('/') |
593 | + netloc = pieces.netloc.strip('/') |
594 | + entire_path = (netloc + '/' + path).strip('/') |
595 | + |
596 | + if '@' in uri: |
597 | + creds, path = entire_path.split('@') |
598 | + cred_parts = creds.split(':') |
599 | + |
600 | + try: |
601 | + access_key = cred_parts[0] |
602 | + secret_key = cred_parts[1] |
603 | + # NOTE(jaypipes): Need to encode to UTF-8 here because of a |
604 | + # bug in the HMAC library that boto uses. |
605 | + # See: http://bugs.python.org/issue5285 |
606 | + # See: http://trac.edgewall.org/ticket/8083 |
607 | + access_key = access_key.encode('utf-8') |
608 | + secret_key = secret_key.encode('utf-8') |
609 | + self.accesskey = access_key |
610 | + self.secretkey = secret_key |
611 | + except IndexError: |
612 | + reason = "Badly formed S3 credentials %s" % creds |
613 | + raise exception.BadStoreUri(uri, reason) |
614 | + else: |
615 | + self.accesskey = None |
616 | + path = entire_path |
617 | + try: |
618 | + path_parts = path.split('/') |
619 | + self.key = path_parts.pop() |
620 | + self.bucket = path_parts.pop() |
621 | + if len(path_parts) > 0: |
622 | + self.s3serviceurl = '/'.join(path_parts) |
623 | + else: |
624 | + reason = "Badly formed S3 URI. Missing s3 service URL." |
625 | + raise exception.BadStoreUri(uri, reason) |
626 | + except IndexError: |
627 | + reason = "Badly formed S3 URI" |
628 | + raise exception.BadStoreUri(uri, reason) |
629 | |
630 | |
631 | class S3Backend(glance.store.Backend): |
632 | @@ -26,29 +120,30 @@ |
633 | EXAMPLE_URL = "s3://ACCESS_KEY:SECRET_KEY@s3_url/bucket/file.gz.0" |
634 | |
635 | @classmethod |
636 | - def get(cls, parsed_uri, expected_size, conn_class=None): |
637 | - """ |
638 | - Takes a parsed_uri in the format of: |
639 | - s3://access_key:secret_key@s3.amazonaws.com/bucket/file.gz.0, connects |
640 | - to s3 and downloads the file. Returns the generator resp_body provided |
641 | - by get_object. |
642 | - """ |
643 | + def get(cls, location, expected_size, conn_class=None): |
644 | + """ |
645 | + Takes a `glance.store.location.Location` object that indicates |
646 | + where to find the image file, and returns a generator from S3 |
647 | + provided by S3's key object |
648 | |
649 | + :location `glance.store.location.Location` object, supplied |
650 | + from glance.store.location.get_location_from_uri() |
651 | + """ |
652 | if conn_class: |
653 | pass |
654 | else: |
655 | import boto.s3.connection |
656 | conn_class = boto.s3.connection.S3Connection |
657 | |
658 | - (access_key, secret_key, host, bucket, obj) = \ |
659 | - cls._parse_s3_tokens(parsed_uri) |
660 | + loc = location.store_location |
661 | |
662 | # Close the connection when we're through. |
663 | - with conn_class(access_key, secret_key, host=host) as s3_conn: |
664 | - bucket = cls._get_bucket(s3_conn, bucket) |
665 | + with conn_class(loc.accesskey, loc.secretkey, |
666 | + host=loc.s3serviceurl) as s3_conn: |
667 | + bucket = cls._get_bucket(s3_conn, loc.bucket) |
668 | |
669 | # Close the key when we're through. |
670 | - with cls._get_key(bucket, obj) as key: |
671 | + with cls._get_key(bucket, loc.obj) as key: |
672 | if not key.size == expected_size: |
673 | raise glance.store.BackendException( |
674 | "Expected %s bytes, got %s" % |
675 | @@ -59,28 +154,28 @@ |
676 | yield chunk |
677 | |
678 | @classmethod |
679 | - def delete(cls, parsed_uri, conn_class=None): |
680 | - """ |
681 | - Takes a parsed_uri in the format of: |
682 | - s3://access_key:secret_key@s3.amazonaws.com/bucket/file.gz.0, connects |
683 | - to s3 and deletes the file. Returns whatever boto.s3.key.Key.delete() |
684 | - returns. |
685 | - """ |
686 | + def delete(cls, location, conn_class=None): |
687 | + """ |
688 | + Takes a `glance.store.location.Location` object that indicates |
689 | + where to find the image file to delete |
690 | |
691 | + :location `glance.store.location.Location` object, supplied |
692 | + from glance.store.location.get_location_from_uri() |
693 | + """ |
694 | if conn_class: |
695 | pass |
696 | else: |
697 | conn_class = boto.s3.connection.S3Connection |
698 | |
699 | - (access_key, secret_key, host, bucket, obj) = \ |
700 | - cls._parse_s3_tokens(parsed_uri) |
701 | + loc = location.store_location |
702 | |
703 | # Close the connection when we're through. |
704 | - with conn_class(access_key, secret_key, host=host) as s3_conn: |
705 | - bucket = cls._get_bucket(s3_conn, bucket) |
706 | + with conn_class(loc.accesskey, loc.secretkey, |
707 | + host=loc.s3serviceurl) as s3_conn: |
708 | + bucket = cls._get_bucket(s3_conn, loc.bucket) |
709 | |
710 | # Close the key when we're through. |
711 | - with cls._get_key(bucket, obj) as key: |
712 | + with cls._get_key(bucket, loc.obj) as key: |
713 | return key.delete() |
714 | |
715 | @classmethod |
716 | @@ -102,8 +197,3 @@ |
717 | if not key: |
718 | raise glance.store.BackendException("Could not get key: %s" % key) |
719 | return key |
720 | - |
721 | - @classmethod |
722 | - def _parse_s3_tokens(cls, parsed_uri): |
723 | - """Parse tokens from the parsed_uri""" |
724 | - return glance.store.parse_uri_tokens(parsed_uri, cls.EXAMPLE_URL) |
725 | |
726 | === modified file 'glance/store/swift.py' |
727 | --- glance/store/swift.py 2011-06-28 14:37:31 +0000 |
728 | +++ glance/store/swift.py 2011-07-21 13:59:26 +0000 |
729 | @@ -21,15 +21,114 @@ |
730 | |
731 | import httplib |
732 | import logging |
733 | +import urlparse |
734 | |
735 | from glance.common import config |
736 | from glance.common import exception |
737 | import glance.store |
738 | +import glance.store.location |
739 | |
740 | DEFAULT_SWIFT_CONTAINER = 'glance' |
741 | |
742 | logger = logging.getLogger('glance.store.swift') |
743 | |
744 | +glance.store.location.add_scheme_map({'swift': 'swift', |
745 | + 'swift+http': 'swift', |
746 | + 'swift+https': 'swift'}) |
747 | + |
748 | + |
749 | +class StoreLocation(glance.store.location.StoreLocation): |
750 | + |
751 | + """ |
752 | + Class describing a Swift URI. A Swift URI can look like any of |
753 | + the following: |
754 | + |
755 | + swift://user:pass@authurl.com/container/obj-id |
756 | + swift+http://user:pass@authurl.com/container/obj-id |
757 | + swift+https://user:pass@authurl.com/container/obj-id |
758 | + |
759 | + The swift+https:// URIs indicate there is an HTTPS authentication URL |
760 | + """ |
761 | + |
762 | + def process_specs(self): |
763 | + self.scheme = self.specs.get('scheme', 'swift+https') |
764 | + self.user = self.specs.get('user') |
765 | + self.key = self.specs.get('key') |
766 | + self.authurl = self.specs.get('authurl') |
767 | + self.container = self.specs.get('container') |
768 | + self.obj = self.specs.get('obj') |
769 | + |
770 | + def _get_credstring(self): |
771 | + if self.user: |
772 | + return '%s:%s@' % (self.user, self.key) |
773 | + return '' |
774 | + |
775 | + def get_uri(self): |
776 | + return "%s://%s%s/%s/%s" % ( |
777 | + self.scheme, |
778 | + self._get_credstring(), |
779 | + self.authurl, |
780 | + self.container, |
781 | + self.obj) |
782 | + |
783 | + def parse_uri(self, uri): |
784 | + """ |
785 | + Parse URLs. This method fixes an issue where credentials specified |
786 | + in the URL are interpreted differently in Python 2.6.1+ than prior |
787 | + versions of Python. It also deals with the peculiarity that new-style |
788 | + Swift URIs have where a username can contain a ':', like so: |
789 | + |
790 | + swift://account:user:pass@authurl.com/container/obj |
791 | + """ |
792 | + pieces = urlparse.urlparse(uri) |
793 | + assert pieces.scheme in ('swift', 'swift+http', 'swift+https') |
794 | + self.scheme = pieces.scheme |
795 | + netloc = pieces.netloc |
796 | + path = pieces.path.lstrip('/') |
797 | + if netloc != '': |
798 | + # > Python 2.6.1 |
799 | + if '@' in netloc: |
800 | + creds, netloc = netloc.split('@') |
801 | + else: |
802 | + creds = None |
803 | + else: |
804 | + # Python 2.6.1 compat |
805 | + # see lp659445 and Python issue7904 |
806 | + if '@' in path: |
807 | + creds, path = path.split('@') |
808 | + else: |
809 | + creds = None |
810 | + netloc = path[0:path.find('/')].strip('/') |
811 | + path = path[path.find('/'):].strip('/') |
812 | + if creds: |
813 | + cred_parts = creds.split(':') |
814 | + |
815 | + # User can be account:user, in which case cred_parts[0:2] will be |
816 | + # the account and user. Combine them into a single username of |
817 | + # account:user |
818 | + if len(cred_parts) == 1: |
819 | + reason = "Badly formed credentials '%s' in Swift URI" % creds |
820 | + raise exception.BadStoreUri(uri, reason) |
821 | + elif len(cred_parts) == 3: |
822 | + user = ':'.join(cred_parts[0:2]) |
823 | + else: |
824 | + user = cred_parts[0] |
825 | + key = cred_parts[-1] |
826 | + self.user = user |
827 | + self.key = key |
828 | + else: |
829 | + self.user = None |
830 | + path_parts = path.split('/') |
831 | + try: |
832 | + self.obj = path_parts.pop() |
833 | + self.container = path_parts.pop() |
834 | + self.authurl = netloc |
835 | + if len(path_parts) > 0: |
836 | + self.authurl = netloc + '/' + '/'.join(path_parts).strip('/') |
837 | + except IndexError: |
838 | + reason = "Badly formed Swift URI" |
839 | + raise exception.BadStoreUri(uri, reason) |
840 | + |
841 | |
842 | class SwiftBackend(glance.store.Backend): |
843 | """An implementation of the swift backend adapter.""" |
844 | @@ -39,31 +138,33 @@ |
845 | CHUNKSIZE = 65536 |
846 | |
847 | @classmethod |
848 | - def get(cls, parsed_uri, expected_size=None, options=None): |
849 | + def get(cls, location, expected_size=None, options=None): |
850 | """ |
851 | - Takes a parsed_uri in the format of: |
852 | - swift://user:password@auth_url/container/file.gz.0, connects to the |
853 | - swift instance at auth_url and downloads the file. Returns the |
854 | - generator resp_body provided by get_object. |
855 | + Takes a `glance.store.location.Location` object that indicates |
856 | + where to find the image file, and returns a generator from Swift |
857 | + provided by Swift client's get_object() method. |
858 | + |
859 | + :location `glance.store.location.Location` object, supplied |
860 | + from glance.store.location.get_location_from_uri() |
861 | """ |
862 | from swift.common import client as swift_client |
863 | - (user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri) |
864 | |
865 | # TODO(sirp): snet=False for now, however, if the instance of |
866 | # swift we're talking to is within our same region, we should set |
867 | # snet=True |
868 | + loc = location.store_location |
869 | swift_conn = swift_client.Connection( |
870 | - authurl=authurl, user=user, key=key, snet=False) |
871 | + authurl=loc.authurl, user=loc.user, key=loc.key, snet=False) |
872 | |
873 | try: |
874 | (resp_headers, resp_body) = swift_conn.get_object( |
875 | - container=container, obj=obj, resp_chunk_size=cls.CHUNKSIZE) |
876 | + container=loc.container, obj=loc.obj, |
877 | + resp_chunk_size=cls.CHUNKSIZE) |
878 | except swift_client.ClientException, e: |
879 | if e.http_status == httplib.NOT_FOUND: |
880 | - location = format_swift_location(user, key, authurl, |
881 | - container, obj) |
882 | + uri = location.get_store_uri() |
883 | raise exception.NotFound("Swift could not find image at " |
884 | - "location %(location)s" % locals()) |
885 | + "uri %(uri)s" % locals()) |
886 | |
887 | if expected_size: |
888 | obj_size = int(resp_headers['content-length']) |
889 | @@ -98,6 +199,10 @@ |
890 | <CONTAINER> = ``swift_store_container`` |
891 | <ID> = The id of the image being added |
892 | |
893 | + :note Swift auth URLs by default use HTTPS. To specify an HTTP |
894 | + auth URL, you can specify http://someurl.com for the |
895 | + swift_store_auth_address config option |
896 | + |
897 | :param id: The opaque image identifier |
898 | :param data: The image data to write, as a file-like object |
899 | :param options: Conf mapping |
900 | @@ -119,9 +224,14 @@ |
901 | user = cls._option_get(options, 'swift_store_user') |
902 | key = cls._option_get(options, 'swift_store_key') |
903 | |
904 | - full_auth_address = auth_address |
905 | - if not full_auth_address.startswith('http'): |
906 | - full_auth_address = 'https://' + full_auth_address |
907 | + scheme = 'swift+https' |
908 | + if auth_address.startswith('http://'): |
909 | + scheme = 'swift+http' |
910 | + full_auth_address = auth_address |
911 | + elif auth_address.startswith('https://'): |
912 | + full_auth_address = auth_address |
913 | + else: |
914 | + full_auth_address = 'https://' + auth_address # Defaults https |
915 | |
916 | swift_conn = swift_client.Connection( |
917 | authurl=full_auth_address, user=user, key=key, snet=False) |
918 | @@ -133,8 +243,13 @@ |
919 | create_container_if_missing(container, swift_conn, options) |
920 | |
921 | obj_name = str(id) |
922 | - location = format_swift_location(user, key, auth_address, |
923 | - container, obj_name) |
924 | + location = StoreLocation({'scheme': scheme, |
925 | + 'container': container, |
926 | + 'obj': obj_name, |
927 | + 'authurl': auth_address, |
928 | + 'user': user, |
929 | + 'key': key}) |
930 | + |
931 | try: |
932 | obj_etag = swift_conn.put_object(container, obj_name, data) |
933 | |
934 | @@ -152,7 +267,7 @@ |
935 | # header keys are lowercased by Swift |
936 | if 'content-length' in resp_headers: |
937 | size = int(resp_headers['content-length']) |
938 | - return (location, size, obj_etag) |
939 | + return (location.get_uri(), size, obj_etag) |
940 | except swift_client.ClientException, e: |
941 | if e.http_status == httplib.CONFLICT: |
942 | raise exception.Duplicate("Swift already has an image at " |
943 | @@ -162,89 +277,34 @@ |
944 | raise glance.store.BackendException(msg) |
945 | |
946 | @classmethod |
947 | - def delete(cls, parsed_uri): |
948 | + def delete(cls, location): |
949 | """ |
950 | - Deletes the swift object(s) at the parsed_uri location |
951 | + Takes a `glance.store.location.Location` object that indicates |
952 | + where to find the image file to delete |
953 | + |
954 | + :location `glance.store.location.Location` object, supplied |
955 | + from glance.store.location.get_location_from_uri() |
956 | """ |
957 | from swift.common import client as swift_client |
958 | - (user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri) |
959 | |
960 | # TODO(sirp): snet=False for now, however, if the instance of |
961 | # swift we're talking to is within our same region, we should set |
962 | # snet=True |
963 | + loc = location.store_location |
964 | swift_conn = swift_client.Connection( |
965 | - authurl=authurl, user=user, key=key, snet=False) |
966 | + authurl=loc.authurl, user=loc.user, key=loc.key, snet=False) |
967 | |
968 | try: |
969 | - swift_conn.delete_object(container, obj) |
970 | + swift_conn.delete_object(loc.container, loc.obj) |
971 | except swift_client.ClientException, e: |
972 | if e.http_status == httplib.NOT_FOUND: |
973 | - location = format_swift_location(user, key, authurl, |
974 | - container, obj) |
975 | + uri = location.get_store_uri() |
976 | raise exception.NotFound("Swift could not find image at " |
977 | - "location %(location)s" % locals()) |
978 | + "uri %(uri)s" % locals()) |
979 | else: |
980 | raise |
981 | |
982 | |
983 | -def parse_swift_tokens(parsed_uri): |
984 | - """ |
985 | - Return the various tokens used by Swift. |
986 | - |
987 | - :param parsed_uri: The pieces of a URI returned by urlparse |
988 | - :retval A tuple of (user, key, auth_address, container, obj_name) |
989 | - """ |
990 | - path = parsed_uri.path.lstrip('//') |
991 | - netloc = parsed_uri.netloc |
992 | - |
993 | - try: |
994 | - try: |
995 | - creds, netloc = netloc.split('@') |
996 | - path = '/'.join([netloc, path]) |
997 | - except ValueError: |
998 | - # Python 2.6.1 compat |
999 | - # see lp659445 and Python issue7904 |
1000 | - creds, path = path.split('@') |
1001 | - |
1002 | - cred_parts = creds.split(':') |
1003 | - |
1004 | - # User can be account:user, in which case cred_parts[0:2] will be |
1005 | - # the account and user. Combine them into a single username of |
1006 | - # account:user |
1007 | - if len(cred_parts) == 3: |
1008 | - user = ':'.join(cred_parts[0:2]) |
1009 | - else: |
1010 | - user = cred_parts[0] |
1011 | - key = cred_parts[-1] |
1012 | - path_parts = path.split('/') |
1013 | - obj = path_parts.pop() |
1014 | - container = path_parts.pop() |
1015 | - except (ValueError, IndexError): |
1016 | - raise glance.store.BackendException( |
1017 | - "Expected four values to unpack in: swift:%s. " |
1018 | - "Should have received something like: %s." |
1019 | - % (parsed_uri.path, SwiftBackend.EXAMPLE_URL)) |
1020 | - |
1021 | - authurl = "https://%s" % '/'.join(path_parts) |
1022 | - |
1023 | - return user, key, authurl, container, obj |
1024 | - |
1025 | - |
1026 | -def format_swift_location(user, key, auth_address, container, obj_name): |
1027 | - """ |
1028 | - Returns the swift URI in the format: |
1029 | - swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<OBJNAME> |
1030 | - |
1031 | - :param user: The swift user to authenticate with |
1032 | - :param key: The auth key for the authenticating user |
1033 | - :param auth_address: The base URL for the authentication service |
1034 | - :param container: The name of the container |
1035 | - :param obj_name: The name of the object |
1036 | - """ |
1037 | - return "swift://%(user)s:%(key)s@%(auth_address)s/"\ |
1038 | - "%(container)s/%(obj_name)s" % locals() |
1039 | - |
1040 | - |
1041 | def create_container_if_missing(container, swift_conn, options): |
1042 | """ |
1043 | Creates a missing container in Swift if the |
1044 | |
1045 | === modified file 'tests/stubs.py' |
1046 | --- tests/stubs.py 2011-07-18 18:22:41 +0000 |
1047 | +++ tests/stubs.py 2011-07-21 13:59:26 +0000 |
1048 | @@ -128,13 +128,9 @@ |
1049 | DATA = 'I am a teapot, short and stout\n' |
1050 | |
1051 | @classmethod |
1052 | - def get(cls, parsed_uri, expected_size, conn_class=None): |
1053 | + def get(cls, location, expected_size, conn_class=None): |
1054 | S3Backend = glance.store.s3.S3Backend |
1055 | |
1056 | - # raise BackendException if URI is bad. |
1057 | - (user, key, authurl, container, obj) = \ |
1058 | - S3Backend._parse_s3_tokens(parsed_uri) |
1059 | - |
1060 | def chunk_it(): |
1061 | for i in xrange(0, len(cls.DATA), cls.CHUNK_SIZE): |
1062 | yield cls.DATA[i:i + cls.CHUNK_SIZE] |
1063 | |
1064 | === modified file 'tests/unit/test_filesystem_store.py' |
1065 | --- tests/unit/test_filesystem_store.py 2011-03-08 15:22:44 +0000 |
1066 | +++ tests/unit/test_filesystem_store.py 2011-07-21 13:59:26 +0000 |
1067 | @@ -20,11 +20,11 @@ |
1068 | import StringIO |
1069 | import hashlib |
1070 | import unittest |
1071 | -import urlparse |
1072 | |
1073 | import stubout |
1074 | |
1075 | from glance.common import exception |
1076 | +from glance.store.location import get_location_from_uri |
1077 | from glance.store.filesystem import FilesystemBackend, ChunkedFile |
1078 | from tests import stubs |
1079 | |
1080 | @@ -51,8 +51,8 @@ |
1081 | |
1082 | def test_get(self): |
1083 | """Test a "normal" retrieval of an image in chunks""" |
1084 | - url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2") |
1085 | - image_file = FilesystemBackend.get(url_pieces) |
1086 | + loc = get_location_from_uri("file:///tmp/glance-tests/2") |
1087 | + image_file = FilesystemBackend.get(loc) |
1088 | |
1089 | expected_data = "chunk00000remainder" |
1090 | expected_num_chunks = 2 |
1091 | @@ -70,10 +70,10 @@ |
1092 | Test that trying to retrieve a file that doesn't exist |
1093 | raises an error |
1094 | """ |
1095 | - url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing") |
1096 | + loc = get_location_from_uri("file:///tmp/glance-tests/non-existing") |
1097 | self.assertRaises(exception.NotFound, |
1098 | FilesystemBackend.get, |
1099 | - url_pieces) |
1100 | + loc) |
1101 | |
1102 | def test_add(self): |
1103 | """Test that we can add an image via the filesystem backend""" |
1104 | @@ -93,8 +93,8 @@ |
1105 | self.assertEquals(expected_file_size, size) |
1106 | self.assertEquals(expected_checksum, checksum) |
1107 | |
1108 | - url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42") |
1109 | - new_image_file = FilesystemBackend.get(url_pieces) |
1110 | + loc = get_location_from_uri("file:///tmp/glance-tests/42") |
1111 | + new_image_file = FilesystemBackend.get(loc) |
1112 | new_image_contents = "" |
1113 | new_image_file_size = 0 |
1114 | |
1115 | @@ -122,20 +122,19 @@ |
1116 | """ |
1117 | Test we can delete an existing image in the filesystem store |
1118 | """ |
1119 | - url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2") |
1120 | - |
1121 | - FilesystemBackend.delete(url_pieces) |
1122 | + loc = get_location_from_uri("file:///tmp/glance-tests/2") |
1123 | + FilesystemBackend.delete(loc) |
1124 | |
1125 | self.assertRaises(exception.NotFound, |
1126 | FilesystemBackend.get, |
1127 | - url_pieces) |
1128 | + loc) |
1129 | |
1130 | def test_delete_non_existing(self): |
1131 | """ |
1132 | Test that trying to delete a file that doesn't exist |
1133 | raises an error |
1134 | """ |
1135 | - url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing") |
1136 | + loc = get_location_from_uri("file:///tmp/glance-tests/non-existing") |
1137 | self.assertRaises(exception.NotFound, |
1138 | FilesystemBackend.delete, |
1139 | - url_pieces) |
1140 | + loc) |
1141 | |
1142 | === added file 'tests/unit/test_store_location.py' |
1143 | --- tests/unit/test_store_location.py 1970-01-01 00:00:00 +0000 |
1144 | +++ tests/unit/test_store_location.py 2011-07-21 13:59:26 +0000 |
1145 | @@ -0,0 +1,243 @@ |
1146 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1147 | + |
1148 | +# Copyright 2011 OpenStack, LLC |
1149 | +# All Rights Reserved. |
1150 | +# |
1151 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1152 | +# not use this file except in compliance with the License. You may obtain |
1153 | +# a copy of the License at |
1154 | +# |
1155 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1156 | +# |
1157 | +# Unless required by applicable law or agreed to in writing, software |
1158 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1159 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1160 | +# License for the specific language governing permissions and limitations |
1161 | +# under the License. |
1162 | + |
1163 | +import unittest |
1164 | + |
1165 | +from glance.common import exception |
1166 | +import glance.store.location as location |
1167 | +import glance.store.http |
1168 | +import glance.store.filesystem |
1169 | +import glance.store.swift |
1170 | +import glance.store.s3 |
1171 | + |
1172 | + |
1173 | +class TestStoreLocation(unittest.TestCase): |
1174 | + |
1175 | + def test_get_location_from_uri_back_to_uri(self): |
1176 | + """ |
1177 | + Test that for various URIs, the correct Location |
1178 | + object can be contructed and then the original URI |
1179 | + returned via the get_store_uri() method. |
1180 | + """ |
1181 | + good_store_uris = [ |
1182 | + 'https://user:pass@example.com:80/images/some-id', |
1183 | + 'http://images.oracle.com/123456', |
1184 | + 'swift://account:user:pass@authurl.com/container/obj-id', |
1185 | + 'swift+https://account:user:pass@authurl.com/container/obj-id', |
1186 | + 's3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id', |
1187 | + 's3://accesskey:secretwith/aslash@s3.amazonaws.com/bucket/key-id', |
1188 | + 's3+http://accesskey:secret@s3.amazonaws.com/bucket/key-id', |
1189 | + 's3+https://accesskey:secretkey@s3.amazonaws.com/bucket/key-id', |
1190 | + 'file:///var/lib/glance/images/1'] |
1191 | + |
1192 | + for uri in good_store_uris: |
1193 | + loc = location.get_location_from_uri(uri) |
1194 | + # The get_store_uri() method *should* return an identical URI |
1195 | + # to the URI that is passed to get_location_from_uri() |
1196 | + self.assertEqual(loc.get_store_uri(), uri) |
1197 | + |
1198 | + def test_bad_store_scheme(self): |
1199 | + """ |
1200 | + Test that a URI with a non-existing scheme triggers exception |
1201 | + """ |
1202 | + bad_uri = 'unknown://user:pass@example.com:80/images/some-id' |
1203 | + |
1204 | + self.assertRaises(exception.UnknownScheme, |
1205 | + location.get_location_from_uri, |
1206 | + bad_uri) |
1207 | + |
1208 | + def test_filesystem_store_location(self): |
1209 | + """ |
1210 | + Test the specific StoreLocation for the Filesystem store |
1211 | + """ |
1212 | + uri = 'file:///var/lib/glance/images/1' |
1213 | + loc = glance.store.filesystem.StoreLocation({}) |
1214 | + loc.parse_uri(uri) |
1215 | + |
1216 | + self.assertEqual("file", loc.scheme) |
1217 | + self.assertEqual("/var/lib/glance/images/1", loc.path) |
1218 | + self.assertEqual(uri, loc.get_uri()) |
1219 | + |
1220 | + bad_uri = 'fil://' |
1221 | + self.assertRaises(Exception, loc.parse_uri, bad_uri) |
1222 | + |
1223 | + bad_uri = 'file://' |
1224 | + self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri) |
1225 | + |
1226 | + def test_http_store_location(self): |
1227 | + """ |
1228 | + Test the specific StoreLocation for the HTTP store |
1229 | + """ |
1230 | + uri = 'http://example.com/images/1' |
1231 | + loc = glance.store.http.StoreLocation({}) |
1232 | + loc.parse_uri(uri) |
1233 | + |
1234 | + self.assertEqual("http", loc.scheme) |
1235 | + self.assertEqual("example.com", loc.netloc) |
1236 | + self.assertEqual("/images/1", loc.path) |
1237 | + self.assertEqual(uri, loc.get_uri()) |
1238 | + |
1239 | + uri = 'https://example.com:8080/images/container/1' |
1240 | + loc.parse_uri(uri) |
1241 | + |
1242 | + self.assertEqual("https", loc.scheme) |
1243 | + self.assertEqual("example.com:8080", loc.netloc) |
1244 | + self.assertEqual("/images/container/1", loc.path) |
1245 | + self.assertEqual(uri, loc.get_uri()) |
1246 | + |
1247 | + uri = 'https://user:password@example.com:8080/images/container/1' |
1248 | + loc.parse_uri(uri) |
1249 | + |
1250 | + self.assertEqual("https", loc.scheme) |
1251 | + self.assertEqual("example.com:8080", loc.netloc) |
1252 | + self.assertEqual("user", loc.user) |
1253 | + self.assertEqual("password", loc.password) |
1254 | + self.assertEqual("/images/container/1", loc.path) |
1255 | + self.assertEqual(uri, loc.get_uri()) |
1256 | + |
1257 | + uri = 'https://user:@example.com:8080/images/1' |
1258 | + loc.parse_uri(uri) |
1259 | + |
1260 | + self.assertEqual("https", loc.scheme) |
1261 | + self.assertEqual("example.com:8080", loc.netloc) |
1262 | + self.assertEqual("user", loc.user) |
1263 | + self.assertEqual("", loc.password) |
1264 | + self.assertEqual("/images/1", loc.path) |
1265 | + self.assertEqual(uri, loc.get_uri()) |
1266 | + |
1267 | + bad_uri = 'htt://' |
1268 | + self.assertRaises(Exception, loc.parse_uri, bad_uri) |
1269 | + |
1270 | + bad_uri = 'http://' |
1271 | + self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri) |
1272 | + |
1273 | + bad_uri = 'http://user@example.com:8080/images/1' |
1274 | + self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri) |
1275 | + |
1276 | + def test_swift_store_location(self): |
1277 | + """ |
1278 | + Test the specific StoreLocation for the Swift store |
1279 | + """ |
1280 | + uri = 'swift://example.com/images/1' |
1281 | + loc = glance.store.swift.StoreLocation({}) |
1282 | + loc.parse_uri(uri) |
1283 | + |
1284 | + self.assertEqual("swift", loc.scheme) |
1285 | + self.assertEqual("example.com", loc.authurl) |
1286 | + self.assertEqual("images", loc.container) |
1287 | + self.assertEqual("1", loc.obj) |
1288 | + self.assertEqual(None, loc.user) |
1289 | + self.assertEqual(uri, loc.get_uri()) |
1290 | + |
1291 | + uri = 'swift+https://user:pass@authurl.com/images/1' |
1292 | + loc.parse_uri(uri) |
1293 | + |
1294 | + self.assertEqual("swift+https", loc.scheme) |
1295 | + self.assertEqual("authurl.com", loc.authurl) |
1296 | + self.assertEqual("images", loc.container) |
1297 | + self.assertEqual("1", loc.obj) |
1298 | + self.assertEqual("user", loc.user) |
1299 | + self.assertEqual("pass", loc.key) |
1300 | + self.assertEqual(uri, loc.get_uri()) |
1301 | + |
1302 | + uri = 'swift+https://user:pass@authurl.com/v1/container/12345' |
1303 | + loc.parse_uri(uri) |
1304 | + |
1305 | + self.assertEqual("swift+https", loc.scheme) |
1306 | + self.assertEqual("authurl.com/v1", loc.authurl) |
1307 | + self.assertEqual("container", loc.container) |
1308 | + self.assertEqual("12345", loc.obj) |
1309 | + self.assertEqual("user", loc.user) |
1310 | + self.assertEqual("pass", loc.key) |
1311 | + self.assertEqual(uri, loc.get_uri()) |
1312 | + |
1313 | + uri = 'swift://account:user:pass@authurl.com/v1/container/12345' |
1314 | + loc.parse_uri(uri) |
1315 | + |
1316 | + self.assertEqual("swift", loc.scheme) |
1317 | + self.assertEqual("authurl.com/v1", loc.authurl) |
1318 | + self.assertEqual("container", loc.container) |
1319 | + self.assertEqual("12345", loc.obj) |
1320 | + self.assertEqual("account:user", loc.user) |
1321 | + self.assertEqual("pass", loc.key) |
1322 | + self.assertEqual(uri, loc.get_uri()) |
1323 | + |
1324 | + bad_uri = 'swif://' |
1325 | + self.assertRaises(Exception, loc.parse_uri, bad_uri) |
1326 | + |
1327 | + bad_uri = 'swift://' |
1328 | + self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri) |
1329 | + |
1330 | + bad_uri = 'swift://user@example.com:8080/images/1' |
1331 | + self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri) |
1332 | + |
1333 | + def test_s3_store_location(self): |
1334 | + """ |
1335 | + Test the specific StoreLocation for the S3 store |
1336 | + """ |
1337 | + uri = 's3://example.com/images/1' |
1338 | + loc = glance.store.s3.StoreLocation({}) |
1339 | + loc.parse_uri(uri) |
1340 | + |
1341 | + self.assertEqual("s3", loc.scheme) |
1342 | + self.assertEqual("example.com", loc.s3serviceurl) |
1343 | + self.assertEqual("images", loc.bucket) |
1344 | + self.assertEqual("1", loc.key) |
1345 | + self.assertEqual(None, loc.accesskey) |
1346 | + self.assertEqual(uri, loc.get_uri()) |
1347 | + |
1348 | + uri = 's3+https://accesskey:pass@s3serviceurl.com/images/1' |
1349 | + loc.parse_uri(uri) |
1350 | + |
1351 | + self.assertEqual("s3+https", loc.scheme) |
1352 | + self.assertEqual("s3serviceurl.com", loc.s3serviceurl) |
1353 | + self.assertEqual("images", loc.bucket) |
1354 | + self.assertEqual("1", loc.key) |
1355 | + self.assertEqual("accesskey", loc.accesskey) |
1356 | + self.assertEqual("pass", loc.secretkey) |
1357 | + self.assertEqual(uri, loc.get_uri()) |
1358 | + |
1359 | + uri = 's3+https://accesskey:pass@s3serviceurl.com/v1/bucket/12345' |
1360 | + loc.parse_uri(uri) |
1361 | + |
1362 | + self.assertEqual("s3+https", loc.scheme) |
1363 | + self.assertEqual("s3serviceurl.com/v1", loc.s3serviceurl) |
1364 | + self.assertEqual("bucket", loc.bucket) |
1365 | + self.assertEqual("12345", loc.key) |
1366 | + self.assertEqual("accesskey", loc.accesskey) |
1367 | + self.assertEqual("pass", loc.secretkey) |
1368 | + self.assertEqual(uri, loc.get_uri()) |
1369 | + |
1370 | + uri = 's3://accesskey:pass/withslash@s3serviceurl.com/v1/bucket/12345' |
1371 | + loc.parse_uri(uri) |
1372 | + |
1373 | + self.assertEqual("s3", loc.scheme) |
1374 | + self.assertEqual("s3serviceurl.com/v1", loc.s3serviceurl) |
1375 | + self.assertEqual("bucket", loc.bucket) |
1376 | + self.assertEqual("12345", loc.key) |
1377 | + self.assertEqual("accesskey", loc.accesskey) |
1378 | + self.assertEqual("pass/withslash", loc.secretkey) |
1379 | + self.assertEqual(uri, loc.get_uri()) |
1380 | + |
1381 | + bad_uri = 'swif://' |
1382 | + self.assertRaises(Exception, loc.parse_uri, bad_uri) |
1383 | + |
1384 | + bad_uri = 's3://' |
1385 | + self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri) |
1386 | + |
1387 | + bad_uri = 's3://accesskey@example.com:8080/images/1' |
1388 | + self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri) |
1389 | |
1390 | === modified file 'tests/unit/test_stores.py' |
1391 | --- tests/unit/test_stores.py 2011-03-05 00:02:26 +0000 |
1392 | +++ tests/unit/test_stores.py 2011-07-21 13:59:26 +0000 |
1393 | @@ -19,7 +19,6 @@ |
1394 | |
1395 | import stubout |
1396 | import unittest |
1397 | -import urlparse |
1398 | |
1399 | from glance.store.s3 import S3Backend |
1400 | from glance.store import Backend, BackendException, get_from_backend |
1401 | |
1402 | === modified file 'tests/unit/test_swift_store.py' |
1403 | --- tests/unit/test_swift_store.py 2011-04-13 23:18:26 +0000 |
1404 | +++ tests/unit/test_swift_store.py 2011-07-21 13:59:26 +0000 |
1405 | @@ -29,9 +29,8 @@ |
1406 | |
1407 | from glance.common import exception |
1408 | from glance.store import BackendException |
1409 | -from glance.store.swift import (SwiftBackend, |
1410 | - format_swift_location, |
1411 | - parse_swift_tokens) |
1412 | +from glance.store.swift import SwiftBackend |
1413 | +from glance.store.location import get_location_from_uri |
1414 | |
1415 | FIVE_KB = (5 * 1024) |
1416 | SWIFT_OPTIONS = {'verbose': True, |
1417 | @@ -146,6 +145,18 @@ |
1418 | 'http_connection', fake_http_connection) |
1419 | |
1420 | |
1421 | +def format_swift_location(user, key, authurl, container, obj): |
1422 | + """ |
1423 | + Helper method that returns a Swift store URI given |
1424 | + the component pieces. |
1425 | + """ |
1426 | + scheme = 'swift+https' |
1427 | + if authurl.startswith('http://'): |
1428 | + scheme = 'swift+http' |
1429 | + return "%s://%s:%s@%s/%s/%s" % (scheme, user, key, authurl, |
1430 | + container, obj) |
1431 | + |
1432 | + |
1433 | class TestSwiftBackend(unittest.TestCase): |
1434 | |
1435 | def setUp(self): |
1436 | @@ -157,46 +168,27 @@ |
1437 | """Clear the test environment""" |
1438 | self.stubs.UnsetAll() |
1439 | |
1440 | - def test_parse_swift_tokens(self): |
1441 | - """ |
1442 | - Test that the parse_swift_tokens function returns |
1443 | - user, key, authurl, container, and objname properly |
1444 | - """ |
1445 | - uri = "swift://user:key@localhost/v1.0/container/objname" |
1446 | - url_pieces = urlparse.urlparse(uri) |
1447 | - user, key, authurl, container, objname =\ |
1448 | - parse_swift_tokens(url_pieces) |
1449 | - self.assertEqual("user", user) |
1450 | - self.assertEqual("key", key) |
1451 | - self.assertEqual("https://localhost/v1.0", authurl) |
1452 | - self.assertEqual("container", container) |
1453 | - self.assertEqual("objname", objname) |
1454 | - |
1455 | - uri = "swift://user:key@localhost:9090/v1.0/container/objname" |
1456 | - url_pieces = urlparse.urlparse(uri) |
1457 | - user, key, authurl, container, objname =\ |
1458 | - parse_swift_tokens(url_pieces) |
1459 | - self.assertEqual("user", user) |
1460 | - self.assertEqual("key", key) |
1461 | - self.assertEqual("https://localhost:9090/v1.0", authurl) |
1462 | - self.assertEqual("container", container) |
1463 | - self.assertEqual("objname", objname) |
1464 | - |
1465 | - uri = "swift://account:user:key@localhost:9090/v1.0/container/objname" |
1466 | - url_pieces = urlparse.urlparse(uri) |
1467 | - user, key, authurl, container, objname =\ |
1468 | - parse_swift_tokens(url_pieces) |
1469 | - self.assertEqual("account:user", user) |
1470 | - self.assertEqual("key", key) |
1471 | - self.assertEqual("https://localhost:9090/v1.0", authurl) |
1472 | - self.assertEqual("container", container) |
1473 | - self.assertEqual("objname", objname) |
1474 | - |
1475 | def test_get(self): |
1476 | """Test a "normal" retrieval of an image in chunks""" |
1477 | - url_pieces = urlparse.urlparse( |
1478 | - "swift://user:key@auth_address/glance/2") |
1479 | - image_swift = SwiftBackend.get(url_pieces) |
1480 | + loc = get_location_from_uri("swift://user:key@auth_address/glance/2") |
1481 | + image_swift = SwiftBackend.get(loc) |
1482 | + |
1483 | + expected_data = "*" * FIVE_KB |
1484 | + data = "" |
1485 | + |
1486 | + for chunk in image_swift: |
1487 | + data += chunk |
1488 | + self.assertEqual(expected_data, data) |
1489 | + |
1490 | + def test_get_with_http_auth(self): |
1491 | + """ |
1492 | + Test a retrieval from Swift with an HTTP authurl. This is |
1493 | + specified either via a Location header with swift+http:// or using |
1494 | + http:// in the swift_store_auth_address config value |
1495 | + """ |
1496 | + loc = get_location_from_uri("swift+http://user:key@auth_address/" |
1497 | + "glance/2") |
1498 | + image_swift = SwiftBackend.get(loc) |
1499 | |
1500 | expected_data = "*" * FIVE_KB |
1501 | data = "" |
1502 | @@ -210,11 +202,10 @@ |
1503 | Test retrieval of an image with wrong expected_size param |
1504 | raises an exception |
1505 | """ |
1506 | - url_pieces = urlparse.urlparse( |
1507 | - "swift://user:key@auth_address/glance/2") |
1508 | + loc = get_location_from_uri("swift://user:key@auth_address/glance/2") |
1509 | self.assertRaises(BackendException, |
1510 | SwiftBackend.get, |
1511 | - url_pieces, |
1512 | + loc, |
1513 | {'expected_size': 42}) |
1514 | |
1515 | def test_get_non_existing(self): |
1516 | @@ -222,11 +213,10 @@ |
1517 | Test that trying to retrieve a swift that doesn't exist |
1518 | raises an error |
1519 | """ |
1520 | - url_pieces = urlparse.urlparse( |
1521 | - "swift://user:key@auth_address/noexist") |
1522 | + loc = get_location_from_uri("swift://user:key@authurl/glance/noexist") |
1523 | self.assertRaises(exception.NotFound, |
1524 | SwiftBackend.get, |
1525 | - url_pieces) |
1526 | + loc) |
1527 | |
1528 | def test_add(self): |
1529 | """Test that we can add an image via the swift backend""" |
1530 | @@ -249,14 +239,62 @@ |
1531 | self.assertEquals(expected_swift_size, size) |
1532 | self.assertEquals(expected_checksum, checksum) |
1533 | |
1534 | - url_pieces = urlparse.urlparse(expected_location) |
1535 | - new_image_swift = SwiftBackend.get(url_pieces) |
1536 | + loc = get_location_from_uri(expected_location) |
1537 | + new_image_swift = SwiftBackend.get(loc) |
1538 | new_image_contents = new_image_swift.getvalue() |
1539 | new_image_swift_size = new_image_swift.len |
1540 | |
1541 | self.assertEquals(expected_swift_contents, new_image_contents) |
1542 | self.assertEquals(expected_swift_size, new_image_swift_size) |
1543 | |
1544 | + def test_add_auth_url_variations(self): |
1545 | + """ |
1546 | + Test that we can add an image via the swift backend with |
1547 | + a variety of different auth_address values |
1548 | + """ |
1549 | + variations = ['http://localhost:80', |
1550 | + 'http://localhost', |
1551 | + 'http://localhost/v1', |
1552 | + 'http://localhost/v1/', |
1553 | + 'https://localhost', |
1554 | + 'https://localhost:8080', |
1555 | + 'https://localhost/v1', |
1556 | + 'https://localhost/v1/', |
1557 | + 'localhost', |
1558 | + 'localhost:8080/v1'] |
1559 | + i = 42 |
1560 | + for variation in variations: |
1561 | + expected_image_id = i |
1562 | + expected_swift_size = FIVE_KB |
1563 | + expected_swift_contents = "*" * expected_swift_size |
1564 | + expected_checksum = \ |
1565 | + hashlib.md5(expected_swift_contents).hexdigest() |
1566 | + new_options = SWIFT_OPTIONS.copy() |
1567 | + new_options['swift_store_auth_address'] = variation |
1568 | + expected_location = format_swift_location( |
1569 | + new_options['swift_store_user'], |
1570 | + new_options['swift_store_key'], |
1571 | + new_options['swift_store_auth_address'], |
1572 | + new_options['swift_store_container'], |
1573 | + expected_image_id) |
1574 | + image_swift = StringIO.StringIO(expected_swift_contents) |
1575 | + |
1576 | + location, size, checksum = SwiftBackend.add(i, image_swift, |
1577 | + new_options) |
1578 | + |
1579 | + self.assertEquals(expected_location, location) |
1580 | + self.assertEquals(expected_swift_size, size) |
1581 | + self.assertEquals(expected_checksum, checksum) |
1582 | + |
1583 | + loc = get_location_from_uri(expected_location) |
1584 | + new_image_swift = SwiftBackend.get(loc) |
1585 | + new_image_contents = new_image_swift.getvalue() |
1586 | + new_image_swift_size = new_image_swift.len |
1587 | + |
1588 | + self.assertEquals(expected_swift_contents, new_image_contents) |
1589 | + self.assertEquals(expected_swift_size, new_image_swift_size) |
1590 | + i = i + 1 |
1591 | + |
1592 | def test_add_no_container_no_create(self): |
1593 | """ |
1594 | Tests that adding an image with a non-existing container |
1595 | @@ -306,8 +344,8 @@ |
1596 | self.assertEquals(expected_swift_size, size) |
1597 | self.assertEquals(expected_checksum, checksum) |
1598 | |
1599 | - url_pieces = urlparse.urlparse(expected_location) |
1600 | - new_image_swift = SwiftBackend.get(url_pieces) |
1601 | + loc = get_location_from_uri(expected_location) |
1602 | + new_image_swift = SwiftBackend.get(loc) |
1603 | new_image_contents = new_image_swift.getvalue() |
1604 | new_image_swift_size = new_image_swift.len |
1605 | |
1606 | @@ -356,22 +394,20 @@ |
1607 | """ |
1608 | Test we can delete an existing image in the swift store |
1609 | """ |
1610 | - url_pieces = urlparse.urlparse( |
1611 | - "swift://user:key@auth_address/glance/2") |
1612 | + loc = get_location_from_uri("swift://user:key@authurl/glance/2") |
1613 | |
1614 | - SwiftBackend.delete(url_pieces) |
1615 | + SwiftBackend.delete(loc) |
1616 | |
1617 | self.assertRaises(exception.NotFound, |
1618 | SwiftBackend.get, |
1619 | - url_pieces) |
1620 | + loc) |
1621 | |
1622 | def test_delete_non_existing(self): |
1623 | """ |
1624 | Test that trying to delete a swift that doesn't exist |
1625 | raises an error |
1626 | """ |
1627 | - url_pieces = urlparse.urlparse( |
1628 | - "swift://user:key@auth_address/noexist") |
1629 | + loc = get_location_from_uri("swift://user:key@authurl/glance/noexist") |
1630 | self.assertRaises(exception.NotFound, |
1631 | SwiftBackend.delete, |
1632 | - url_pieces) |
1633 | + loc) |
Seeing some test failures, Jay: http:// paste.openstack .org/show/ 1887/