Adds multi tenant support for swift backend.

Updates the swift store to support multiple tenants:

 * Added configuration option for swift_store_multi_tenant.

 * Updated the swift connection creation logic so that in multi-tenant
   mode the token and storage URL from the service catalog are used
   to create swift connection.

 * When in multi-tenant mode locations URL's (stored in the DB) do
   not contain hard coded swift credentials.

Includes unit tests to verify multi-tenant swift storage URLs.

Partially implements blueprint: swift-tenant-specific-storage.

Change-Id: I45fc97027e6f211ac353513c2d9d6da51ccf4489
This commit is contained in:
Dan Prince 2012-06-29 14:43:29 -04:00
parent 53e210a0b3
commit 8b2d038185
7 changed files with 126 additions and 34 deletions

View File

@ -380,6 +380,19 @@ Can only be specified in configuration files.
When doing a large object manifest, what size, in MB, should
Glance write chunks to Swift? The default is 200MB.
* ``swift_store_multi_tenant=False``
Optional. Default: ``False``
Can only be specified in configuration files.
`This option is specific to the Swift storage backend.`
If set to True enables multi-tenant storage mode which causes Glance images
to be stored in tenant specific Swift accounts. When set to False Glance
stores all images in a single Swift account.
Configuring the S3 Storage Backend
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -203,6 +203,10 @@ swift_store_large_object_chunk_size = 200
# Ex. https://example.com/v1.0/ -> https://snet-example.com/v1.0/
swift_enable_snet = False
# If set to True enables multi-tenant storage mode which causes Glance images
# to be stored in tenant specific Swift accounts.
# swift_store_multi_tenant = False
# ============ S3 Store Options =============================
# Address where the S3 authentication service lives

View File

@ -163,7 +163,7 @@ def legacy_parse_uri(self, uri):
if not netloc.startswith('http'):
# push hostname back into the remaining to build full authurl
path_parts.insert(0, netloc)
self.authurl = '/'.join(path_parts)
self.auth_or_store_url = '/'.join(path_parts)
except IndexError:
reason = _("Badly formed S3 URI: %s") % uri
LOG.error(message=reason)

View File

@ -35,7 +35,7 @@ be the host:port of that Glance API server along with /images/<IMAGE_ID>.
The Glance storage URI is an internal URI structure that Glance
uses to maintain critical information about how to access the images
that it stores in its storage backends. It **does contain** security
that it stores in its storage backends. It **may contain** security
credentials and is **not** user-facing.
"""
@ -61,6 +61,7 @@ def get_location_from_uri(uri):
Example URIs:
https://user:pass@example.com:80/images/some-id
http://images.oracle.com/123456
swift://example.com/container/obj-id
swift://user:account:pass@authurl.com/container/obj-id
swift+http://user:account:pass@authurl.com/container/obj-id
s3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id

View File

@ -25,6 +25,7 @@ import math
import urllib
import urlparse
from glance.common import auth
from glance.common import exception
from glance.openstack.common import cfg
import glance.openstack.common.log as logging
@ -57,6 +58,7 @@ swift_opts = [
cfg.IntOpt('swift_store_large_object_chunk_size',
default=DEFAULT_LARGE_OBJECT_CHUNK_SIZE),
cfg.BoolOpt('swift_store_create_container_on_put', default=False),
cfg.BoolOpt('swift_store_multi_tenant', default=False),
]
CONF = cfg.CONF
@ -74,6 +76,10 @@ class StoreLocation(glance.store.location.StoreLocation):
swift+http://user:pass@authurl.com/container/obj-id
swift+https://user:pass@authurl.com/container/obj-id
When using multi-tenant a URI might look like this (a storage URL):
swift+https://example.com/container/obj-id
The swift+http:// URIs indicate there is an HTTP authentication URL.
The default for Swift is an HTTPS authentication URL, so swift:// and
swift+https:// are the same...
@ -83,28 +89,28 @@ class StoreLocation(glance.store.location.StoreLocation):
self.scheme = self.specs.get('scheme', 'swift+https')
self.user = self.specs.get('user')
self.key = self.specs.get('key')
self.authurl = self.specs.get('authurl')
self.auth_or_store_url = self.specs.get('auth_or_store_url')
self.container = self.specs.get('container')
self.obj = self.specs.get('obj')
def _get_credstring(self):
if self.user:
if self.user and self.key:
return '%s:%s@' % (urllib.quote(self.user), urllib.quote(self.key))
return ''
def get_uri(self):
authurl = self.authurl
if authurl.startswith('http://'):
authurl = authurl[7:]
elif authurl.startswith('https://'):
authurl = authurl[8:]
auth_or_store_url = self.auth_or_store_url
if auth_or_store_url.startswith('http://'):
auth_or_store_url = auth_or_store_url[len('http://'):]
elif auth_or_store_url.startswith('https://'):
auth_or_store_url = auth_or_store_url[len('https://'):]
credstring = self._get_credstring()
authurl = authurl.strip('/')
auth_or_store_url = auth_or_store_url.strip('/')
container = self.container.strip('/')
obj = self.obj.strip('/')
return '%s://%s%s/%s/%s' % (self.scheme, credstring, authurl,
return '%s://%s%s/%s/%s' % (self.scheme, credstring, auth_or_store_url,
container, obj)
def parse_uri(self, uri):
@ -163,6 +169,7 @@ class StoreLocation(glance.store.location.StoreLocation):
self.key = urllib.unquote(key)
else:
self.user = None
self.key = None
path_parts = path.split('/')
try:
self.obj = path_parts.pop()
@ -170,14 +177,14 @@ class StoreLocation(glance.store.location.StoreLocation):
if not netloc.startswith('http'):
# push hostname back into the remaining to build full authurl
path_parts.insert(0, netloc)
self.authurl = '/'.join(path_parts)
self.auth_or_store_url = '/'.join(path_parts)
except IndexError:
reason = _("Badly formed Swift URI: %s") % uri
LOG.error(reason)
raise exception.BadStoreUri()
@property
def swift_auth_url(self):
def swift_url(self):
"""
Creates a fully-qualified auth url that the Swift client library can
use. The scheme for the auth_url is determined using the scheme
@ -190,7 +197,7 @@ class StoreLocation(glance.store.location.StoreLocation):
else:
auth_scheme = 'http://'
full_url = ''.join([auth_scheme, self.authurl])
full_url = ''.join([auth_scheme, self.auth_or_store_url])
return full_url
@ -206,7 +213,10 @@ class Store(glance.store.base.Store):
def configure(self):
self.snet = CONF.swift_enable_snet
self.multi_tenant = CONF.swift_store_multi_tenant
self.auth_version = self._option_get('swift_store_auth_version')
self.storage_url = None
self.token = None
def configure_add(self):
"""
@ -219,6 +229,20 @@ class Store(glance.store.base.Store):
self.user = self._option_get('swift_store_user')
self.key = self._option_get('swift_store_key')
self.container = CONF.swift_store_container
if self.multi_tenant:
if context is None:
reason = _("Multi-tenant Swift storage requires a context.")
raise exception.BadStoreConfiguration(store_name="swift",
reason=reason)
self.token = context.auth_tok
self.key = None # multi-tenant uses tokens, not (passwords)
if context.tenant and context.user:
self.user = context.tenant + ':' + context.user
if context.service_catalog:
service_catalog = context.service_catalog
self.storage_url = self._get_swift_endpoint(service_catalog)
try:
# The config file has swift_store_large_object_*size in MB, but
# internally we store it in bytes, since the image_size parameter
@ -242,6 +266,9 @@ class Store(glance.store.base.Store):
else: # Defaults https
self.full_auth_address = 'https://' + self.auth_address
def _get_swift_endpoint(self, service_catalog):
return auth.get_endpoint(service_catalog, service_type='object-store')
def get(self, location):
"""
Takes a `glance.store.location.Location` object that indicates
@ -253,8 +280,7 @@ class Store(glance.store.base.Store):
:raises `glance.exception.NotFound` if image does not exist
"""
loc = location.store_location
swift_conn = self._make_swift_connection(
auth_url=loc.swift_auth_url, user=loc.user, key=loc.key)
swift_conn = self._swift_connection_for_location(loc)
try:
(resp_headers, resp_body) = swift_conn.get_object(
@ -288,8 +314,7 @@ class Store(glance.store.base.Store):
from glance.store.location.get_location_from_uri()
"""
loc = location.store_location
swift_conn = self._make_swift_connection(
auth_url=loc.swift_auth_url, user=loc.user, key=loc.key)
swift_conn = self._swift_connection_for_location(loc)
try:
resp_headers = swift_conn.head_object(container=loc.container,
@ -298,9 +323,31 @@ class Store(glance.store.base.Store):
except Exception:
return 0
def _make_swift_connection(self, auth_url, user, key):
def _swift_connection_for_location(self, loc):
if loc.user:
return self._make_swift_connection(
loc.swift_url, loc.user, loc.key)
else:
if self.multi_tenant:
return self._make_swift_connection(
None, self.user, None,
storage_url=loc.swift_url, token=self.token)
else:
reason = (_("Location is missing user:password information."))
LOG.error(reason)
raise exception.BadStoreUri(message=reason)
def _make_swift_connection(self, auth_url, user, key, storage_url=None,
token=None):
"""
Creates a connection using the Swift client library.
:param auth_url The authentication for v1 style Swift auth or
v2 style Keystone auth.
:param user A string containing the tenant:user information.
:param key A string containing the key/password for the connection.
:param storage_url A string containing the storage URL.
:param token A string containing the token
"""
snet = self.snet
auth_version = self.auth_version
@ -320,9 +367,15 @@ class Store(glance.store.base.Store):
raise exception.BadStoreUri()
(tenant_name, user) = tenant_user
return swiftclient.Connection(
authurl=full_auth_url, user=user, key=key, snet=snet,
tenant_name=tenant_name, auth_version=auth_version)
if self.multi_tenant:
#NOTE: multi-tenant supports v2 auth only
return swiftclient.Connection(
None, user, None, preauthurl=storage_url, preauthtoken=token,
snet=snet, tenant_name=tenant_name, auth_version='2')
else:
return swiftclient.Connection(
full_auth_url, user, key, snet=snet,
tenant_name=tenant_name, auth_version=auth_version)
def _option_get(self, param):
result = getattr(CONF, param)
@ -370,7 +423,8 @@ class Store(glance.store.base.Store):
fail if the image turns out to be greater than 5GB.
"""
swift_conn = self._make_swift_connection(
auth_url=self.full_auth_address, user=self.user, key=self.key)
self.full_auth_address, self.user, self.key,
storage_url=self.storage_url, token=self.token)
create_container_if_missing(self.container, swift_conn)
@ -378,7 +432,7 @@ class Store(glance.store.base.Store):
location = StoreLocation({'scheme': self.scheme,
'container': self.container,
'obj': obj_name,
'authurl': self.auth_address,
'auth_or_store_url': self.auth_address,
'user': self.user,
'key': self.key})
@ -488,8 +542,7 @@ class Store(glance.store.base.Store):
:raises NotFound if image does not exist
"""
loc = location.store_location
swift_conn = self._make_swift_connection(
auth_url=loc.swift_auth_url, user=loc.user, key=loc.key)
swift_conn = self._swift_connection_for_location(loc)
try:
# We request the manifest for the object. If one exists,

View File

@ -43,6 +43,7 @@ class TestStoreLocation(base.StoreClearingUnitTest):
'https://user:pass@example.com:80/images/some-id',
'http://images.oracle.com/123456',
'swift://account%3Auser:pass@authurl.com/container/obj-id',
'swift://storeurl.com/container/obj-id',
'swift+https://account%3Auser:pass@authurl.com/container/obj-id',
's3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id',
's3://accesskey:secretwith/aslash@s3.amazonaws.com/bucket/key-id',
@ -143,8 +144,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
loc.parse_uri(uri)
self.assertEqual("swift", loc.scheme)
self.assertEqual("example.com", loc.authurl)
self.assertEqual("https://example.com", loc.swift_auth_url)
self.assertEqual("example.com", loc.auth_or_store_url)
self.assertEqual("https://example.com", loc.swift_url)
self.assertEqual("images", loc.container)
self.assertEqual("1", loc.obj)
self.assertEqual(None, loc.user)
@ -154,8 +155,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
loc.parse_uri(uri)
self.assertEqual("swift+https", loc.scheme)
self.assertEqual("authurl.com", loc.authurl)
self.assertEqual("https://authurl.com", loc.swift_auth_url)
self.assertEqual("authurl.com", loc.auth_or_store_url)
self.assertEqual("https://authurl.com", loc.swift_url)
self.assertEqual("images", loc.container)
self.assertEqual("1", loc.obj)
self.assertEqual("user", loc.user)
@ -166,8 +167,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
loc.parse_uri(uri)
self.assertEqual("swift+https", loc.scheme)
self.assertEqual("authurl.com/v1", loc.authurl)
self.assertEqual("https://authurl.com/v1", loc.swift_auth_url)
self.assertEqual("authurl.com/v1", loc.auth_or_store_url)
self.assertEqual("https://authurl.com/v1", loc.swift_url)
self.assertEqual("container", loc.container)
self.assertEqual("12345", loc.obj)
self.assertEqual("user", loc.user)
@ -179,14 +180,27 @@ class TestStoreLocation(base.StoreClearingUnitTest):
loc.parse_uri(uri)
self.assertEqual("swift+http", loc.scheme)
self.assertEqual("authurl.com/v1", loc.authurl)
self.assertEqual("http://authurl.com/v1", loc.swift_auth_url)
self.assertEqual("authurl.com/v1", loc.auth_or_store_url)
self.assertEqual("http://authurl.com/v1", loc.swift_url)
self.assertEqual("container", loc.container)
self.assertEqual("12345", loc.obj)
self.assertEqual("a:user@example.com", loc.user)
self.assertEqual("p@ss", loc.key)
self.assertEqual(uri, loc.get_uri())
# multitenant puts store URL in the location (not auth)
uri = ('swift+http://storeurl.com/v1/container/12345')
loc.parse_uri(uri)
self.assertEqual("swift+http", loc.scheme)
self.assertEqual("storeurl.com/v1", loc.auth_or_store_url)
self.assertEqual("http://storeurl.com/v1", loc.swift_url)
self.assertEqual("container", loc.container)
self.assertEqual("12345", loc.obj)
self.assertEqual(None, loc.user)
self.assertEqual(None, loc.key)
self.assertEqual(uri, loc.get_uri())
bad_uri = 'swif://'
self.assertRaises(Exception, loc.parse_uri, bad_uri)

View File

@ -613,6 +613,13 @@ class TestStoreAuthV2(TestStoreAuthV1):
self.store.get,
loc)
def test_v2_multi_tenant_location(self):
conf = self.getConfig()
conf['swift_store_multi_tenant'] = True
uri = "swift://auth_address/glance/%s" % (FAKE_UUID)
loc = get_location_from_uri(uri)
self.assertEqual('swift', loc.store_name)
class TestChunkReader(base.StoreClearingUnitTest):