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
=== modified file 'glance/common/exception.py'
--- glance/common/exception.py 2011-06-26 20:43:41 +0000
+++ glance/common/exception.py 2011-07-21 13:59:26 +0000
@@ -54,6 +54,24 @@
54 pass54 pass
5555
5656
57class UnknownScheme(Error):
58
59 msg = "Unknown scheme '%s' found in URI"
60
61 def __init__(self, scheme):
62 msg = self.__class__.msg % scheme
63 super(UnknownScheme, self).__init__(msg)
64
65
66class BadStoreUri(Error):
67
68 msg = "The Store URI %s was malformed. Reason: %s"
69
70 def __init__(self, uri, reason):
71 msg = self.__class__.msg % (uri, reason)
72 super(BadStoreUri, self).__init__(msg)
73
74
57class Duplicate(Error):75class Duplicate(Error):
58 pass76 pass
5977
6078
=== modified file 'glance/store/__init__.py'
--- glance/store/__init__.py 2011-06-28 14:37:31 +0000
+++ glance/store/__init__.py 2011-07-21 13:59:26 +0000
@@ -20,6 +20,7 @@
20import urlparse20import urlparse
2121
22from glance.common import exception22from glance.common import exception
23from glance.store import location
2324
2425
25# TODO(sirp): should this be moved out to common/utils.py ?26# TODO(sirp): should this be moved out to common/utils.py ?
@@ -74,69 +75,29 @@
74def get_from_backend(uri, **kwargs):75def get_from_backend(uri, **kwargs):
75 """Yields chunks of data from backend specified by uri"""76 """Yields chunks of data from backend specified by uri"""
7677
77 parsed_uri = urlparse.urlparse(uri)78 loc = location.get_location_from_uri(uri)
78 scheme = parsed_uri.scheme79 backend_class = get_backend_class(loc.store_name)
7980
80 backend_class = get_backend_class(scheme)81 return backend_class.get(loc, **kwargs)
81
82 return backend_class.get(parsed_uri, **kwargs)
8382
8483
85def delete_from_backend(uri, **kwargs):84def delete_from_backend(uri, **kwargs):
86 """Removes chunks of data from backend specified by uri"""85 """Removes chunks of data from backend specified by uri"""
8786
88 parsed_uri = urlparse.urlparse(uri)87 loc = location.get_location_from_uri(uri)
89 scheme = parsed_uri.scheme88 backend_class = get_backend_class(loc.store_name)
90
91 backend_class = get_backend_class(scheme)
9289
93 if hasattr(backend_class, 'delete'):90 if hasattr(backend_class, 'delete'):
94 return backend_class.delete(parsed_uri, **kwargs)91 return backend_class.delete(loc, **kwargs)
9592
9693
97def get_store_from_location(location):94def get_store_from_location(uri):
98 """95 """
99 Given a location (assumed to be a URL), attempt to determine96 Given a location (assumed to be a URL), attempt to determine
100 the store from the location. We use here a simple guess that97 the store from the location. We use here a simple guess that
101 the scheme of the parsed URL is the store...98 the scheme of the parsed URL is the store...
10299
103 :param location: Location to check for the store100 :param uri: Location to check for the store
104 """101 """
105 loc_pieces = urlparse.urlparse(location)102 loc = location.get_location_from_uri(uri)
106 return loc_pieces.scheme103 return loc.store_name
107
108
109def parse_uri_tokens(parsed_uri, example_url):
110 """
111 Given a URI and an example_url, attempt to parse the uri to assemble an
112 authurl. This method returns the user, key, authurl, referenced container,
113 and the object we're looking for in that container.
114
115 Parsing the uri is three phases:
116 1) urlparse to split the tokens
117 2) use RE to split on @ and /
118 3) reassemble authurl
119 """
120 path = parsed_uri.path.lstrip('//')
121 netloc = parsed_uri.netloc
122
123 try:
124 try:
125 creds, netloc = netloc.split('@')
126 except ValueError:
127 # Python 2.6.1 compat
128 # see lp659445 and Python issue7904
129 creds, path = path.split('@')
130 user, key = creds.split(':')
131 path_parts = path.split('/')
132 obj = path_parts.pop()
133 container = path_parts.pop()
134 except (ValueError, IndexError):
135 raise BackendException(
136 "Expected four values to unpack in: %s:%s. "
137 "Should have received something like: %s."
138 % (parsed_uri.scheme, parsed_uri.path, example_url))
139
140 authurl = "https://%s" % '/'.join(path_parts)
141
142 return user, key, authurl, container, obj
143104
=== modified file 'glance/store/filesystem.py'
--- glance/store/filesystem.py 2011-06-28 14:37:31 +0000
+++ glance/store/filesystem.py 2011-07-21 13:59:26 +0000
@@ -26,9 +26,39 @@
2626
27from glance.common import exception27from glance.common import exception
28import glance.store28import glance.store
29import glance.store.location
2930
30logger = logging.getLogger('glance.store.filesystem')31logger = logging.getLogger('glance.store.filesystem')
3132
33glance.store.location.add_scheme_map({'file': 'filesystem'})
34
35
36class StoreLocation(glance.store.location.StoreLocation):
37
38 """Class describing a Filesystem URI"""
39
40 def process_specs(self):
41 self.scheme = self.specs.get('scheme', 'file')
42 self.path = self.specs.get('path')
43
44 def get_uri(self):
45 return "file://%s" % self.path
46
47 def parse_uri(self, uri):
48 """
49 Parse URLs. This method fixes an issue where credentials specified
50 in the URL are interpreted differently in Python 2.6.1+ than prior
51 versions of Python.
52 """
53 pieces = urlparse.urlparse(uri)
54 assert pieces.scheme == 'file'
55 self.scheme = pieces.scheme
56 path = (pieces.netloc + pieces.path).strip()
57 if path == '':
58 reason = "No path specified"
59 raise exception.BadStoreUri(uri, reason)
60 self.path = path
61
3262
33class ChunkedFile(object):63class ChunkedFile(object):
3464
@@ -64,13 +94,19 @@
6494
65class FilesystemBackend(glance.store.Backend):95class FilesystemBackend(glance.store.Backend):
66 @classmethod96 @classmethod
67 def get(cls, parsed_uri, expected_size=None, options=None):97 def get(cls, location, expected_size=None, options=None):
68 """98 """
69 Filesystem-based backend99 Takes a `glance.store.location.Location` object that indicates
70100 where to find the image file, and returns a generator to use in
71 file:///path/to/file.tar.gz.0101 reading the image file.
72 """102
73 filepath = parsed_uri.path103 :location `glance.store.location.Location` object, supplied
104 from glance.store.location.get_location_from_uri()
105
106 :raises NotFound if file does not exist
107 """
108 loc = location.store_location
109 filepath = loc.path
74 if not os.path.exists(filepath):110 if not os.path.exists(filepath):
75 raise exception.NotFound("Image file %s not found" % filepath)111 raise exception.NotFound("Image file %s not found" % filepath)
76 else:112 else:
@@ -79,17 +115,19 @@
79 return ChunkedFile(filepath)115 return ChunkedFile(filepath)
80116
81 @classmethod117 @classmethod
82 def delete(cls, parsed_uri):118 def delete(cls, location):
83 """119 """
84 Removes a file from the filesystem backend.120 Takes a `glance.store.location.Location` object that indicates
121 where to find the image file to delete
85122
86 :param parsed_uri: Parsed pieces of URI in form of::123 :location `glance.store.location.Location` object, supplied
87 file:///path/to/filename.ext124 from glance.store.location.get_location_from_uri()
88125
89 :raises NotFound if file does not exist126 :raises NotFound if file does not exist
90 :raises NotAuthorized if cannot delete because of permissions127 :raises NotAuthorized if cannot delete because of permissions
91 """128 """
92 fn = parsed_uri.path129 loc = location.store_location
130 fn = loc.path
93 if os.path.exists(fn):131 if os.path.exists(fn):
94 try:132 try:
95 logger.debug("Deleting image at %s", fn)133 logger.debug("Deleting image at %s", fn)
96134
=== modified file 'glance/store/http.py'
--- glance/store/http.py 2011-06-28 14:37:31 +0000
+++ glance/store/http.py 2011-07-21 13:59:26 +0000
@@ -16,31 +16,104 @@
16# under the License.16# under the License.
1717
18import httplib18import httplib
19import urlparse
1920
21from glance.common import exception
20import glance.store22import glance.store
23import glance.store.location
24
25glance.store.location.add_scheme_map({'http': 'http',
26 'https': 'http'})
27
28
29class StoreLocation(glance.store.location.StoreLocation):
30
31 """Class describing an HTTP(S) URI"""
32
33 def process_specs(self):
34 self.scheme = self.specs.get('scheme', 'http')
35 self.netloc = self.specs['netloc']
36 self.user = self.specs.get('user')
37 self.password = self.specs.get('password')
38 self.path = self.specs.get('path')
39
40 def _get_credstring(self):
41 if self.user:
42 return '%s:%s@' % (self.user, self.password)
43 return ''
44
45 def get_uri(self):
46 return "%s://%s%s%s" % (
47 self.scheme,
48 self._get_credstring(),
49 self.netloc,
50 self.path)
51
52 def parse_uri(self, uri):
53 """
54 Parse URLs. This method fixes an issue where credentials specified
55 in the URL are interpreted differently in Python 2.6.1+ than prior
56 versions of Python.
57 """
58 pieces = urlparse.urlparse(uri)
59 assert pieces.scheme in ('https', 'http')
60 self.scheme = pieces.scheme
61 netloc = pieces.netloc
62 path = pieces.path
63 try:
64 if '@' in netloc:
65 creds, netloc = netloc.split('@')
66 else:
67 creds = None
68 except ValueError:
69 # Python 2.6.1 compat
70 # see lp659445 and Python issue7904
71 if '@' in path:
72 creds, path = path.split('@')
73 else:
74 creds = None
75 if creds:
76 try:
77 self.user, self.password = creds.split(':')
78 except ValueError:
79 reason = ("Credentials '%s' not well-formatted."
80 % "".join(creds))
81 raise exception.BadStoreUri(uri, reason)
82 else:
83 self.user = None
84 if netloc == '':
85 reason = "No address specified in HTTP URL"
86 raise exception.BadStoreUri(uri, reason)
87 self.netloc = netloc
88 self.path = path
2189
2290
23class HTTPBackend(glance.store.Backend):91class HTTPBackend(glance.store.Backend):
24 """ An implementation of the HTTP Backend Adapter """92 """ An implementation of the HTTP Backend Adapter """
2593
26 @classmethod94 @classmethod
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):
28 """96 """
29 Takes a parsed uri for an HTTP resource, fetches it, and97 Takes a `glance.store.location.Location` object that indicates
30 yields the data.98 where to find the image file, and returns a generator from Swift
31 """99 provided by Swift client's get_object() method.
100
101 :location `glance.store.location.Location` object, supplied
102 from glance.store.location.get_location_from_uri()
103 """
104 loc = location.store_location
32 if conn_class:105 if conn_class:
33 pass # use the conn_class passed in106 pass # use the conn_class passed in
34 elif parsed_uri.scheme == "http":107 elif loc.scheme == "http":
35 conn_class = httplib.HTTPConnection108 conn_class = httplib.HTTPConnection
36 elif parsed_uri.scheme == "https":109 elif loc.scheme == "https":
37 conn_class = httplib.HTTPSConnection110 conn_class = httplib.HTTPSConnection
38 else:111 else:
39 raise glance.store.BackendException(112 raise glance.store.BackendException(
40 "scheme '%s' not supported for HTTPBackend")113 "scheme '%s' not supported for HTTPBackend")
41114
42 conn = conn_class(parsed_uri.netloc)115 conn = conn_class(loc.netloc)
43 conn.request("GET", parsed_uri.path, "", {})116 conn.request("GET", loc.path, "", {})
44117
45 try:118 try:
46 return glance.store._file_iter(conn.getresponse(), cls.CHUNKSIZE)119 return glance.store._file_iter(conn.getresponse(), cls.CHUNKSIZE)
47120
=== added file 'glance/store/location.py'
--- glance/store/location.py 1970-01-01 00:00:00 +0000
+++ glance/store/location.py 2011-07-21 13:59:26 +0000
@@ -0,0 +1,182 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack, LLC
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18"""
19A class that describes the location of an image in Glance.
20
21In Glance, an image can either be **stored** in Glance, or it can be
22**registered** in Glance but actually be stored somewhere else.
23
24We needed a class that could support the various ways that Glance
25describes where exactly an image is stored.
26
27An image in Glance has two location properties: the image URI
28and the image storage URI.
29
30The image URI is essentially the permalink identifier for the image.
31It is displayed in the output of various Glance API calls and,
32while read-only, is entirely user-facing. It shall **not** contain any
33security credential information at all. The Glance image URI shall
34be the host:port of that Glance API server along with /images/<IMAGE_ID>.
35
36The Glance storage URI is an internal URI structure that Glance
37uses to maintain critical information about how to access the images
38that it stores in its storage backends. It **does contain** security
39credentials and is **not** user-facing.
40"""
41
42import logging
43import urlparse
44
45from glance.common import exception
46from glance.common import utils
47
48logger = logging.getLogger('glance.store.location')
49
50SCHEME_TO_STORE_MAP = {}
51
52
53def get_location_from_uri(uri):
54 """
55 Given a URI, return a Location object that has had an appropriate
56 store parse the URI.
57
58 :param uri: A URI that could come from the end-user in the Location
59 attribute/header
60
61 Example URIs:
62 https://user:pass@example.com:80/images/some-id
63 http://images.oracle.com/123456
64 swift://user:account:pass@authurl.com/container/obj-id
65 swift+http://user:account:pass@authurl.com/container/obj-id
66 s3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id
67 s3+https://accesskey:secretkey@s3.amazonaws.com/bucket/key-id
68 file:///var/lib/glance/images/1
69 """
70 # Add known stores to mapping... this gets past circular import
71 # issues. There's a better way to do this, but that's for another
72 # patch...
73 # TODO(jaypipes) Clear up these imports in refactor-stores blueprint
74 import glance.store.filesystem
75 import glance.store.http
76 import glance.store.s3
77 import glance.store.swift
78 pieces = urlparse.urlparse(uri)
79 if pieces.scheme not in SCHEME_TO_STORE_MAP.keys():
80 raise exception.UnknownScheme(pieces.scheme)
81 loc = Location(pieces.scheme, uri=uri)
82 return loc
83
84
85def add_scheme_map(scheme_map):
86 """
87 Given a mapping of 'scheme' to store_name, adds the mapping to the
88 known list of schemes.
89
90 Each store should call this method and let Glance know about which
91 schemes to map to a store name.
92 """
93 SCHEME_TO_STORE_MAP.update(scheme_map)
94
95
96class Location(object):
97
98 """
99 Class describing the location of an image that Glance knows about
100 """
101
102 def __init__(self, store_name, uri=None, image_id=None, store_specs=None):
103 """
104 Create a new Location object.
105
106 :param store_name: The string identifier of the storage backend
107 :param image_id: The identifier of the image in whatever storage
108 backend is used.
109 :param uri: Optional URI to construct location from
110 :param store_specs: Dictionary of information about the location
111 of the image that is dependent on the backend
112 store
113 """
114 self.store_name = store_name
115 self.image_id = image_id
116 self.store_specs = store_specs or {}
117 self.store_location = self._get_store_location()
118 if uri:
119 self.store_location.parse_uri(uri)
120
121 def _get_store_location(self):
122 """
123 We find the store module and then grab an instance of the store's
124 StoreLocation class which handles store-specific location information
125 """
126 try:
127 cls = utils.import_class('glance.store.%s.StoreLocation'
128 % SCHEME_TO_STORE_MAP[self.store_name])
129 return cls(self.store_specs)
130 except exception.NotFound:
131 logger.error("Unable to find StoreLocation class in store %s",
132 self.store_name)
133 return None
134
135 def get_store_uri(self):
136 """
137 Returns the Glance image URI, which is the host:port of the API server
138 along with /images/<IMAGE_ID>
139 """
140 return self.store_location.get_uri()
141
142 def get_uri(self):
143 return None
144
145
146class StoreLocation(object):
147
148 """
149 Base class that must be implemented by each store
150 """
151
152 def __init__(self, store_specs):
153 self.specs = store_specs
154 if self.specs:
155 self.process_specs()
156
157 def process_specs(self):
158 """
159 Subclasses should implement any processing of the self.specs collection
160 such as storing credentials and possibly establishing connections.
161 """
162 pass
163
164 def get_uri(self):
165 """
166 Subclasses should implement a method that returns an internal URI that,
167 when supplied to the StoreLocation instance, can be interpreted by the
168 StoreLocation's parse_uri() method. The URI returned from this method
169 shall never be public and only used internally within Glance, so it is
170 fine to encode credentials in this URI.
171 """
172 raise NotImplementedError("StoreLocation subclass must implement "
173 "get_uri()")
174
175 def parse_uri(self, uri):
176 """
177 Subclasses should implement a method that accepts a string URI and
178 sets appropriate internal fields such that a call to get_uri() will
179 return a proper internal URI
180 """
181 raise NotImplementedError("StoreLocation subclass must implement "
182 "parse_uri()")
0183
=== modified file 'glance/store/s3.py'
--- glance/store/s3.py 2011-01-27 04:19:13 +0000
+++ glance/store/s3.py 2011-07-21 13:59:26 +0000
@@ -17,7 +17,101 @@
1717
18"""The s3 backend adapter"""18"""The s3 backend adapter"""
1919
20import urlparse
21
22from glance.common import exception
20import glance.store23import glance.store
24import glance.store.location
25
26glance.store.location.add_scheme_map({'s3': 's3',
27 's3+http': 's3',
28 's3+https': 's3'})
29
30
31class StoreLocation(glance.store.location.StoreLocation):
32
33 """
34 Class describing an S3 URI. An S3 URI can look like any of
35 the following:
36
37 s3://accesskey:secretkey@s3service.com/bucket/key-id
38 s3+http://accesskey:secretkey@s3service.com/bucket/key-id
39 s3+https://accesskey:secretkey@s3service.com/bucket/key-id
40
41 The s3+https:// URIs indicate there is an HTTPS s3service URL
42 """
43
44 def process_specs(self):
45 self.scheme = self.specs.get('scheme', 's3')
46 self.accesskey = self.specs.get('accesskey')
47 self.secretkey = self.specs.get('secretkey')
48 self.s3serviceurl = self.specs.get('s3serviceurl')
49 self.bucket = self.specs.get('bucket')
50 self.key = self.specs.get('key')
51
52 def _get_credstring(self):
53 if self.accesskey:
54 return '%s:%s@' % (self.accesskey, self.secretkey)
55 return ''
56
57 def get_uri(self):
58 return "%s://%s%s/%s/%s" % (
59 self.scheme,
60 self._get_credstring(),
61 self.s3serviceurl,
62 self.bucket,
63 self.key)
64
65 def parse_uri(self, uri):
66 """
67 Parse URLs. This method fixes an issue where credentials specified
68 in the URL are interpreted differently in Python 2.6.1+ than prior
69 versions of Python.
70
71 Note that an Amazon AWS secret key can contain the forward slash,
72 which is entirely retarded, and breaks urlparse miserably.
73 This function works around that issue.
74 """
75 pieces = urlparse.urlparse(uri)
76 assert pieces.scheme in ('s3', 's3+http', 's3+https')
77 self.scheme = pieces.scheme
78 path = pieces.path.strip('/')
79 netloc = pieces.netloc.strip('/')
80 entire_path = (netloc + '/' + path).strip('/')
81
82 if '@' in uri:
83 creds, path = entire_path.split('@')
84 cred_parts = creds.split(':')
85
86 try:
87 access_key = cred_parts[0]
88 secret_key = cred_parts[1]
89 # NOTE(jaypipes): Need to encode to UTF-8 here because of a
90 # bug in the HMAC library that boto uses.
91 # See: http://bugs.python.org/issue5285
92 # See: http://trac.edgewall.org/ticket/8083
93 access_key = access_key.encode('utf-8')
94 secret_key = secret_key.encode('utf-8')
95 self.accesskey = access_key
96 self.secretkey = secret_key
97 except IndexError:
98 reason = "Badly formed S3 credentials %s" % creds
99 raise exception.BadStoreUri(uri, reason)
100 else:
101 self.accesskey = None
102 path = entire_path
103 try:
104 path_parts = path.split('/')
105 self.key = path_parts.pop()
106 self.bucket = path_parts.pop()
107 if len(path_parts) > 0:
108 self.s3serviceurl = '/'.join(path_parts)
109 else:
110 reason = "Badly formed S3 URI. Missing s3 service URL."
111 raise exception.BadStoreUri(uri, reason)
112 except IndexError:
113 reason = "Badly formed S3 URI"
114 raise exception.BadStoreUri(uri, reason)
21115
22116
23class S3Backend(glance.store.Backend):117class S3Backend(glance.store.Backend):
@@ -26,29 +120,30 @@
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"
27121
28 @classmethod122 @classmethod
29 def get(cls, parsed_uri, expected_size, conn_class=None):123 def get(cls, location, expected_size, conn_class=None):
30 """124 """
31 Takes a parsed_uri in the format of:125 Takes a `glance.store.location.Location` object that indicates
32 s3://access_key:secret_key@s3.amazonaws.com/bucket/file.gz.0, connects126 where to find the image file, and returns a generator from S3
33 to s3 and downloads the file. Returns the generator resp_body provided127 provided by S3's key object
34 by get_object.
35 """
36128
129 :location `glance.store.location.Location` object, supplied
130 from glance.store.location.get_location_from_uri()
131 """
37 if conn_class:132 if conn_class:
38 pass133 pass
39 else:134 else:
40 import boto.s3.connection135 import boto.s3.connection
41 conn_class = boto.s3.connection.S3Connection136 conn_class = boto.s3.connection.S3Connection
42137
43 (access_key, secret_key, host, bucket, obj) = \138 loc = location.store_location
44 cls._parse_s3_tokens(parsed_uri)
45139
46 # Close the connection when we're through.140 # Close the connection when we're through.
47 with conn_class(access_key, secret_key, host=host) as s3_conn:141 with conn_class(loc.accesskey, loc.secretkey,
48 bucket = cls._get_bucket(s3_conn, bucket)142 host=loc.s3serviceurl) as s3_conn:
143 bucket = cls._get_bucket(s3_conn, loc.bucket)
49144
50 # Close the key when we're through.145 # Close the key when we're through.
51 with cls._get_key(bucket, obj) as key:146 with cls._get_key(bucket, loc.obj) as key:
52 if not key.size == expected_size:147 if not key.size == expected_size:
53 raise glance.store.BackendException(148 raise glance.store.BackendException(
54 "Expected %s bytes, got %s" %149 "Expected %s bytes, got %s" %
@@ -59,28 +154,28 @@
59 yield chunk154 yield chunk
60155
61 @classmethod156 @classmethod
62 def delete(cls, parsed_uri, conn_class=None):157 def delete(cls, location, conn_class=None):
63 """158 """
64 Takes a parsed_uri in the format of:159 Takes a `glance.store.location.Location` object that indicates
65 s3://access_key:secret_key@s3.amazonaws.com/bucket/file.gz.0, connects160 where to find the image file to delete
66 to s3 and deletes the file. Returns whatever boto.s3.key.Key.delete()
67 returns.
68 """
69161
162 :location `glance.store.location.Location` object, supplied
163 from glance.store.location.get_location_from_uri()
164 """
70 if conn_class:165 if conn_class:
71 pass166 pass
72 else:167 else:
73 conn_class = boto.s3.connection.S3Connection168 conn_class = boto.s3.connection.S3Connection
74169
75 (access_key, secret_key, host, bucket, obj) = \170 loc = location.store_location
76 cls._parse_s3_tokens(parsed_uri)
77171
78 # Close the connection when we're through.172 # Close the connection when we're through.
79 with conn_class(access_key, secret_key, host=host) as s3_conn:173 with conn_class(loc.accesskey, loc.secretkey,
80 bucket = cls._get_bucket(s3_conn, bucket)174 host=loc.s3serviceurl) as s3_conn:
175 bucket = cls._get_bucket(s3_conn, loc.bucket)
81176
82 # Close the key when we're through.177 # Close the key when we're through.
83 with cls._get_key(bucket, obj) as key:178 with cls._get_key(bucket, loc.obj) as key:
84 return key.delete()179 return key.delete()
85180
86 @classmethod181 @classmethod
@@ -102,8 +197,3 @@
102 if not key:197 if not key:
103 raise glance.store.BackendException("Could not get key: %s" % key)198 raise glance.store.BackendException("Could not get key: %s" % key)
104 return key199 return key
105
106 @classmethod
107 def _parse_s3_tokens(cls, parsed_uri):
108 """Parse tokens from the parsed_uri"""
109 return glance.store.parse_uri_tokens(parsed_uri, cls.EXAMPLE_URL)
110200
=== modified file 'glance/store/swift.py'
--- glance/store/swift.py 2011-06-28 14:37:31 +0000
+++ glance/store/swift.py 2011-07-21 13:59:26 +0000
@@ -21,15 +21,114 @@
2121
22import httplib22import httplib
23import logging23import logging
24import urlparse
2425
25from glance.common import config26from glance.common import config
26from glance.common import exception27from glance.common import exception
27import glance.store28import glance.store
29import glance.store.location
2830
29DEFAULT_SWIFT_CONTAINER = 'glance'31DEFAULT_SWIFT_CONTAINER = 'glance'
3032
31logger = logging.getLogger('glance.store.swift')33logger = logging.getLogger('glance.store.swift')
3234
35glance.store.location.add_scheme_map({'swift': 'swift',
36 'swift+http': 'swift',
37 'swift+https': 'swift'})
38
39
40class StoreLocation(glance.store.location.StoreLocation):
41
42 """
43 Class describing a Swift URI. A Swift URI can look like any of
44 the following:
45
46 swift://user:pass@authurl.com/container/obj-id
47 swift+http://user:pass@authurl.com/container/obj-id
48 swift+https://user:pass@authurl.com/container/obj-id
49
50 The swift+https:// URIs indicate there is an HTTPS authentication URL
51 """
52
53 def process_specs(self):
54 self.scheme = self.specs.get('scheme', 'swift+https')
55 self.user = self.specs.get('user')
56 self.key = self.specs.get('key')
57 self.authurl = self.specs.get('authurl')
58 self.container = self.specs.get('container')
59 self.obj = self.specs.get('obj')
60
61 def _get_credstring(self):
62 if self.user:
63 return '%s:%s@' % (self.user, self.key)
64 return ''
65
66 def get_uri(self):
67 return "%s://%s%s/%s/%s" % (
68 self.scheme,
69 self._get_credstring(),
70 self.authurl,
71 self.container,
72 self.obj)
73
74 def parse_uri(self, uri):
75 """
76 Parse URLs. This method fixes an issue where credentials specified
77 in the URL are interpreted differently in Python 2.6.1+ than prior
78 versions of Python. It also deals with the peculiarity that new-style
79 Swift URIs have where a username can contain a ':', like so:
80
81 swift://account:user:pass@authurl.com/container/obj
82 """
83 pieces = urlparse.urlparse(uri)
84 assert pieces.scheme in ('swift', 'swift+http', 'swift+https')
85 self.scheme = pieces.scheme
86 netloc = pieces.netloc
87 path = pieces.path.lstrip('/')
88 if netloc != '':
89 # > Python 2.6.1
90 if '@' in netloc:
91 creds, netloc = netloc.split('@')
92 else:
93 creds = None
94 else:
95 # Python 2.6.1 compat
96 # see lp659445 and Python issue7904
97 if '@' in path:
98 creds, path = path.split('@')
99 else:
100 creds = None
101 netloc = path[0:path.find('/')].strip('/')
102 path = path[path.find('/'):].strip('/')
103 if creds:
104 cred_parts = creds.split(':')
105
106 # User can be account:user, in which case cred_parts[0:2] will be
107 # the account and user. Combine them into a single username of
108 # account:user
109 if len(cred_parts) == 1:
110 reason = "Badly formed credentials '%s' in Swift URI" % creds
111 raise exception.BadStoreUri(uri, reason)
112 elif len(cred_parts) == 3:
113 user = ':'.join(cred_parts[0:2])
114 else:
115 user = cred_parts[0]
116 key = cred_parts[-1]
117 self.user = user
118 self.key = key
119 else:
120 self.user = None
121 path_parts = path.split('/')
122 try:
123 self.obj = path_parts.pop()
124 self.container = path_parts.pop()
125 self.authurl = netloc
126 if len(path_parts) > 0:
127 self.authurl = netloc + '/' + '/'.join(path_parts).strip('/')
128 except IndexError:
129 reason = "Badly formed Swift URI"
130 raise exception.BadStoreUri(uri, reason)
131
33132
34class SwiftBackend(glance.store.Backend):133class SwiftBackend(glance.store.Backend):
35 """An implementation of the swift backend adapter."""134 """An implementation of the swift backend adapter."""
@@ -39,31 +138,33 @@
39 CHUNKSIZE = 65536138 CHUNKSIZE = 65536
40139
41 @classmethod140 @classmethod
42 def get(cls, parsed_uri, expected_size=None, options=None):141 def get(cls, location, expected_size=None, options=None):
43 """142 """
44 Takes a parsed_uri in the format of:143 Takes a `glance.store.location.Location` object that indicates
45 swift://user:password@auth_url/container/file.gz.0, connects to the144 where to find the image file, and returns a generator from Swift
46 swift instance at auth_url and downloads the file. Returns the145 provided by Swift client's get_object() method.
47 generator resp_body provided by get_object.146
147 :location `glance.store.location.Location` object, supplied
148 from glance.store.location.get_location_from_uri()
48 """149 """
49 from swift.common import client as swift_client150 from swift.common import client as swift_client
50 (user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri)
51151
52 # TODO(sirp): snet=False for now, however, if the instance of152 # TODO(sirp): snet=False for now, however, if the instance of
53 # swift we're talking to is within our same region, we should set153 # swift we're talking to is within our same region, we should set
54 # snet=True154 # snet=True
155 loc = location.store_location
55 swift_conn = swift_client.Connection(156 swift_conn = swift_client.Connection(
56 authurl=authurl, user=user, key=key, snet=False)157 authurl=loc.authurl, user=loc.user, key=loc.key, snet=False)
57158
58 try:159 try:
59 (resp_headers, resp_body) = swift_conn.get_object(160 (resp_headers, resp_body) = swift_conn.get_object(
60 container=container, obj=obj, resp_chunk_size=cls.CHUNKSIZE)161 container=loc.container, obj=loc.obj,
162 resp_chunk_size=cls.CHUNKSIZE)
61 except swift_client.ClientException, e:163 except swift_client.ClientException, e:
62 if e.http_status == httplib.NOT_FOUND:164 if e.http_status == httplib.NOT_FOUND:
63 location = format_swift_location(user, key, authurl,165 uri = location.get_store_uri()
64 container, obj)
65 raise exception.NotFound("Swift could not find image at "166 raise exception.NotFound("Swift could not find image at "
66 "location %(location)s" % locals())167 "uri %(uri)s" % locals())
67168
68 if expected_size:169 if expected_size:
69 obj_size = int(resp_headers['content-length'])170 obj_size = int(resp_headers['content-length'])
@@ -98,6 +199,10 @@
98 <CONTAINER> = ``swift_store_container``199 <CONTAINER> = ``swift_store_container``
99 <ID> = The id of the image being added200 <ID> = The id of the image being added
100201
202 :note Swift auth URLs by default use HTTPS. To specify an HTTP
203 auth URL, you can specify http://someurl.com for the
204 swift_store_auth_address config option
205
101 :param id: The opaque image identifier206 :param id: The opaque image identifier
102 :param data: The image data to write, as a file-like object207 :param data: The image data to write, as a file-like object
103 :param options: Conf mapping208 :param options: Conf mapping
@@ -119,9 +224,14 @@
119 user = cls._option_get(options, 'swift_store_user')224 user = cls._option_get(options, 'swift_store_user')
120 key = cls._option_get(options, 'swift_store_key')225 key = cls._option_get(options, 'swift_store_key')
121226
122 full_auth_address = auth_address227 scheme = 'swift+https'
123 if not full_auth_address.startswith('http'):228 if auth_address.startswith('http://'):
124 full_auth_address = 'https://' + full_auth_address229 scheme = 'swift+http'
230 full_auth_address = auth_address
231 elif auth_address.startswith('https://'):
232 full_auth_address = auth_address
233 else:
234 full_auth_address = 'https://' + auth_address # Defaults https
125235
126 swift_conn = swift_client.Connection(236 swift_conn = swift_client.Connection(
127 authurl=full_auth_address, user=user, key=key, snet=False)237 authurl=full_auth_address, user=user, key=key, snet=False)
@@ -133,8 +243,13 @@
133 create_container_if_missing(container, swift_conn, options)243 create_container_if_missing(container, swift_conn, options)
134244
135 obj_name = str(id)245 obj_name = str(id)
136 location = format_swift_location(user, key, auth_address,246 location = StoreLocation({'scheme': scheme,
137 container, obj_name)247 'container': container,
248 'obj': obj_name,
249 'authurl': auth_address,
250 'user': user,
251 'key': key})
252
138 try:253 try:
139 obj_etag = swift_conn.put_object(container, obj_name, data)254 obj_etag = swift_conn.put_object(container, obj_name, data)
140255
@@ -152,7 +267,7 @@
152 # header keys are lowercased by Swift267 # header keys are lowercased by Swift
153 if 'content-length' in resp_headers:268 if 'content-length' in resp_headers:
154 size = int(resp_headers['content-length'])269 size = int(resp_headers['content-length'])
155 return (location, size, obj_etag)270 return (location.get_uri(), size, obj_etag)
156 except swift_client.ClientException, e:271 except swift_client.ClientException, e:
157 if e.http_status == httplib.CONFLICT:272 if e.http_status == httplib.CONFLICT:
158 raise exception.Duplicate("Swift already has an image at "273 raise exception.Duplicate("Swift already has an image at "
@@ -162,89 +277,34 @@
162 raise glance.store.BackendException(msg)277 raise glance.store.BackendException(msg)
163278
164 @classmethod279 @classmethod
165 def delete(cls, parsed_uri):280 def delete(cls, location):
166 """281 """
167 Deletes the swift object(s) at the parsed_uri location282 Takes a `glance.store.location.Location` object that indicates
283 where to find the image file to delete
284
285 :location `glance.store.location.Location` object, supplied
286 from glance.store.location.get_location_from_uri()
168 """287 """
169 from swift.common import client as swift_client288 from swift.common import client as swift_client
170 (user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri)
171289
172 # TODO(sirp): snet=False for now, however, if the instance of290 # TODO(sirp): snet=False for now, however, if the instance of
173 # swift we're talking to is within our same region, we should set291 # swift we're talking to is within our same region, we should set
174 # snet=True292 # snet=True
293 loc = location.store_location
175 swift_conn = swift_client.Connection(294 swift_conn = swift_client.Connection(
176 authurl=authurl, user=user, key=key, snet=False)295 authurl=loc.authurl, user=loc.user, key=loc.key, snet=False)
177296
178 try:297 try:
179 swift_conn.delete_object(container, obj)298 swift_conn.delete_object(loc.container, loc.obj)
180 except swift_client.ClientException, e:299 except swift_client.ClientException, e:
181 if e.http_status == httplib.NOT_FOUND:300 if e.http_status == httplib.NOT_FOUND:
182 location = format_swift_location(user, key, authurl,301 uri = location.get_store_uri()
183 container, obj)
184 raise exception.NotFound("Swift could not find image at "302 raise exception.NotFound("Swift could not find image at "
185 "location %(location)s" % locals())303 "uri %(uri)s" % locals())
186 else:304 else:
187 raise305 raise
188306
189307
190def parse_swift_tokens(parsed_uri):
191 """
192 Return the various tokens used by Swift.
193
194 :param parsed_uri: The pieces of a URI returned by urlparse
195 :retval A tuple of (user, key, auth_address, container, obj_name)
196 """
197 path = parsed_uri.path.lstrip('//')
198 netloc = parsed_uri.netloc
199
200 try:
201 try:
202 creds, netloc = netloc.split('@')
203 path = '/'.join([netloc, path])
204 except ValueError:
205 # Python 2.6.1 compat
206 # see lp659445 and Python issue7904
207 creds, path = path.split('@')
208
209 cred_parts = creds.split(':')
210
211 # User can be account:user, in which case cred_parts[0:2] will be
212 # the account and user. Combine them into a single username of
213 # account:user
214 if len(cred_parts) == 3:
215 user = ':'.join(cred_parts[0:2])
216 else:
217 user = cred_parts[0]
218 key = cred_parts[-1]
219 path_parts = path.split('/')
220 obj = path_parts.pop()
221 container = path_parts.pop()
222 except (ValueError, IndexError):
223 raise glance.store.BackendException(
224 "Expected four values to unpack in: swift:%s. "
225 "Should have received something like: %s."
226 % (parsed_uri.path, SwiftBackend.EXAMPLE_URL))
227
228 authurl = "https://%s" % '/'.join(path_parts)
229
230 return user, key, authurl, container, obj
231
232
233def format_swift_location(user, key, auth_address, container, obj_name):
234 """
235 Returns the swift URI in the format:
236 swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<OBJNAME>
237
238 :param user: The swift user to authenticate with
239 :param key: The auth key for the authenticating user
240 :param auth_address: The base URL for the authentication service
241 :param container: The name of the container
242 :param obj_name: The name of the object
243 """
244 return "swift://%(user)s:%(key)s@%(auth_address)s/"\
245 "%(container)s/%(obj_name)s" % locals()
246
247
248def create_container_if_missing(container, swift_conn, options):308def create_container_if_missing(container, swift_conn, options):
249 """309 """
250 Creates a missing container in Swift if the310 Creates a missing container in Swift if the
251311
=== modified file 'tests/stubs.py'
--- tests/stubs.py 2011-07-18 18:22:41 +0000
+++ tests/stubs.py 2011-07-21 13:59:26 +0000
@@ -128,13 +128,9 @@
128 DATA = 'I am a teapot, short and stout\n'128 DATA = 'I am a teapot, short and stout\n'
129129
130 @classmethod130 @classmethod
131 def get(cls, parsed_uri, expected_size, conn_class=None):131 def get(cls, location, expected_size, conn_class=None):
132 S3Backend = glance.store.s3.S3Backend132 S3Backend = glance.store.s3.S3Backend
133133
134 # raise BackendException if URI is bad.
135 (user, key, authurl, container, obj) = \
136 S3Backend._parse_s3_tokens(parsed_uri)
137
138 def chunk_it():134 def chunk_it():
139 for i in xrange(0, len(cls.DATA), cls.CHUNK_SIZE):135 for i in xrange(0, len(cls.DATA), cls.CHUNK_SIZE):
140 yield cls.DATA[i:i + cls.CHUNK_SIZE]136 yield cls.DATA[i:i + cls.CHUNK_SIZE]
141137
=== modified file 'tests/unit/test_filesystem_store.py'
--- tests/unit/test_filesystem_store.py 2011-03-08 15:22:44 +0000
+++ tests/unit/test_filesystem_store.py 2011-07-21 13:59:26 +0000
@@ -20,11 +20,11 @@
20import StringIO20import StringIO
21import hashlib21import hashlib
22import unittest22import unittest
23import urlparse
2423
25import stubout24import stubout
2625
27from glance.common import exception26from glance.common import exception
27from glance.store.location import get_location_from_uri
28from glance.store.filesystem import FilesystemBackend, ChunkedFile28from glance.store.filesystem import FilesystemBackend, ChunkedFile
29from tests import stubs29from tests import stubs
3030
@@ -51,8 +51,8 @@
5151
52 def test_get(self):52 def test_get(self):
53 """Test a "normal" retrieval of an image in chunks"""53 """Test a "normal" retrieval of an image in chunks"""
54 url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2")54 loc = get_location_from_uri("file:///tmp/glance-tests/2")
55 image_file = FilesystemBackend.get(url_pieces)55 image_file = FilesystemBackend.get(loc)
5656
57 expected_data = "chunk00000remainder"57 expected_data = "chunk00000remainder"
58 expected_num_chunks = 258 expected_num_chunks = 2
@@ -70,10 +70,10 @@
70 Test that trying to retrieve a file that doesn't exist70 Test that trying to retrieve a file that doesn't exist
71 raises an error71 raises an error
72 """72 """
73 url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing")73 loc = get_location_from_uri("file:///tmp/glance-tests/non-existing")
74 self.assertRaises(exception.NotFound,74 self.assertRaises(exception.NotFound,
75 FilesystemBackend.get,75 FilesystemBackend.get,
76 url_pieces)76 loc)
7777
78 def test_add(self):78 def test_add(self):
79 """Test that we can add an image via the filesystem backend"""79 """Test that we can add an image via the filesystem backend"""
@@ -93,8 +93,8 @@
93 self.assertEquals(expected_file_size, size)93 self.assertEquals(expected_file_size, size)
94 self.assertEquals(expected_checksum, checksum)94 self.assertEquals(expected_checksum, checksum)
9595
96 url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42")96 loc = get_location_from_uri("file:///tmp/glance-tests/42")
97 new_image_file = FilesystemBackend.get(url_pieces)97 new_image_file = FilesystemBackend.get(loc)
98 new_image_contents = ""98 new_image_contents = ""
99 new_image_file_size = 099 new_image_file_size = 0
100100
@@ -122,20 +122,19 @@
122 """122 """
123 Test we can delete an existing image in the filesystem store123 Test we can delete an existing image in the filesystem store
124 """124 """
125 url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2")125 loc = get_location_from_uri("file:///tmp/glance-tests/2")
126126 FilesystemBackend.delete(loc)
127 FilesystemBackend.delete(url_pieces)
128127
129 self.assertRaises(exception.NotFound,128 self.assertRaises(exception.NotFound,
130 FilesystemBackend.get,129 FilesystemBackend.get,
131 url_pieces)130 loc)
132131
133 def test_delete_non_existing(self):132 def test_delete_non_existing(self):
134 """133 """
135 Test that trying to delete a file that doesn't exist134 Test that trying to delete a file that doesn't exist
136 raises an error135 raises an error
137 """136 """
138 url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing")137 loc = get_location_from_uri("file:///tmp/glance-tests/non-existing")
139 self.assertRaises(exception.NotFound,138 self.assertRaises(exception.NotFound,
140 FilesystemBackend.delete,139 FilesystemBackend.delete,
141 url_pieces)140 loc)
142141
=== added file 'tests/unit/test_store_location.py'
--- tests/unit/test_store_location.py 1970-01-01 00:00:00 +0000
+++ tests/unit/test_store_location.py 2011-07-21 13:59:26 +0000
@@ -0,0 +1,243 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack, LLC
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18import unittest
19
20from glance.common import exception
21import glance.store.location as location
22import glance.store.http
23import glance.store.filesystem
24import glance.store.swift
25import glance.store.s3
26
27
28class TestStoreLocation(unittest.TestCase):
29
30 def test_get_location_from_uri_back_to_uri(self):
31 """
32 Test that for various URIs, the correct Location
33 object can be contructed and then the original URI
34 returned via the get_store_uri() method.
35 """
36 good_store_uris = [
37 'https://user:pass@example.com:80/images/some-id',
38 'http://images.oracle.com/123456',
39 'swift://account:user:pass@authurl.com/container/obj-id',
40 'swift+https://account:user:pass@authurl.com/container/obj-id',
41 's3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id',
42 's3://accesskey:secretwith/aslash@s3.amazonaws.com/bucket/key-id',
43 's3+http://accesskey:secret@s3.amazonaws.com/bucket/key-id',
44 's3+https://accesskey:secretkey@s3.amazonaws.com/bucket/key-id',
45 'file:///var/lib/glance/images/1']
46
47 for uri in good_store_uris:
48 loc = location.get_location_from_uri(uri)
49 # The get_store_uri() method *should* return an identical URI
50 # to the URI that is passed to get_location_from_uri()
51 self.assertEqual(loc.get_store_uri(), uri)
52
53 def test_bad_store_scheme(self):
54 """
55 Test that a URI with a non-existing scheme triggers exception
56 """
57 bad_uri = 'unknown://user:pass@example.com:80/images/some-id'
58
59 self.assertRaises(exception.UnknownScheme,
60 location.get_location_from_uri,
61 bad_uri)
62
63 def test_filesystem_store_location(self):
64 """
65 Test the specific StoreLocation for the Filesystem store
66 """
67 uri = 'file:///var/lib/glance/images/1'
68 loc = glance.store.filesystem.StoreLocation({})
69 loc.parse_uri(uri)
70
71 self.assertEqual("file", loc.scheme)
72 self.assertEqual("/var/lib/glance/images/1", loc.path)
73 self.assertEqual(uri, loc.get_uri())
74
75 bad_uri = 'fil://'
76 self.assertRaises(Exception, loc.parse_uri, bad_uri)
77
78 bad_uri = 'file://'
79 self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
80
81 def test_http_store_location(self):
82 """
83 Test the specific StoreLocation for the HTTP store
84 """
85 uri = 'http://example.com/images/1'
86 loc = glance.store.http.StoreLocation({})
87 loc.parse_uri(uri)
88
89 self.assertEqual("http", loc.scheme)
90 self.assertEqual("example.com", loc.netloc)
91 self.assertEqual("/images/1", loc.path)
92 self.assertEqual(uri, loc.get_uri())
93
94 uri = 'https://example.com:8080/images/container/1'
95 loc.parse_uri(uri)
96
97 self.assertEqual("https", loc.scheme)
98 self.assertEqual("example.com:8080", loc.netloc)
99 self.assertEqual("/images/container/1", loc.path)
100 self.assertEqual(uri, loc.get_uri())
101
102 uri = 'https://user:password@example.com:8080/images/container/1'
103 loc.parse_uri(uri)
104
105 self.assertEqual("https", loc.scheme)
106 self.assertEqual("example.com:8080", loc.netloc)
107 self.assertEqual("user", loc.user)
108 self.assertEqual("password", loc.password)
109 self.assertEqual("/images/container/1", loc.path)
110 self.assertEqual(uri, loc.get_uri())
111
112 uri = 'https://user:@example.com:8080/images/1'
113 loc.parse_uri(uri)
114
115 self.assertEqual("https", loc.scheme)
116 self.assertEqual("example.com:8080", loc.netloc)
117 self.assertEqual("user", loc.user)
118 self.assertEqual("", loc.password)
119 self.assertEqual("/images/1", loc.path)
120 self.assertEqual(uri, loc.get_uri())
121
122 bad_uri = 'htt://'
123 self.assertRaises(Exception, loc.parse_uri, bad_uri)
124
125 bad_uri = 'http://'
126 self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
127
128 bad_uri = 'http://user@example.com:8080/images/1'
129 self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
130
131 def test_swift_store_location(self):
132 """
133 Test the specific StoreLocation for the Swift store
134 """
135 uri = 'swift://example.com/images/1'
136 loc = glance.store.swift.StoreLocation({})
137 loc.parse_uri(uri)
138
139 self.assertEqual("swift", loc.scheme)
140 self.assertEqual("example.com", loc.authurl)
141 self.assertEqual("images", loc.container)
142 self.assertEqual("1", loc.obj)
143 self.assertEqual(None, loc.user)
144 self.assertEqual(uri, loc.get_uri())
145
146 uri = 'swift+https://user:pass@authurl.com/images/1'
147 loc.parse_uri(uri)
148
149 self.assertEqual("swift+https", loc.scheme)
150 self.assertEqual("authurl.com", loc.authurl)
151 self.assertEqual("images", loc.container)
152 self.assertEqual("1", loc.obj)
153 self.assertEqual("user", loc.user)
154 self.assertEqual("pass", loc.key)
155 self.assertEqual(uri, loc.get_uri())
156
157 uri = 'swift+https://user:pass@authurl.com/v1/container/12345'
158 loc.parse_uri(uri)
159
160 self.assertEqual("swift+https", loc.scheme)
161 self.assertEqual("authurl.com/v1", loc.authurl)
162 self.assertEqual("container", loc.container)
163 self.assertEqual("12345", loc.obj)
164 self.assertEqual("user", loc.user)
165 self.assertEqual("pass", loc.key)
166 self.assertEqual(uri, loc.get_uri())
167
168 uri = 'swift://account:user:pass@authurl.com/v1/container/12345'
169 loc.parse_uri(uri)
170
171 self.assertEqual("swift", loc.scheme)
172 self.assertEqual("authurl.com/v1", loc.authurl)
173 self.assertEqual("container", loc.container)
174 self.assertEqual("12345", loc.obj)
175 self.assertEqual("account:user", loc.user)
176 self.assertEqual("pass", loc.key)
177 self.assertEqual(uri, loc.get_uri())
178
179 bad_uri = 'swif://'
180 self.assertRaises(Exception, loc.parse_uri, bad_uri)
181
182 bad_uri = 'swift://'
183 self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
184
185 bad_uri = 'swift://user@example.com:8080/images/1'
186 self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
187
188 def test_s3_store_location(self):
189 """
190 Test the specific StoreLocation for the S3 store
191 """
192 uri = 's3://example.com/images/1'
193 loc = glance.store.s3.StoreLocation({})
194 loc.parse_uri(uri)
195
196 self.assertEqual("s3", loc.scheme)
197 self.assertEqual("example.com", loc.s3serviceurl)
198 self.assertEqual("images", loc.bucket)
199 self.assertEqual("1", loc.key)
200 self.assertEqual(None, loc.accesskey)
201 self.assertEqual(uri, loc.get_uri())
202
203 uri = 's3+https://accesskey:pass@s3serviceurl.com/images/1'
204 loc.parse_uri(uri)
205
206 self.assertEqual("s3+https", loc.scheme)
207 self.assertEqual("s3serviceurl.com", loc.s3serviceurl)
208 self.assertEqual("images", loc.bucket)
209 self.assertEqual("1", loc.key)
210 self.assertEqual("accesskey", loc.accesskey)
211 self.assertEqual("pass", loc.secretkey)
212 self.assertEqual(uri, loc.get_uri())
213
214 uri = 's3+https://accesskey:pass@s3serviceurl.com/v1/bucket/12345'
215 loc.parse_uri(uri)
216
217 self.assertEqual("s3+https", loc.scheme)
218 self.assertEqual("s3serviceurl.com/v1", loc.s3serviceurl)
219 self.assertEqual("bucket", loc.bucket)
220 self.assertEqual("12345", loc.key)
221 self.assertEqual("accesskey", loc.accesskey)
222 self.assertEqual("pass", loc.secretkey)
223 self.assertEqual(uri, loc.get_uri())
224
225 uri = 's3://accesskey:pass/withslash@s3serviceurl.com/v1/bucket/12345'
226 loc.parse_uri(uri)
227
228 self.assertEqual("s3", loc.scheme)
229 self.assertEqual("s3serviceurl.com/v1", loc.s3serviceurl)
230 self.assertEqual("bucket", loc.bucket)
231 self.assertEqual("12345", loc.key)
232 self.assertEqual("accesskey", loc.accesskey)
233 self.assertEqual("pass/withslash", loc.secretkey)
234 self.assertEqual(uri, loc.get_uri())
235
236 bad_uri = 'swif://'
237 self.assertRaises(Exception, loc.parse_uri, bad_uri)
238
239 bad_uri = 's3://'
240 self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
241
242 bad_uri = 's3://accesskey@example.com:8080/images/1'
243 self.assertRaises(exception.BadStoreUri, loc.parse_uri, bad_uri)
0244
=== modified file 'tests/unit/test_stores.py'
--- tests/unit/test_stores.py 2011-03-05 00:02:26 +0000
+++ tests/unit/test_stores.py 2011-07-21 13:59:26 +0000
@@ -19,7 +19,6 @@
1919
20import stubout20import stubout
21import unittest21import unittest
22import urlparse
2322
24from glance.store.s3 import S3Backend23from glance.store.s3 import S3Backend
25from glance.store import Backend, BackendException, get_from_backend24from glance.store import Backend, BackendException, get_from_backend
2625
=== modified file 'tests/unit/test_swift_store.py'
--- tests/unit/test_swift_store.py 2011-04-13 23:18:26 +0000
+++ tests/unit/test_swift_store.py 2011-07-21 13:59:26 +0000
@@ -29,9 +29,8 @@
2929
30from glance.common import exception30from glance.common import exception
31from glance.store import BackendException31from glance.store import BackendException
32from glance.store.swift import (SwiftBackend,32from glance.store.swift import SwiftBackend
33 format_swift_location,33from glance.store.location import get_location_from_uri
34 parse_swift_tokens)
3534
36FIVE_KB = (5 * 1024)35FIVE_KB = (5 * 1024)
37SWIFT_OPTIONS = {'verbose': True,36SWIFT_OPTIONS = {'verbose': True,
@@ -146,6 +145,18 @@
146 'http_connection', fake_http_connection)145 'http_connection', fake_http_connection)
147146
148147
148def format_swift_location(user, key, authurl, container, obj):
149 """
150 Helper method that returns a Swift store URI given
151 the component pieces.
152 """
153 scheme = 'swift+https'
154 if authurl.startswith('http://'):
155 scheme = 'swift+http'
156 return "%s://%s:%s@%s/%s/%s" % (scheme, user, key, authurl,
157 container, obj)
158
159
149class TestSwiftBackend(unittest.TestCase):160class TestSwiftBackend(unittest.TestCase):
150161
151 def setUp(self):162 def setUp(self):
@@ -157,46 +168,27 @@
157 """Clear the test environment"""168 """Clear the test environment"""
158 self.stubs.UnsetAll()169 self.stubs.UnsetAll()
159170
160 def test_parse_swift_tokens(self):
161 """
162 Test that the parse_swift_tokens function returns
163 user, key, authurl, container, and objname properly
164 """
165 uri = "swift://user:key@localhost/v1.0/container/objname"
166 url_pieces = urlparse.urlparse(uri)
167 user, key, authurl, container, objname =\
168 parse_swift_tokens(url_pieces)
169 self.assertEqual("user", user)
170 self.assertEqual("key", key)
171 self.assertEqual("https://localhost/v1.0", authurl)
172 self.assertEqual("container", container)
173 self.assertEqual("objname", objname)
174
175 uri = "swift://user:key@localhost:9090/v1.0/container/objname"
176 url_pieces = urlparse.urlparse(uri)
177 user, key, authurl, container, objname =\
178 parse_swift_tokens(url_pieces)
179 self.assertEqual("user", user)
180 self.assertEqual("key", key)
181 self.assertEqual("https://localhost:9090/v1.0", authurl)
182 self.assertEqual("container", container)
183 self.assertEqual("objname", objname)
184
185 uri = "swift://account:user:key@localhost:9090/v1.0/container/objname"
186 url_pieces = urlparse.urlparse(uri)
187 user, key, authurl, container, objname =\
188 parse_swift_tokens(url_pieces)
189 self.assertEqual("account:user", user)
190 self.assertEqual("key", key)
191 self.assertEqual("https://localhost:9090/v1.0", authurl)
192 self.assertEqual("container", container)
193 self.assertEqual("objname", objname)
194
195 def test_get(self):171 def test_get(self):
196 """Test a "normal" retrieval of an image in chunks"""172 """Test a "normal" retrieval of an image in chunks"""
197 url_pieces = urlparse.urlparse(173 loc = get_location_from_uri("swift://user:key@auth_address/glance/2")
198 "swift://user:key@auth_address/glance/2")174 image_swift = SwiftBackend.get(loc)
199 image_swift = SwiftBackend.get(url_pieces)175
176 expected_data = "*" * FIVE_KB
177 data = ""
178
179 for chunk in image_swift:
180 data += chunk
181 self.assertEqual(expected_data, data)
182
183 def test_get_with_http_auth(self):
184 """
185 Test a retrieval from Swift with an HTTP authurl. This is
186 specified either via a Location header with swift+http:// or using
187 http:// in the swift_store_auth_address config value
188 """
189 loc = get_location_from_uri("swift+http://user:key@auth_address/"
190 "glance/2")
191 image_swift = SwiftBackend.get(loc)
200192
201 expected_data = "*" * FIVE_KB193 expected_data = "*" * FIVE_KB
202 data = ""194 data = ""
@@ -210,11 +202,10 @@
210 Test retrieval of an image with wrong expected_size param202 Test retrieval of an image with wrong expected_size param
211 raises an exception203 raises an exception
212 """204 """
213 url_pieces = urlparse.urlparse(205 loc = get_location_from_uri("swift://user:key@auth_address/glance/2")
214 "swift://user:key@auth_address/glance/2")
215 self.assertRaises(BackendException,206 self.assertRaises(BackendException,
216 SwiftBackend.get,207 SwiftBackend.get,
217 url_pieces,208 loc,
218 {'expected_size': 42})209 {'expected_size': 42})
219210
220 def test_get_non_existing(self):211 def test_get_non_existing(self):
@@ -222,11 +213,10 @@
222 Test that trying to retrieve a swift that doesn't exist213 Test that trying to retrieve a swift that doesn't exist
223 raises an error214 raises an error
224 """215 """
225 url_pieces = urlparse.urlparse(216 loc = get_location_from_uri("swift://user:key@authurl/glance/noexist")
226 "swift://user:key@auth_address/noexist")
227 self.assertRaises(exception.NotFound,217 self.assertRaises(exception.NotFound,
228 SwiftBackend.get,218 SwiftBackend.get,
229 url_pieces)219 loc)
230220
231 def test_add(self):221 def test_add(self):
232 """Test that we can add an image via the swift backend"""222 """Test that we can add an image via the swift backend"""
@@ -249,14 +239,62 @@
249 self.assertEquals(expected_swift_size, size)239 self.assertEquals(expected_swift_size, size)
250 self.assertEquals(expected_checksum, checksum)240 self.assertEquals(expected_checksum, checksum)
251241
252 url_pieces = urlparse.urlparse(expected_location)242 loc = get_location_from_uri(expected_location)
253 new_image_swift = SwiftBackend.get(url_pieces)243 new_image_swift = SwiftBackend.get(loc)
254 new_image_contents = new_image_swift.getvalue()244 new_image_contents = new_image_swift.getvalue()
255 new_image_swift_size = new_image_swift.len245 new_image_swift_size = new_image_swift.len
256246
257 self.assertEquals(expected_swift_contents, new_image_contents)247 self.assertEquals(expected_swift_contents, new_image_contents)
258 self.assertEquals(expected_swift_size, new_image_swift_size)248 self.assertEquals(expected_swift_size, new_image_swift_size)
259249
250 def test_add_auth_url_variations(self):
251 """
252 Test that we can add an image via the swift backend with
253 a variety of different auth_address values
254 """
255 variations = ['http://localhost:80',
256 'http://localhost',
257 'http://localhost/v1',
258 'http://localhost/v1/',
259 'https://localhost',
260 'https://localhost:8080',
261 'https://localhost/v1',
262 'https://localhost/v1/',
263 'localhost',
264 'localhost:8080/v1']
265 i = 42
266 for variation in variations:
267 expected_image_id = i
268 expected_swift_size = FIVE_KB
269 expected_swift_contents = "*" * expected_swift_size
270 expected_checksum = \
271 hashlib.md5(expected_swift_contents).hexdigest()
272 new_options = SWIFT_OPTIONS.copy()
273 new_options['swift_store_auth_address'] = variation
274 expected_location = format_swift_location(
275 new_options['swift_store_user'],
276 new_options['swift_store_key'],
277 new_options['swift_store_auth_address'],
278 new_options['swift_store_container'],
279 expected_image_id)
280 image_swift = StringIO.StringIO(expected_swift_contents)
281
282 location, size, checksum = SwiftBackend.add(i, image_swift,
283 new_options)
284
285 self.assertEquals(expected_location, location)
286 self.assertEquals(expected_swift_size, size)
287 self.assertEquals(expected_checksum, checksum)
288
289 loc = get_location_from_uri(expected_location)
290 new_image_swift = SwiftBackend.get(loc)
291 new_image_contents = new_image_swift.getvalue()
292 new_image_swift_size = new_image_swift.len
293
294 self.assertEquals(expected_swift_contents, new_image_contents)
295 self.assertEquals(expected_swift_size, new_image_swift_size)
296 i = i + 1
297
260 def test_add_no_container_no_create(self):298 def test_add_no_container_no_create(self):
261 """299 """
262 Tests that adding an image with a non-existing container300 Tests that adding an image with a non-existing container
@@ -306,8 +344,8 @@
306 self.assertEquals(expected_swift_size, size)344 self.assertEquals(expected_swift_size, size)
307 self.assertEquals(expected_checksum, checksum)345 self.assertEquals(expected_checksum, checksum)
308346
309 url_pieces = urlparse.urlparse(expected_location)347 loc = get_location_from_uri(expected_location)
310 new_image_swift = SwiftBackend.get(url_pieces)348 new_image_swift = SwiftBackend.get(loc)
311 new_image_contents = new_image_swift.getvalue()349 new_image_contents = new_image_swift.getvalue()
312 new_image_swift_size = new_image_swift.len350 new_image_swift_size = new_image_swift.len
313351
@@ -356,22 +394,20 @@
356 """394 """
357 Test we can delete an existing image in the swift store395 Test we can delete an existing image in the swift store
358 """396 """
359 url_pieces = urlparse.urlparse(397 loc = get_location_from_uri("swift://user:key@authurl/glance/2")
360 "swift://user:key@auth_address/glance/2")
361398
362 SwiftBackend.delete(url_pieces)399 SwiftBackend.delete(loc)
363400
364 self.assertRaises(exception.NotFound,401 self.assertRaises(exception.NotFound,
365 SwiftBackend.get,402 SwiftBackend.get,
366 url_pieces)403 loc)
367404
368 def test_delete_non_existing(self):405 def test_delete_non_existing(self):
369 """406 """
370 Test that trying to delete a swift that doesn't exist407 Test that trying to delete a swift that doesn't exist
371 raises an error408 raises an error
372 """409 """
373 url_pieces = urlparse.urlparse(410 loc = get_location_from_uri("swift://user:key@authurl/glance/noexist")
374 "swift://user:key@auth_address/noexist")
375 self.assertRaises(exception.NotFound,411 self.assertRaises(exception.NotFound,
376 SwiftBackend.delete,412 SwiftBackend.delete,
377 url_pieces)413 loc)

Subscribers

People subscribed via source and target branches