Merge lp:~jaypipes/glance/bug771849 into lp:~hudson-openstack/glance/trunk

Proposed by Jay Pipes
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
Reviewer Review Type Date Requested Status
Brian Lamar (community) Approve
Brian Waldon (community) Approve
Review via email: mp+67888@code.launchpad.net

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_store_auth_address = http://localhost/v1

    The following specifies an HTTPS Swift auth URL:

    swift_store_auth_address = https://localhost/v1
    swift_store_autth_address = localhost/v1

    The default scheme for Swift auth URL is HTTPS. For S3,
    the default for the s3_store_service_address is HTTP,
    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)

To post a comment you must log in.
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Seeing some test failures, Jay: http://paste.openstack.org/show/1887/

review: Needs Fixing
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Awesome work, Jay. Great tests, btw.

review: Approve
Revision history for this message
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://paste.openstack.org/show/1920/

Also, is there a blueprint for the refactoring of backends/locations so as to remove the delayed imports?

review: Needs Fixing
Revision history for this message
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://paste.openstack.org/show/1920/

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://blueprints.launchpad.net/glance/+spec/refactor-stores was the one I was going to do that in.
-jay

Revision history for this message
Jay Pipes (jaypipes) wrote :

Alright, tests pass on Ubuntu 10.10, 11.04, and MacOSX. Please re-try. Thanks.
-jay

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Yep, still working for me.

lp:~jaypipes/glance/bug771849 updated
136. By Jay Pipes

PEP8 nit

Revision history for this message
Brian Lamar (blamar) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches