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