summaryrefslogtreecommitdiff
path: root/glance/store/_drivers/swift.py
diff options
context:
space:
mode:
Diffstat (limited to 'glance/store/_drivers/swift.py')
-rw-r--r--glance/store/_drivers/swift.py687
1 files changed, 0 insertions, 687 deletions
diff --git a/glance/store/_drivers/swift.py b/glance/store/_drivers/swift.py
deleted file mode 100644
index 8e79d00..0000000
--- a/glance/store/_drivers/swift.py
+++ /dev/null
@@ -1,687 +0,0 @@
-# Copyright 2010-2011 OpenStack Foundation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""Storage backend for SWIFT"""
-
-from __future__ import absolute_import
-
-import hashlib
-import httplib
-import logging
-import math
-import urllib
-import urlparse
-
-from oslo.config import cfg
-
-from glance.common import auth
-from glance.common import exception
-from glance.openstack.common import excutils
-import glance.store
-import glance.store.driver
-import glance.store.location
-
-try:
- import swiftclient
-except ImportError:
- pass
-
-LOG = logging.getLogger(__name__)
-
-DEFAULT_CONTAINER = 'glance'
-DEFAULT_LARGE_OBJECT_SIZE = 5 * 1024 # 5GB
-DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 200 # 200M
-ONE_MB = 1000 * 1024
-
-swift_opts = [
- cfg.BoolOpt('swift_enable_snet', default=False,
- help=_('Whether to use ServiceNET to communicate with the '
- 'Swift storage servers.')),
- cfg.StrOpt('swift_store_auth_address',
- help=_('The address where the Swift authentication service '
- 'is listening.')),
- cfg.StrOpt('swift_store_user', secret=True,
- help=_('The user to authenticate against the Swift '
- 'authentication service')),
- cfg.StrOpt('swift_store_key', secret=True,
- help=_('Auth key for the user authenticating against the '
- 'Swift authentication service.')),
- cfg.StrOpt('swift_store_auth_version', default='2',
- help=_('Version of the authentication service to use. '
- 'Valid versions are 2 for keystone and 1 for swauth '
- 'and rackspace')),
- cfg.BoolOpt('swift_store_auth_insecure', default=False,
- help=_('If True, swiftclient won\'t check for a valid SSL '
- 'certificate when authenticating.')),
- cfg.StrOpt('swift_store_region',
- help=_('The region of the swift endpoint to be used for '
- 'single tenant. This setting is only necessary if the '
- 'tenant has multiple swift endpoints.')),
- cfg.StrOpt('swift_store_endpoint_type', default='publicURL',
- help=_('A string giving the endpoint type of the swift '
- 'service to use (publicURL, adminURL or internalURL). '
- 'This setting is only used if swift_store_auth_version '
- 'is 2.')),
- cfg.StrOpt('swift_store_service_type', default='object-store',
- help=_('A string giving the service type of the swift service '
- 'to use. This setting is only used if '
- 'swift_store_auth_version is 2.')),
- cfg.StrOpt('swift_store_container',
- default=DEFAULT_CONTAINER,
- help=_('Container within the account that the account should '
- 'use for storing images in Swift.')),
- cfg.IntOpt('swift_store_large_object_size',
- default=DEFAULT_LARGE_OBJECT_SIZE,
- help=_('The size, in MB, that Glance will start chunking image '
- 'files and do a large object manifest in Swift')),
- cfg.IntOpt('swift_store_large_object_chunk_size',
- default=DEFAULT_LARGE_OBJECT_CHUNK_SIZE,
- help=_('The amount of data written to a temporary disk buffer '
- 'during the process of chunking the image file.')),
- cfg.BoolOpt('swift_store_create_container_on_put', default=False,
- help=_('A boolean value that determines if we create the '
- 'container if it does not exist.')),
- cfg.BoolOpt('swift_store_multi_tenant', default=False,
- help=_('If set to True, enables multi-tenant storage '
- 'mode which causes Glance images to be stored in '
- 'tenant specific Swift accounts.')),
- cfg.ListOpt('swift_store_admin_tenants', default=[],
- help=_('A list of tenants that will be granted read/write '
- 'access on all Swift containers created by Glance in '
- 'multi-tenant mode.')),
- cfg.BoolOpt('swift_store_ssl_compression', default=True,
- help=_('If set to False, disables SSL layer compression of '
- 'https swift requests. Setting to False may improve '
- 'performance for images which are already in a '
- 'compressed format, eg qcow2.')),
-]
-
-CONF = cfg.CONF
-CONF.register_opts(swift_opts)
-
-
-class StoreLocation(glance.store.location.StoreLocation):
-
- """
- Class describing a Swift URI. A Swift URI can look like any of
- the following:
-
- swift://user:pass@authurl.com/container/obj-id
- swift://account: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
-
- 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...
- """
-
- def process_specs(self):
- self.scheme = self.specs.get('scheme', 'swift+https')
- self.user = self.specs.get('user')
- self.key = self.specs.get('key')
- 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 and self.key:
- return '%s:%s@' % (urllib.quote(self.user), urllib.quote(self.key))
- return ''
-
- def get_uri(self):
- 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()
- 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, auth_or_store_url,
- container, obj)
-
- def parse_uri(self, uri):
- """
- Parse URLs. This method fixes an issue where credentials specified
- in the URL are interpreted differently in Python 2.6.1+ than prior
- versions of Python. It also deals with the peculiarity that new-style
- Swift URIs have where a username can contain a ':', like so:
-
- swift://account:user:pass@authurl.com/container/obj
- """
- # Make sure that URIs that contain multiple schemes, such as:
- # swift://user:pass@http://authurl.com/v1/container/obj
- # are immediately rejected.
- if uri.count('://') != 1:
- reason = _("URI cannot contain more than one occurrence "
- "of a scheme. If you have specified a URI like "
- "swift://user:pass@http://authurl.com/v1/container/obj"
- ", you need to change it to use the "
- "swift+http:// scheme, like so: "
- "swift+http://user:pass@authurl.com/v1/container/obj")
- LOG.debug(_("Invalid store URI: %(reason)s"), {'reason': reason})
- raise exceptions.BadStoreUri(message=reason)
-
- pieces = urlparse.urlparse(uri)
- assert pieces.scheme in ('swift', 'swift+http', 'swift+https')
- self.scheme = pieces.scheme
- netloc = pieces.netloc
- path = pieces.path.lstrip('/')
- if netloc != '':
- # > Python 2.6.1
- if '@' in netloc:
- creds, netloc = netloc.split('@')
- else:
- creds = None
- else:
- # Python 2.6.1 compat
- # see lp659445 and Python issue7904
- if '@' in path:
- creds, path = path.split('@')
- else:
- creds = None
- netloc = path[0:path.find('/')].strip('/')
- path = path[path.find('/'):].strip('/')
- if creds:
- cred_parts = creds.split(':')
- if len(cred_parts) != 2:
- reason = (_("Badly formed credentials in Swift URI."))
- LOG.debug(reason)
- raise exceptions.BadStoreUri()
- user, key = cred_parts
- self.user = urllib.unquote(user)
- self.key = urllib.unquote(key)
- else:
- self.user = None
- self.key = None
- path_parts = path.split('/')
- try:
- self.obj = path_parts.pop()
- self.container = path_parts.pop()
- if not netloc.startswith('http'):
- # push hostname back into the remaining to build full authurl
- path_parts.insert(0, netloc)
- self.auth_or_store_url = '/'.join(path_parts)
- except IndexError:
- reason = _("Badly formed Swift URI.")
- LOG.debug(reason)
- raise exceptions.BadStoreUri()
-
- @property
- 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
- included in the `location` field.
-
- HTTPS is assumed, unless 'swift+http' is specified.
- """
- if self.auth_or_store_url.startswith('http'):
- return self.auth_or_store_url
- else:
- if self.scheme in ('swift+https', 'swift'):
- auth_scheme = 'https://'
- else:
- auth_scheme = 'http://'
-
- return ''.join([auth_scheme, self.auth_or_store_url])
-
-
-def Store(context=None, loc=None):
- if (CONF.swift_store_multi_tenant and
- (loc is None or loc.store_location.user is None)):
- return MultiTenantStore(context, loc)
- return SingleTenantStore(context, loc)
-
-
-class BaseStore(glance.store.driver.Store):
- CHUNKSIZE = 65536
-
- def get_schemes(self):
- return ('swift+https', 'swift', 'swift+http')
-
- def configure(self):
- _obj_size = self._option_get('swift_store_large_object_size')
- self.large_object_size = _obj_size * ONE_MB
- _chunk_size = self._option_get('swift_store_large_object_chunk_size')
- self.large_object_chunk_size = _chunk_size * ONE_MB
- self.admin_tenants = CONF.swift_store_admin_tenants
- self.region = CONF.swift_store_region
- self.service_type = CONF.swift_store_service_type
- self.endpoint_type = CONF.swift_store_endpoint_type
- self.snet = CONF.swift_enable_snet
- self.insecure = CONF.swift_store_auth_insecure
- self.ssl_compression = CONF.swift_store_ssl_compression
-
- def get(self, location, offset=0, chunk_size=None, context=None):
- location = location.store_location
- if not connection:
- connection = self.get_connection(location)
-
- try:
- resp_headers, resp_body = connection.get_object(
- container=location.container, obj=location.obj,
- resp_chunk_size=self.CHUNKSIZE)
- except swiftclient.ClientException as e:
- if e.http_status == httplib.NOT_FOUND:
- msg = _("Swift could not find object %s.") % location.obj
- LOG.warn(msg)
- raise exceptions.NotFound(msg)
- else:
- raise
-
- class ResponseIndexable(glance.store.Indexable):
- def another(self):
- try:
- return self.wrapped.next()
- except StopIteration:
- return ''
-
- length = int(resp_headers.get('content-length', 0))
- return (ResponseIndexable(resp_body, length), length)
-
- def get_size(self, location, connection=None):
- location = location.store_location
- if not connection:
- connection = self.get_connection(location)
- try:
- resp_headers = connection.head_object(
- container=location.container, obj=location.obj)
- return int(resp_headers.get('content-length', 0))
- except Exception:
- return 0
-
- def _option_get(self, param):
- result = getattr(CONF, param)
- if not result:
- reason = (_("Could not find %(param)s in configuration "
- "options.") % {'param': param})
- LOG.error(reason)
- raise exceptions.BadStoreConfiguration(store_name="swift",
- reason=reason)
- return result
-
- def _delete_stale_chunks(self, connection, container, chunk_list):
- for chunk in chunk_list:
- LOG.debug(_("Deleting chunk %s") % chunk)
- try:
- connection.delete_object(container, chunk)
- except Exception:
- msg = _("Failed to delete orphaned chunk %s/%s")
- LOG.exception(msg, container, chunk)
-
- def add(self, image_id, image_file, image_size, connection=None):
- location = self.create_location(image_id)
- if not connection:
- connection = self.get_connection(location)
-
- self._create_container_if_missing(location.container, connection)
-
- LOG.debug(_("Adding image object '%(obj_name)s' "
- "to Swift") % dict(obj_name=location.obj))
- try:
- if image_size > 0 and image_size < self.large_object_size:
- # Image size is known, and is less than large_object_size.
- # Send to Swift with regular PUT.
- obj_etag = connection.put_object(location.container,
- location.obj, image_file,
- content_length=image_size)
- else:
- # Write the image into Swift in chunks.
- chunk_id = 1
- if image_size > 0:
- total_chunks = str(int(
- math.ceil(float(image_size) /
- float(self.large_object_chunk_size))))
- else:
- # image_size == 0 is when we don't know the size
- # of the image. This can occur with older clients
- # that don't inspect the payload size.
- LOG.debug(_("Cannot determine image size. Adding as a "
- "segmented object to Swift."))
- total_chunks = '?'
-
- checksum = hashlib.md5()
- written_chunks = []
- combined_chunks_size = 0
- while True:
- chunk_size = self.large_object_chunk_size
- if image_size == 0:
- content_length = None
- else:
- left = image_size - combined_chunks_size
- if left == 0:
- break
- if chunk_size > left:
- chunk_size = left
- content_length = chunk_size
-
- chunk_name = "%s-%05d" % (location.obj, chunk_id)
- reader = ChunkReader(image_file, checksum, chunk_size)
- try:
- chunk_etag = connection.put_object(
- location.container, chunk_name, reader,
- content_length=content_length)
- written_chunks.append(chunk_name)
- except Exception:
- # Delete orphaned segments from swift backend
- with excutils.save_and_reraise_exception():
- LOG.exception(_("Error during chunked upload to "
- "backend, deleting stale chunks"))
- self._delete_stale_chunks(connection,
- location.container,
- written_chunks)
-
- bytes_read = reader.bytes_read
- msg = (_("Wrote chunk %(chunk_name)s (%(chunk_id)d/"
- "%(total_chunks)s) of length %(bytes_read)d "
- "to Swift returning MD5 of content: "
- "%(chunk_etag)s") %
- {'chunk_name': chunk_name,
- 'chunk_id': chunk_id,
- 'total_chunks': total_chunks,
- 'bytes_read': bytes_read,
- 'chunk_etag': chunk_etag})
- LOG.debug(msg)
-
- if bytes_read == 0:
- # Delete the last chunk, because it's of zero size.
- # This will happen if size == 0.
- LOG.debug(_("Deleting final zero-length chunk"))
- connection.delete_object(location.container,
- chunk_name)
- break
-
- chunk_id += 1
- combined_chunks_size += bytes_read
-
- # In the case we have been given an unknown image size,
- # set the size to the total size of the combined chunks.
- if image_size == 0:
- image_size = combined_chunks_size
-
- # Now we write the object manifest and return the
- # manifest's etag...
- manifest = "%s/%s-" % (location.container, location.obj)
- headers = {'ETag': hashlib.md5("").hexdigest(),
- 'X-Object-Manifest': manifest}
-
- # The ETag returned for the manifest is actually the
- # MD5 hash of the concatenated checksums of the strings
- # of each chunk...so we ignore this result in favour of
- # the MD5 of the entire image file contents, so that
- # users can verify the image file contents accordingly
- connection.put_object(location.container, location.obj,
- None, headers=headers)
- obj_etag = checksum.hexdigest()
-
- # NOTE: We return the user and key here! Have to because
- # location is used by the API server to return the actual
- # image data. We *really* should consider NOT returning
- # the location attribute from GET /images/<ID> and
- # GET /images/details
-
- return (location.get_uri(), image_size, obj_etag, {})
- except swiftclient.ClientException as e:
- if e.http_status == httplib.CONFLICT:
- raise exceptions.Duplicate(_("Swift already has an image at "
- "this location"))
- msg = (_("Failed to add object to Swift.\n"
- "Got error from Swift: %(e)s") % {'e': e})
- LOG.error(msg)
- raise glance.store.BackendException(msg)
-
- def delete(self, location, connection=None):
- location = location.store_location
- if not connection:
- connection = self.get_connection(location)
-
- try:
- # We request the manifest for the object. If one exists,
- # that means the object was uploaded in chunks/segments,
- # and we need to delete all the chunks as well as the
- # manifest.
- manifest = None
- try:
- headers = connection.head_object(
- location.container, location.obj)
- manifest = headers.get('x-object-manifest')
- except swiftclient.ClientException as e:
- if e.http_status != httplib.NOT_FOUND:
- raise
- if manifest:
- # Delete all the chunks before the object manifest itself
- obj_container, obj_prefix = manifest.split('/', 1)
- segments = connection.get_container(
- obj_container, prefix=obj_prefix)[1]
- for segment in segments:
- # TODO(jaypipes): This would be an easy area to parallelize
- # since we're simply sending off parallelizable requests
- # to Swift to delete stuff. It's not like we're going to
- # be hogging up network or file I/O here...
- connection.delete_object(obj_container,
- segment['name'])
-
- # Delete object (or, in segmented case, the manifest)
- connection.delete_object(location.container, location.obj)
-
- except swiftclient.ClientException as e:
- if e.http_status == httplib.NOT_FOUND:
- msg = _("Swift could not find image at URI.")
- raise exceptions.NotFound(msg)
- else:
- raise
-
- def _create_container_if_missing(self, container, connection):
- """
- Creates a missing container in Swift if the
- ``swift_store_create_container_on_put`` option is set.
-
- :param container: Name of container to create
- :param connection: Connection to swift service
- """
- try:
- connection.head_container(container)
- except swiftclient.ClientException as e:
- if e.http_status == httplib.NOT_FOUND:
- if CONF.swift_store_create_container_on_put:
- try:
- connection.put_container(container)
- except swiftclient.ClientException as e:
- msg = (_("Failed to add container to Swift.\n"
- "Got error from Swift: %(e)s") % {'e': e})
- raise glance.store.BackendException(msg)
- else:
- msg = (_("The container %(container)s does not exist in "
- "Swift. Please set the "
- "swift_store_create_container_on_put option"
- "to add container to Swift automatically.") %
- {'container': container})
- raise glance.store.BackendException(msg)
- else:
- raise
-
- def get_connection(self):
- raise NotImplemented()
-
- def create_location(self):
- raise NotImplemented()
-
-
-class SingleTenantStore(BaseStore):
- EXAMPLE_URL = "swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<FILE>"
-
- def configure(self):
- super(SingleTenantStore, self).configure()
- self.auth_version = self._option_get('swift_store_auth_version')
-
- def configure_add(self):
- self.auth_address = self._option_get('swift_store_auth_address')
- if self.auth_address.startswith('http://'):
- self.scheme = 'swift+http'
- else:
- self.scheme = 'swift+https'
- self.container = CONF.swift_store_container
- self.user = self._option_get('swift_store_user')
- self.key = self._option_get('swift_store_key')
-
- def create_location(self, image_id):
- specs = {'scheme': self.scheme,
- 'container': self.container,
- 'obj': str(image_id),
- 'auth_or_store_url': self.auth_address,
- 'user': self.user,
- 'key': self.key}
- return StoreLocation(specs)
-
- def get_connection(self, location):
- if not location.user:
- reason = (_("Location is missing user:password information."))
- LOG.debug(reason)
- raise exceptions.BadStoreUri(message=reason)
-
- auth_url = location.swift_url
- if not auth_url.endswith('/'):
- auth_url += '/'
-
- if self.auth_version == '2':
- try:
- tenant_name, user = location.user.split(':')
- except ValueError:
- reason = (_("Badly formed tenant:user '%(user)s' in "
- "Swift URI") % {'user': location.user})
- LOG.debug(reason)
- raise exceptions.BadStoreUri()
- else:
- tenant_name = None
- user = location.user
-
- os_options = {}
- if self.region:
- os_options['region_name'] = self.region
- os_options['endpoint_type'] = self.endpoint_type
- os_options['service_type'] = self.service_type
-
- return swiftclient.Connection(
- auth_url, user, location.key, insecure=self.insecure,
- tenant_name=tenant_name, snet=self.snet,
- auth_version=self.auth_version, os_options=os_options,
- ssl_compression=self.ssl_compression)
-
-
-class MultiTenantStore(BaseStore):
- EXAMPLE_URL = "swift://<SWIFT_URL>/<CONTAINER>/<FILE>"
-
- def configure_add(self):
- self.container = CONF.swift_store_container
- if self.context is None:
- reason = _("Multi-tenant Swift storage requires a context.")
- raise exceptions.BadStoreConfiguration(store_name="swift",
- reason=reason)
- if self.context.service_catalog is None:
- reason = _("Multi-tenant Swift storage requires "
- "a service catalog.")
- raise exceptions.BadStoreConfiguration(store_name="swift",
- reason=reason)
- self.storage_url = auth.get_endpoint(
- self.context.service_catalog, service_type=self.service_type,
- endpoint_region=self.region, endpoint_type=self.endpoint_type)
- if self.storage_url.startswith('http://'):
- self.scheme = 'swift+http'
- else:
- self.scheme = 'swift+https'
-
- def delete(self, location, connection=None):
- if not connection:
- connection = self.get_connection(location.store_location)
- super(MultiTenantStore, self).delete(location, connection)
- connection.delete_container(location.store_location.container)
-
- def set_acls(self, location, public=False, read_tenants=None,
- write_tenants=None, connection=None):
- location = location.store_location
- if not connection:
- connection = self.get_connection(location)
-
- if read_tenants is None:
- read_tenants = []
- if write_tenants is None:
- write_tenants = []
-
- headers = {}
- if public:
- headers['X-Container-Read'] = ".r:*,.rlistings"
- elif read_tenants:
- headers['X-Container-Read'] = ','.join('%s:*' % i
- for i in read_tenants)
- else:
- headers['X-Container-Read'] = ''
-
- write_tenants.extend(self.admin_tenants)
- if write_tenants:
- headers['X-Container-Write'] = ','.join('%s:*' % i
- for i in write_tenants)
- else:
- headers['X-Container-Write'] = ''
-
- try:
- connection.post_container(location.container, headers=headers)
- except swiftclient.ClientException as e:
- if e.http_status == httplib.NOT_FOUND:
- msg = _("Swift could not find image at URI.")
- raise exceptions.NotFound(msg)
- else:
- raise
-
- def create_location(self, image_id):
- specs = {'scheme': self.scheme,
- 'container': self.container + '_' + str(image_id),
- 'obj': str(image_id),
- 'auth_or_store_url': self.storage_url}
- return StoreLocation(specs)
-
- def get_connection(self, location):
- return swiftclient.Connection(
- None, self.context.user, None,
- preauthurl=location.swift_url,
- preauthtoken=self.context.auth_tok,
- tenant_name=self.context.tenant,
- auth_version='2', snet=self.snet, insecure=self.insecure,
- ssl_compression=self.ssl_compression)
-
-
-class ChunkReader(object):
- def __init__(self, fd, checksum, total):
- self.fd = fd
- self.checksum = checksum
- self.total = total
- self.bytes_read = 0
-
- def read(self, i):
- left = self.total - self.bytes_read
- if i > left:
- i = left
- result = self.fd.read(i)
- self.bytes_read += len(result)
- self.checksum.update(result)
- return result