970 lines
41 KiB
Python
970 lines
41 KiB
Python
# 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"""
|
|
|
|
import hashlib
|
|
import logging
|
|
import math
|
|
|
|
from keystoneclient import service_catalog as keystone_sc
|
|
from oslo_config import cfg
|
|
from oslo_utils import encodeutils
|
|
from oslo_utils import excutils
|
|
from oslo_utils import units
|
|
import six
|
|
from six.moves import http_client
|
|
from six.moves import urllib
|
|
try:
|
|
import swiftclient
|
|
except ImportError:
|
|
swiftclient = None
|
|
|
|
import glance_store
|
|
from glance_store._drivers.swift import utils as sutils
|
|
from glance_store import capabilities
|
|
from glance_store import driver
|
|
from glance_store import exceptions
|
|
from glance_store import i18n
|
|
from glance_store import location
|
|
|
|
|
|
_ = i18n._
|
|
LOG = logging.getLogger(__name__)
|
|
_LI = i18n._LI
|
|
|
|
DEFAULT_CONTAINER = 'glance'
|
|
DEFAULT_LARGE_OBJECT_SIZE = 5 * units.Ki # 5GB
|
|
DEFAULT_LARGE_OBJECT_CHUNK_SIZE = 200 # 200M
|
|
ONE_MB = units.k * units.Ki # Here we used the mixed meaning of MB
|
|
|
|
_SWIFT_OPTS = [
|
|
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_cacert',
|
|
help=_('A string giving the CA certificate file to use in '
|
|
'SSL connections for verifying certs.')),
|
|
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',
|
|
help=_('If set, the configured endpoint will be used. If '
|
|
'None, the storage url from the auth response will be '
|
|
'used.')),
|
|
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 when using single '
|
|
'container mode. In multiple container mode, this will '
|
|
'be the prefix for all containers.')),
|
|
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.IntOpt('swift_store_multiple_containers_seed',
|
|
default=0,
|
|
help=_('When set to 0, a single-tenant store will only use one '
|
|
'container to store all images. When set to an integer '
|
|
'value between 1 and 32, a single-tenant store will use '
|
|
'multiple containers to store images, and this value '
|
|
'will determine how many containers are created.'
|
|
'Used only when swift_store_multi_tenant is disabled. '
|
|
'The total number of containers that will be used is '
|
|
'equal to 16^N, so if this config option is set to 2, '
|
|
'then 16^2=256 containers will be used to store images.'
|
|
)),
|
|
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.')),
|
|
cfg.IntOpt('swift_store_retry_get_count', default=0,
|
|
help=_('The number of times a Swift download will be retried '
|
|
'before the request fails.'))
|
|
]
|
|
|
|
|
|
def swift_retry_iter(resp_iter, length, store, location, context):
|
|
if not length and isinstance(resp_iter, six.BytesIO):
|
|
if six.PY3:
|
|
# On Python 3, io.BytesIO does not have a len attribute, instead
|
|
# go the end using seek to get the size of the file
|
|
pos = resp_iter.tell()
|
|
resp_iter.seek(0, 2)
|
|
length = resp_iter.tell()
|
|
resp_iter.seek(pos)
|
|
else:
|
|
# On Python 2, StringIO has a len attribute
|
|
length = resp_iter.len
|
|
|
|
length = length if length else (resp_iter.len
|
|
if hasattr(resp_iter, 'len') else 0)
|
|
retries = 0
|
|
bytes_read = 0
|
|
|
|
while retries <= store.conf.glance_store.swift_store_retry_get_count:
|
|
try:
|
|
for chunk in resp_iter:
|
|
yield chunk
|
|
bytes_read += len(chunk)
|
|
except swiftclient.ClientException as e:
|
|
LOG.warn(_("Swift exception raised %s")
|
|
% encodeutils.exception_to_unicode(e))
|
|
|
|
if bytes_read != length:
|
|
if retries == store.conf.glance_store.swift_store_retry_get_count:
|
|
# terminate silently and let higher level decide
|
|
LOG.error(_("Stopping Swift retries after %d "
|
|
"attempts") % retries)
|
|
break
|
|
else:
|
|
retries += 1
|
|
glance_conf = store.conf.glance_store
|
|
retry_count = glance_conf.swift_store_retry_get_count
|
|
LOG.info(_("Retrying Swift connection "
|
|
"(%(retries)d/%(max_retries)d) with "
|
|
"range=%(start)d-%(end)d") %
|
|
{'retries': retries,
|
|
'max_retries': retry_count,
|
|
'start': bytes_read,
|
|
'end': length})
|
|
(_resp_headers, resp_iter) = store._get_object(location, None,
|
|
bytes_read,
|
|
context=context)
|
|
else:
|
|
break
|
|
|
|
|
|
class StoreLocation(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.parse.quote(self.user),
|
|
urllib.parse.quote(self.key))
|
|
return ''
|
|
|
|
def get_uri(self, credentials_included=True):
|
|
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('/')
|
|
|
|
if not credentials_included:
|
|
# Used only in case of an add
|
|
# Get the current store from config
|
|
store = self.conf.glance_store.default_swift_reference
|
|
|
|
return '%s://%s/%s/%s' % ('swift+config', store, container, obj)
|
|
if self.scheme == 'swift+config':
|
|
if self.ssl_enabled:
|
|
self.scheme = 'swift+https'
|
|
else:
|
|
self.scheme = 'swift+http'
|
|
if credstring != '':
|
|
credstring = "%s@" % credstring
|
|
return '%s://%s%s/%s/%s' % (self.scheme, credstring, auth_or_store_url,
|
|
container, obj)
|
|
|
|
def _get_conf_value_from_account_ref(self, netloc):
|
|
try:
|
|
ref_params = sutils.SwiftParams(self.conf).params
|
|
self.user = ref_params[netloc]['user']
|
|
self.key = ref_params[netloc]['key']
|
|
netloc = ref_params[netloc]['auth_address']
|
|
self.ssl_enabled = True
|
|
if netloc != '':
|
|
if netloc.startswith('http://'):
|
|
self.ssl_enabled = False
|
|
netloc = netloc[len('http://'):]
|
|
elif netloc.startswith('https://'):
|
|
netloc = netloc[len('https://'):]
|
|
except KeyError:
|
|
reason = _("Badly formed Swift URI. Credentials not found for "
|
|
"account reference")
|
|
LOG.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
return netloc
|
|
|
|
def _form_uri_parts(self, netloc, path):
|
|
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.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
key = cred_parts.pop()
|
|
user = ':'.join(cred_parts)
|
|
creds = urllib.parse.unquote(creds)
|
|
try:
|
|
self.user, self.key = creds.rsplit(':', 1)
|
|
except exceptions.BadStoreConfiguration:
|
|
self.user = urllib.parse.unquote(user)
|
|
self.key = urllib.parse.unquote(key)
|
|
else:
|
|
self.user = None
|
|
self.key = None
|
|
return netloc, path
|
|
|
|
def _form_auth_or_store_url(self, netloc, path):
|
|
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.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
|
|
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
|
|
and for system created locations with account reference
|
|
swift+config://account_reference/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.info(_LI("Invalid store URI: %(reason)s"), {'reason': reason})
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
|
|
pieces = urllib.parse.urlparse(uri)
|
|
assert pieces.scheme in ('swift', 'swift+http', 'swift+https',
|
|
'swift+config')
|
|
|
|
self.scheme = pieces.scheme
|
|
netloc = pieces.netloc
|
|
path = pieces.path.lstrip('/')
|
|
|
|
# NOTE(Sridevi): Fix to map the account reference to the
|
|
# corresponding configuration value
|
|
if self.scheme == 'swift+config':
|
|
netloc = self._get_conf_value_from_account_ref(netloc)
|
|
else:
|
|
netloc, path = self._form_uri_parts(netloc, path)
|
|
|
|
self._form_auth_or_store_url(netloc, path)
|
|
|
|
@property
|
|
def swift_url(self):
|
|
"""
|
|
Creates a fully-qualified auth address that the Swift client library
|
|
can use. The scheme for the auth_address 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 == 'swift+config':
|
|
if self.ssl_enabled:
|
|
self.scheme = 'swift+https'
|
|
else:
|
|
self.scheme = 'swift+http'
|
|
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(conf):
|
|
try:
|
|
conf.register_opts(_SWIFT_OPTS + sutils.swift_opts,
|
|
group='glance_store')
|
|
except cfg.DuplicateOptError:
|
|
pass
|
|
|
|
if conf.glance_store.swift_store_multi_tenant:
|
|
return MultiTenantStore(conf)
|
|
return SingleTenantStore(conf)
|
|
|
|
Store.OPTIONS = _SWIFT_OPTS + sutils.swift_opts
|
|
|
|
|
|
def _is_slo(slo_header):
|
|
if (slo_header is not None and isinstance(slo_header, six.string_types)
|
|
and slo_header.lower() == 'true'):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class BaseStore(driver.Store):
|
|
|
|
_CAPABILITIES = capabilities.BitMasks.RW_ACCESS
|
|
CHUNKSIZE = 65536
|
|
OPTIONS = _SWIFT_OPTS + sutils.swift_opts
|
|
|
|
def get_schemes(self):
|
|
return ('swift+https', 'swift', 'swift+http', 'swift+config')
|
|
|
|
def configure(self, re_raise_bsc=False):
|
|
glance_conf = self.conf.glance_store
|
|
_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 = glance_conf.swift_store_admin_tenants
|
|
self.region = glance_conf.swift_store_region
|
|
self.service_type = glance_conf.swift_store_service_type
|
|
self.conf_endpoint = glance_conf.swift_store_endpoint
|
|
self.endpoint_type = glance_conf.swift_store_endpoint_type
|
|
self.insecure = glance_conf.swift_store_auth_insecure
|
|
self.ssl_compression = glance_conf.swift_store_ssl_compression
|
|
self.cacert = glance_conf.swift_store_cacert
|
|
if swiftclient is None:
|
|
msg = _("Missing dependency python_swiftclient.")
|
|
raise exceptions.BadStoreConfiguration(store_name="swift",
|
|
reason=msg)
|
|
super(BaseStore, self).configure(re_raise_bsc=re_raise_bsc)
|
|
|
|
def _get_object(self, location, connection=None, start=None, context=None):
|
|
if not connection:
|
|
connection = self.get_connection(location, context=context)
|
|
headers = {}
|
|
if start is not None:
|
|
bytes_range = 'bytes=%d-' % start
|
|
headers = {'Range': bytes_range}
|
|
|
|
try:
|
|
resp_headers, resp_body = connection.get_object(
|
|
location.container, location.obj,
|
|
resp_chunk_size=self.CHUNKSIZE, headers=headers)
|
|
except swiftclient.ClientException as e:
|
|
if e.http_status == http_client.NOT_FOUND:
|
|
msg = _("Swift could not find object %s.") % location.obj
|
|
LOG.warn(msg)
|
|
raise exceptions.NotFound(message=msg)
|
|
else:
|
|
raise
|
|
|
|
return (resp_headers, resp_body)
|
|
|
|
@capabilities.check
|
|
def get(self, location, connection=None,
|
|
offset=0, chunk_size=None, context=None):
|
|
location = location.store_location
|
|
(resp_headers, resp_body) = self._get_object(location, connection,
|
|
context=context)
|
|
|
|
class ResponseIndexable(glance_store.Indexable):
|
|
def another(self):
|
|
try:
|
|
return next(self.wrapped)
|
|
except StopIteration:
|
|
return ''
|
|
|
|
length = int(resp_headers.get('content-length', 0))
|
|
if self.conf.glance_store.swift_store_retry_get_count > 0:
|
|
resp_body = swift_retry_iter(resp_body, length,
|
|
self, location, context)
|
|
return (ResponseIndexable(resp_body, length), length)
|
|
|
|
def get_size(self, location, connection=None, context=None):
|
|
location = location.store_location
|
|
if not connection:
|
|
connection = self.get_connection(location, context=context)
|
|
try:
|
|
resp_headers = connection.head_object(
|
|
location.container, location.obj)
|
|
return int(resp_headers.get('content-length', 0))
|
|
except Exception:
|
|
return 0
|
|
|
|
def _option_get(self, param):
|
|
result = getattr(self.conf.glance_store, param)
|
|
if not result:
|
|
reason = (_("Could not find %(param)s in configuration options.")
|
|
% 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 "
|
|
"%(container)s/%(chunk)s")
|
|
LOG.exception(msg % {'container': container,
|
|
'chunk': chunk})
|
|
|
|
@capabilities.check
|
|
def add(self, image_id, image_file, image_size,
|
|
connection=None, context=None, verifier=None):
|
|
location = self.create_location(image_id, context=context)
|
|
if not connection:
|
|
connection = self.get_connection(location, context=context)
|
|
|
|
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,
|
|
verifier)
|
|
try:
|
|
chunk_etag = connection.put_object(
|
|
location.container, chunk_name, reader,
|
|
content_length=content_length)
|
|
written_chunks.append(chunk_name)
|
|
except exceptions.ZeroSizeChunk:
|
|
LOG.debug('Not writing zero-length chunk')
|
|
break
|
|
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)
|
|
|
|
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(b"").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
|
|
if sutils.is_multiple_swift_store_accounts_enabled(self.conf):
|
|
include_creds = False
|
|
else:
|
|
include_creds = True
|
|
|
|
return (location.get_uri(credentials_included=include_creds),
|
|
image_size, obj_etag, {})
|
|
except swiftclient.ClientException as e:
|
|
if e.http_status == http_client.CONFLICT:
|
|
msg = _("Swift already has an image at this location")
|
|
raise exceptions.Duplicate(message=msg)
|
|
|
|
msg = (_(u"Failed to add object to Swift.\n"
|
|
"Got error from Swift: %s.")
|
|
% encodeutils.exception_to_unicode(e))
|
|
LOG.error(msg)
|
|
raise glance_store.BackendException(msg)
|
|
|
|
@capabilities.check
|
|
def delete(self, location, connection=None, context=None):
|
|
location = location.store_location
|
|
if not connection:
|
|
connection = self.get_connection(location, context=context)
|
|
|
|
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.
|
|
dlo_manifest = None
|
|
slo_manifest = None
|
|
try:
|
|
headers = connection.head_object(
|
|
location.container, location.obj)
|
|
dlo_manifest = headers.get('x-object-manifest')
|
|
slo_manifest = headers.get('x-static-large-object')
|
|
except swiftclient.ClientException as e:
|
|
if e.http_status != http_client.NOT_FOUND:
|
|
raise
|
|
|
|
if _is_slo(slo_manifest):
|
|
# Delete the manifest as well as the segments
|
|
query_string = 'multipart-manifest=delete'
|
|
connection.delete_object(location.container, location.obj,
|
|
query_string=query_string)
|
|
return
|
|
|
|
if dlo_manifest:
|
|
# Delete all the chunks before the object manifest itself
|
|
obj_container, obj_prefix = dlo_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...
|
|
try:
|
|
connection.delete_object(obj_container,
|
|
segment['name'])
|
|
except swiftclient.ClientException as e:
|
|
msg = _('Unable to delete segment %(segment_name)s')
|
|
msg = msg % {'segment_name': segment['name']}
|
|
LOG.exception(msg)
|
|
|
|
# Delete object (or, in segmented case, the manifest)
|
|
connection.delete_object(location.container, location.obj)
|
|
|
|
except swiftclient.ClientException as e:
|
|
if e.http_status == http_client.NOT_FOUND:
|
|
msg = _("Swift could not find image at URI.")
|
|
raise exceptions.NotFound(message=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 == http_client.NOT_FOUND:
|
|
if self.conf.glance_store.swift_store_create_container_on_put:
|
|
try:
|
|
msg = (_LI("Creating swift container %(container)s") %
|
|
{'container': container})
|
|
LOG.info(msg)
|
|
connection.put_container(container)
|
|
except swiftclient.ClientException as e:
|
|
msg = (_("Failed to add container to Swift.\n"
|
|
"Got error from Swift: %s.")
|
|
% encodeutils.exception_to_unicode(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, location, context=None):
|
|
raise NotImplementedError()
|
|
|
|
def create_location(self, image_id, context=None):
|
|
raise NotImplementedError()
|
|
|
|
|
|
class SingleTenantStore(BaseStore):
|
|
EXAMPLE_URL = "swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<FILE>"
|
|
|
|
def __init__(self, conf):
|
|
super(SingleTenantStore, self).__init__(conf)
|
|
self.ref_params = sutils.SwiftParams(self.conf).params
|
|
|
|
def configure(self, re_raise_bsc=False):
|
|
# set configuration before super so configure_add can override
|
|
self.auth_version = self._option_get('swift_store_auth_version')
|
|
self.user_domain_id = None
|
|
self.user_domain_name = None
|
|
self.project_domain_id = None
|
|
self.project_domain_name = None
|
|
|
|
super(SingleTenantStore, self).configure(re_raise_bsc=re_raise_bsc)
|
|
|
|
def configure_add(self):
|
|
default_ref = self.conf.glance_store.default_swift_reference
|
|
default_swift_reference = self.ref_params.get(default_ref)
|
|
if default_swift_reference:
|
|
self.auth_address = default_swift_reference.get('auth_address')
|
|
if (not default_swift_reference) or (not self.auth_address):
|
|
reason = _("A value for swift_store_auth_address is required.")
|
|
LOG.error(reason)
|
|
raise exceptions.BadStoreConfiguration(message=reason)
|
|
|
|
if self.auth_address.startswith('http://'):
|
|
self.scheme = 'swift+http'
|
|
else:
|
|
self.scheme = 'swift+https'
|
|
self.container = self.conf.glance_store.swift_store_container
|
|
self.auth_version = default_swift_reference.get('auth_version')
|
|
self.user = default_swift_reference.get('user')
|
|
self.key = default_swift_reference.get('key')
|
|
self.user_domain_id = default_swift_reference.get('user_domain_id')
|
|
self.user_domain_name = default_swift_reference.get('user_domain_name')
|
|
self.project_domain_id = default_swift_reference.get(
|
|
'project_domain_id')
|
|
self.project_domain_name = default_swift_reference.get(
|
|
'project_domain_name')
|
|
|
|
if not (self.user or self.key):
|
|
reason = _("A value for swift_store_ref_params is required.")
|
|
LOG.error(reason)
|
|
raise exceptions.BadStoreConfiguration(store_name="swift",
|
|
reason=reason)
|
|
|
|
def create_location(self, image_id, context=None):
|
|
container_name = self.get_container_name(image_id, self.container)
|
|
specs = {'scheme': self.scheme,
|
|
'container': container_name,
|
|
'obj': str(image_id),
|
|
'auth_or_store_url': self.auth_address,
|
|
'user': self.user,
|
|
'key': self.key}
|
|
return StoreLocation(specs, self.conf)
|
|
|
|
def get_container_name(self, image_id, default_image_container):
|
|
"""
|
|
Returns appropriate container name depending upon value of
|
|
``swift_store_multiple_containers_seed``. In single-container mode,
|
|
which is a seed value of 0, simply returns default_image_container.
|
|
In multiple-container mode, returns default_image_container as the
|
|
prefix plus a suffix determined by the multiple container seed
|
|
|
|
examples:
|
|
single-container mode: 'glance'
|
|
multiple-container mode: 'glance_3a1' for image uuid 3A1xxxxxxx...
|
|
|
|
:param image_id: UUID of image
|
|
:param default_image_container: container name from
|
|
``swift_store_container``
|
|
"""
|
|
seed_num_chars = \
|
|
self.conf.glance_store.swift_store_multiple_containers_seed
|
|
if seed_num_chars is None \
|
|
or seed_num_chars < 0 or seed_num_chars > 32:
|
|
reason = _("An integer value between 0 and 32 is required for"
|
|
" swift_store_multiple_containers_seed.")
|
|
LOG.error(reason)
|
|
raise exceptions.BadStoreConfiguration(store_name="swift",
|
|
reason=reason)
|
|
elif seed_num_chars > 0:
|
|
image_id = str(image_id).lower()
|
|
|
|
num_dashes = image_id[:seed_num_chars].count('-')
|
|
num_chars = seed_num_chars + num_dashes
|
|
name_suffix = image_id[:num_chars]
|
|
new_container_name = default_image_container + '_' + name_suffix
|
|
return new_container_name
|
|
else:
|
|
return default_image_container
|
|
|
|
def get_connection(self, location, context=None):
|
|
if not location.user:
|
|
reason = _("Location is missing user:password information.")
|
|
LOG.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
|
|
auth_url = location.swift_url
|
|
if not auth_url.endswith('/'):
|
|
auth_url += '/'
|
|
|
|
if self.auth_version in ('2', '3'):
|
|
try:
|
|
tenant_name, user = location.user.split(':')
|
|
except ValueError:
|
|
reason = (_("Badly formed tenant:user '%(user)s' in "
|
|
"Swift URI") % {'user': location.user})
|
|
LOG.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
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
|
|
if self.user_domain_id:
|
|
os_options['user_domain_id'] = self.user_domain_id
|
|
if self.user_domain_name:
|
|
os_options['user_domain_name'] = self.user_domain_name
|
|
if self.project_domain_id:
|
|
os_options['project_domain_id'] = self.project_domain_id
|
|
if self.project_domain_name:
|
|
os_options['project_domain_name'] = self.project_domain_name
|
|
|
|
return swiftclient.Connection(
|
|
auth_url, user, location.key, preauthurl=self.conf_endpoint,
|
|
insecure=self.insecure, tenant_name=tenant_name,
|
|
auth_version=self.auth_version, os_options=os_options,
|
|
ssl_compression=self.ssl_compression, cacert=self.cacert)
|
|
|
|
|
|
class MultiTenantStore(BaseStore):
|
|
EXAMPLE_URL = "swift://<SWIFT_URL>/<CONTAINER>/<FILE>"
|
|
|
|
def _get_endpoint(self, context):
|
|
self.container = self.conf.glance_store.swift_store_container
|
|
if context is None:
|
|
reason = _("Multi-tenant Swift storage requires a context.")
|
|
raise exceptions.BadStoreConfiguration(store_name="swift",
|
|
reason=reason)
|
|
if 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 = self.conf_endpoint
|
|
if not self.storage_url:
|
|
sc = {'serviceCatalog': context.service_catalog}
|
|
self.storage_url = keystone_sc.ServiceCatalogV2(sc).url_for(
|
|
service_type=self.service_type, region_name=self.region,
|
|
endpoint_type=self.endpoint_type)
|
|
|
|
if self.storage_url.startswith('http://'):
|
|
self.scheme = 'swift+http'
|
|
else:
|
|
self.scheme = 'swift+https'
|
|
|
|
return self.storage_url
|
|
|
|
def delete(self, location, connection=None, context=None):
|
|
if not connection:
|
|
connection = self.get_connection(location.store_location,
|
|
context=context)
|
|
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, context=None):
|
|
location = location.store_location
|
|
if not connection:
|
|
connection = self.get_connection(location, context=context)
|
|
|
|
if read_tenants is None:
|
|
read_tenants = []
|
|
if write_tenants is None:
|
|
write_tenants = []
|
|
|
|
headers = {}
|
|
if public:
|
|
headers['X-Container-Read'] = "*:*"
|
|
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 == http_client.NOT_FOUND:
|
|
msg = _("Swift could not find image at URI.")
|
|
raise exceptions.NotFound(message=msg)
|
|
else:
|
|
raise
|
|
|
|
def create_location(self, image_id, context=None):
|
|
ep = self._get_endpoint(context)
|
|
specs = {'scheme': self.scheme,
|
|
'container': self.container + '_' + str(image_id),
|
|
'obj': str(image_id),
|
|
'auth_or_store_url': ep}
|
|
return StoreLocation(specs, self.conf)
|
|
|
|
def get_connection(self, location, context=None):
|
|
return swiftclient.Connection(
|
|
None, context.user, None,
|
|
preauthurl=location.swift_url,
|
|
preauthtoken=context.auth_token,
|
|
tenant_name=context.tenant,
|
|
auth_version='2', insecure=self.insecure,
|
|
ssl_compression=self.ssl_compression,
|
|
cacert=self.cacert)
|
|
|
|
|
|
class ChunkReader(object):
|
|
def __init__(self, fd, checksum, total, verifier=None):
|
|
self.fd = fd
|
|
self.checksum = checksum
|
|
self.total = total
|
|
self.verifier = verifier
|
|
self.bytes_read = 0
|
|
|
|
def read(self, i):
|
|
left = self.total - self.bytes_read
|
|
if i > left:
|
|
i = left
|
|
result = self.fd.read(i)
|
|
if len(result) == 0 and self.bytes_read == 0:
|
|
# fd was empty
|
|
raise exceptions.ZeroSizeChunk()
|
|
self.bytes_read += len(result)
|
|
self.checksum.update(result)
|
|
if self.verifier:
|
|
self.verifier.update(result)
|
|
return result
|