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 When doing a large object manifest, what size, in MB, should
Glance write chunks to Swift? The default is 200MB. 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 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/ # Ex. https://example.com/v1.0/ -> https://snet-example.com/v1.0/
swift_enable_snet = False 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 ============================= # ============ S3 Store Options =============================
# Address where the S3 authentication service lives # Address where the S3 authentication service lives

View File

@ -163,7 +163,7 @@ def legacy_parse_uri(self, uri):
if not netloc.startswith('http'): if not netloc.startswith('http'):
# push hostname back into the remaining to build full authurl # push hostname back into the remaining to build full authurl
path_parts.insert(0, netloc) path_parts.insert(0, netloc)
self.authurl = '/'.join(path_parts) self.auth_or_store_url = '/'.join(path_parts)
except IndexError: except IndexError:
reason = _("Badly formed S3 URI: %s") % uri reason = _("Badly formed S3 URI: %s") % uri
LOG.error(message=reason) 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 The Glance storage URI is an internal URI structure that Glance
uses to maintain critical information about how to access the images 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. credentials and is **not** user-facing.
""" """
@ -61,6 +61,7 @@ def get_location_from_uri(uri):
Example URIs: Example URIs:
https://user:pass@example.com:80/images/some-id https://user:pass@example.com:80/images/some-id
http://images.oracle.com/123456 http://images.oracle.com/123456
swift://example.com/container/obj-id
swift://user:account:pass@authurl.com/container/obj-id swift://user:account:pass@authurl.com/container/obj-id
swift+http://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 s3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id

View File

@ -25,6 +25,7 @@ import math
import urllib import urllib
import urlparse import urlparse
from glance.common import auth
from glance.common import exception from glance.common import exception
from glance.openstack.common import cfg from glance.openstack.common import cfg
import glance.openstack.common.log as logging import glance.openstack.common.log as logging
@ -57,6 +58,7 @@ swift_opts = [
cfg.IntOpt('swift_store_large_object_chunk_size', cfg.IntOpt('swift_store_large_object_chunk_size',
default=DEFAULT_LARGE_OBJECT_CHUNK_SIZE), default=DEFAULT_LARGE_OBJECT_CHUNK_SIZE),
cfg.BoolOpt('swift_store_create_container_on_put', default=False), cfg.BoolOpt('swift_store_create_container_on_put', default=False),
cfg.BoolOpt('swift_store_multi_tenant', default=False),
] ]
CONF = cfg.CONF CONF = cfg.CONF
@ -74,6 +76,10 @@ class StoreLocation(glance.store.location.StoreLocation):
swift+http://user:pass@authurl.com/container/obj-id swift+http://user:pass@authurl.com/container/obj-id
swift+https://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 swift+http:// URIs indicate there is an HTTP authentication URL.
The default for Swift is an HTTPS authentication URL, so swift:// and The default for Swift is an HTTPS authentication URL, so swift:// and
swift+https:// are the same... swift+https:// are the same...
@ -83,28 +89,28 @@ class StoreLocation(glance.store.location.StoreLocation):
self.scheme = self.specs.get('scheme', 'swift+https') self.scheme = self.specs.get('scheme', 'swift+https')
self.user = self.specs.get('user') self.user = self.specs.get('user')
self.key = self.specs.get('key') 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.container = self.specs.get('container')
self.obj = self.specs.get('obj') self.obj = self.specs.get('obj')
def _get_credstring(self): 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 '%s:%s@' % (urllib.quote(self.user), urllib.quote(self.key))
return '' return ''
def get_uri(self): def get_uri(self):
authurl = self.authurl auth_or_store_url = self.auth_or_store_url
if authurl.startswith('http://'): if auth_or_store_url.startswith('http://'):
authurl = authurl[7:] auth_or_store_url = auth_or_store_url[len('http://'):]
elif authurl.startswith('https://'): elif auth_or_store_url.startswith('https://'):
authurl = authurl[8:] auth_or_store_url = auth_or_store_url[len('https://'):]
credstring = self._get_credstring() credstring = self._get_credstring()
authurl = authurl.strip('/') auth_or_store_url = auth_or_store_url.strip('/')
container = self.container.strip('/') container = self.container.strip('/')
obj = self.obj.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) container, obj)
def parse_uri(self, uri): def parse_uri(self, uri):
@ -163,6 +169,7 @@ class StoreLocation(glance.store.location.StoreLocation):
self.key = urllib.unquote(key) self.key = urllib.unquote(key)
else: else:
self.user = None self.user = None
self.key = None
path_parts = path.split('/') path_parts = path.split('/')
try: try:
self.obj = path_parts.pop() self.obj = path_parts.pop()
@ -170,14 +177,14 @@ class StoreLocation(glance.store.location.StoreLocation):
if not netloc.startswith('http'): if not netloc.startswith('http'):
# push hostname back into the remaining to build full authurl # push hostname back into the remaining to build full authurl
path_parts.insert(0, netloc) path_parts.insert(0, netloc)
self.authurl = '/'.join(path_parts) self.auth_or_store_url = '/'.join(path_parts)
except IndexError: except IndexError:
reason = _("Badly formed Swift URI: %s") % uri reason = _("Badly formed Swift URI: %s") % uri
LOG.error(reason) LOG.error(reason)
raise exception.BadStoreUri() raise exception.BadStoreUri()
@property @property
def swift_auth_url(self): def swift_url(self):
""" """
Creates a fully-qualified auth url that the Swift client library can Creates a fully-qualified auth url that the Swift client library can
use. The scheme for the auth_url is determined using the scheme use. The scheme for the auth_url is determined using the scheme
@ -190,7 +197,7 @@ class StoreLocation(glance.store.location.StoreLocation):
else: else:
auth_scheme = 'http://' auth_scheme = 'http://'
full_url = ''.join([auth_scheme, self.authurl]) full_url = ''.join([auth_scheme, self.auth_or_store_url])
return full_url return full_url
@ -206,7 +213,10 @@ class Store(glance.store.base.Store):
def configure(self): def configure(self):
self.snet = CONF.swift_enable_snet 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.auth_version = self._option_get('swift_store_auth_version')
self.storage_url = None
self.token = None
def configure_add(self): def configure_add(self):
""" """
@ -219,6 +229,20 @@ class Store(glance.store.base.Store):
self.user = self._option_get('swift_store_user') self.user = self._option_get('swift_store_user')
self.key = self._option_get('swift_store_key') self.key = self._option_get('swift_store_key')
self.container = CONF.swift_store_container 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: try:
# The config file has swift_store_large_object_*size in MB, but # The config file has swift_store_large_object_*size in MB, but
# internally we store it in bytes, since the image_size parameter # internally we store it in bytes, since the image_size parameter
@ -242,6 +266,9 @@ class Store(glance.store.base.Store):
else: # Defaults https else: # Defaults https
self.full_auth_address = 'https://' + self.auth_address 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): def get(self, location):
""" """
Takes a `glance.store.location.Location` object that indicates 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 :raises `glance.exception.NotFound` if image does not exist
""" """
loc = location.store_location loc = location.store_location
swift_conn = self._make_swift_connection( swift_conn = self._swift_connection_for_location(loc)
auth_url=loc.swift_auth_url, user=loc.user, key=loc.key)
try: try:
(resp_headers, resp_body) = swift_conn.get_object( (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() from glance.store.location.get_location_from_uri()
""" """
loc = location.store_location loc = location.store_location
swift_conn = self._make_swift_connection( swift_conn = self._swift_connection_for_location(loc)
auth_url=loc.swift_auth_url, user=loc.user, key=loc.key)
try: try:
resp_headers = swift_conn.head_object(container=loc.container, resp_headers = swift_conn.head_object(container=loc.container,
@ -298,9 +323,31 @@ class Store(glance.store.base.Store):
except Exception: except Exception:
return 0 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. 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 snet = self.snet
auth_version = self.auth_version auth_version = self.auth_version
@ -320,9 +367,15 @@ class Store(glance.store.base.Store):
raise exception.BadStoreUri() raise exception.BadStoreUri()
(tenant_name, user) = tenant_user (tenant_name, user) = tenant_user
return swiftclient.Connection( if self.multi_tenant:
authurl=full_auth_url, user=user, key=key, snet=snet, #NOTE: multi-tenant supports v2 auth only
tenant_name=tenant_name, auth_version=auth_version) 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): def _option_get(self, param):
result = getattr(CONF, 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. fail if the image turns out to be greater than 5GB.
""" """
swift_conn = self._make_swift_connection( 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) create_container_if_missing(self.container, swift_conn)
@ -378,7 +432,7 @@ class Store(glance.store.base.Store):
location = StoreLocation({'scheme': self.scheme, location = StoreLocation({'scheme': self.scheme,
'container': self.container, 'container': self.container,
'obj': obj_name, 'obj': obj_name,
'authurl': self.auth_address, 'auth_or_store_url': self.auth_address,
'user': self.user, 'user': self.user,
'key': self.key}) 'key': self.key})
@ -488,8 +542,7 @@ class Store(glance.store.base.Store):
:raises NotFound if image does not exist :raises NotFound if image does not exist
""" """
loc = location.store_location loc = location.store_location
swift_conn = self._make_swift_connection( swift_conn = self._swift_connection_for_location(loc)
auth_url=loc.swift_auth_url, user=loc.user, key=loc.key)
try: try:
# We request the manifest for the object. If one exists, # 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', 'https://user:pass@example.com:80/images/some-id',
'http://images.oracle.com/123456', 'http://images.oracle.com/123456',
'swift://account%3Auser:pass@authurl.com/container/obj-id', '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', 'swift+https://account%3Auser:pass@authurl.com/container/obj-id',
's3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id', 's3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id',
's3://accesskey:secretwith/aslash@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) loc.parse_uri(uri)
self.assertEqual("swift", loc.scheme) self.assertEqual("swift", loc.scheme)
self.assertEqual("example.com", loc.authurl) self.assertEqual("example.com", loc.auth_or_store_url)
self.assertEqual("https://example.com", loc.swift_auth_url) self.assertEqual("https://example.com", loc.swift_url)
self.assertEqual("images", loc.container) self.assertEqual("images", loc.container)
self.assertEqual("1", loc.obj) self.assertEqual("1", loc.obj)
self.assertEqual(None, loc.user) self.assertEqual(None, loc.user)
@ -154,8 +155,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
loc.parse_uri(uri) loc.parse_uri(uri)
self.assertEqual("swift+https", loc.scheme) self.assertEqual("swift+https", loc.scheme)
self.assertEqual("authurl.com", loc.authurl) self.assertEqual("authurl.com", loc.auth_or_store_url)
self.assertEqual("https://authurl.com", loc.swift_auth_url) self.assertEqual("https://authurl.com", loc.swift_url)
self.assertEqual("images", loc.container) self.assertEqual("images", loc.container)
self.assertEqual("1", loc.obj) self.assertEqual("1", loc.obj)
self.assertEqual("user", loc.user) self.assertEqual("user", loc.user)
@ -166,8 +167,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
loc.parse_uri(uri) loc.parse_uri(uri)
self.assertEqual("swift+https", loc.scheme) self.assertEqual("swift+https", loc.scheme)
self.assertEqual("authurl.com/v1", loc.authurl) self.assertEqual("authurl.com/v1", loc.auth_or_store_url)
self.assertEqual("https://authurl.com/v1", loc.swift_auth_url) self.assertEqual("https://authurl.com/v1", loc.swift_url)
self.assertEqual("container", loc.container) self.assertEqual("container", loc.container)
self.assertEqual("12345", loc.obj) self.assertEqual("12345", loc.obj)
self.assertEqual("user", loc.user) self.assertEqual("user", loc.user)
@ -179,14 +180,27 @@ class TestStoreLocation(base.StoreClearingUnitTest):
loc.parse_uri(uri) loc.parse_uri(uri)
self.assertEqual("swift+http", loc.scheme) self.assertEqual("swift+http", loc.scheme)
self.assertEqual("authurl.com/v1", loc.authurl) self.assertEqual("authurl.com/v1", loc.auth_or_store_url)
self.assertEqual("http://authurl.com/v1", loc.swift_auth_url) self.assertEqual("http://authurl.com/v1", loc.swift_url)
self.assertEqual("container", loc.container) self.assertEqual("container", loc.container)
self.assertEqual("12345", loc.obj) self.assertEqual("12345", loc.obj)
self.assertEqual("a:user@example.com", loc.user) self.assertEqual("a:user@example.com", loc.user)
self.assertEqual("p@ss", loc.key) self.assertEqual("p@ss", loc.key)
self.assertEqual(uri, loc.get_uri()) 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://' bad_uri = 'swif://'
self.assertRaises(Exception, loc.parse_uri, bad_uri) self.assertRaises(Exception, loc.parse_uri, bad_uri)

View File

@ -613,6 +613,13 @@ class TestStoreAuthV2(TestStoreAuthV1):
self.store.get, self.store.get,
loc) 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): class TestChunkReader(base.StoreClearingUnitTest):