glance_store/glance_store/multi_backend.py

454 lines
16 KiB
Python

# Copyright 2018 RedHat Inc.
# 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.
import hashlib
import logging
from oslo_config import cfg
from oslo_utils import encodeutils
import six
from stevedore import driver
from stevedore import extension
from glance_store import capabilities
from glance_store import exceptions
from glance_store.i18n import _
from glance_store import location
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
_STORE_OPTS = [
cfg.StrOpt('default_backend',
help=_("""
The default scheme to use for storing images.
Provide a string value representing the default scheme to use for
storing images. If not set, Glance API service will fail to start.
Related Options:
* enabled_backends
""")),
]
_STORE_CFG_GROUP = 'glance_store'
def _list_config_opts():
# NOTE(abhishekk): This separated approach could list
# store options before all driver ones, which easier
# to generate sampe config file.
driver_opts = _list_driver_opts()
sample_opts = [(_STORE_CFG_GROUP, _STORE_OPTS)]
for store_entry in driver_opts:
# NOTE(abhishekk): Do not include no_conf store
if store_entry == "no_conf":
continue
sample_opts.append((store_entry, driver_opts[store_entry]))
return sample_opts
def _list_driver_opts():
driver_opts = {}
mgr = extension.ExtensionManager('glance_store.drivers')
# NOTE(zhiyan): Handle available drivers entry_points provided
# NOTE(nikhil): Return a sorted list of drivers to ensure that the sample
# configuration files generated by oslo config generator retain the order
# in which the config opts appear across different runs. If this order of
# config opts is not preserved, some downstream packagers may see a long
# diff of the changes though not relevant as only order has changed. See
# some more details at bug 1619487.
drivers = sorted([ext.name for ext in mgr])
handled_drivers = [] # Used to handle backwards-compatible entries
for store_entry in drivers:
driver_cls = _load_multi_store(None, store_entry, False)
if driver_cls and driver_cls not in handled_drivers:
if getattr(driver_cls, 'OPTIONS', None) is not None:
driver_opts[store_entry] = driver_cls.OPTIONS
handled_drivers.append(driver_cls)
# NOTE(zhiyan): This separated approach could list
# store options before all driver ones, which easier
# to read and configure by operator.
return driver_opts
def register_store_opts(conf):
LOG.debug("Registering options for group %s", _STORE_CFG_GROUP)
conf.register_opts(_STORE_OPTS, group=_STORE_CFG_GROUP)
driver_opts = _list_driver_opts()
enabled_backends = conf.enabled_backends
for backend in enabled_backends:
for opt_list in driver_opts:
if enabled_backends[backend] not in opt_list:
continue
LOG.debug("Registering options for group %s", backend)
conf.register_opts(driver_opts[opt_list], group=backend)
def _load_multi_store(conf, store_entry,
invoke_load=True,
backend=None):
if backend:
invoke_args = [conf, backend]
else:
invoke_args = [conf]
try:
LOG.debug("Attempting to import store %s", store_entry)
mgr = driver.DriverManager('glance_store.drivers',
store_entry,
invoke_args=invoke_args,
invoke_on_load=invoke_load)
return mgr.driver
except RuntimeError as e:
LOG.warning("Failed to load driver %(driver)s. The "
"driver will be disabled", dict(driver=str([driver, e])))
def _load_multi_stores(conf):
enabled_backends = conf.enabled_backends
for backend, store_entry in enabled_backends.items():
try:
# FIXME(flaper87): Don't hide BadStoreConfiguration
# exceptions. These exceptions should be propagated
# to the user of the library.
store_instance = _load_multi_store(conf, store_entry,
backend=backend)
if not store_instance:
continue
yield (store_entry, store_instance, backend)
except exceptions.BadStoreConfiguration:
continue
def create_multi_stores(conf=CONF):
"""Registers all store modules and all schemes from the given config."""
store_count = 0
scheme_map = {}
for (store_entry, store_instance,
store_identifier) in _load_multi_stores(conf):
try:
schemes = store_instance.get_schemes()
store_instance.configure(re_raise_bsc=False)
except NotImplementedError:
continue
if not schemes:
raise exceptions.BackendException(
_('Unable to register store %s. No schemes associated '
'with it.') % store_entry)
else:
LOG.debug("Registering store %s with schemes %s",
store_entry, schemes)
loc_cls = store_instance.get_store_location_class()
for scheme in schemes:
if scheme not in scheme_map:
scheme_map[scheme] = {}
scheme_map[scheme][store_identifier] = {
'store': store_instance,
'location_class': loc_cls,
'store_entry': store_entry
}
location.register_scheme_backend_map(scheme_map)
store_count += 1
return store_count
def verify_store():
store_id = CONF.glance_store.default_backend
if not store_id:
msg = _("'default_backend' config option is not set.")
raise RuntimeError(msg)
try:
get_store_from_store_identifier(store_id)
except exceptions.UnknownScheme:
msg = _("Store for identifier %s not found") % store_id
raise RuntimeError(msg)
def get_store_from_store_identifier(store_identifier):
"""Determine backing store from identifier.
Given a store identifier, return the appropriate store object
for handling that scheme.
"""
scheme_map = {}
enabled_backends = CONF.enabled_backends
try:
scheme = enabled_backends[store_identifier]
except KeyError:
msg = _("Store for identifier %s not found") % store_identifier
raise exceptions.UnknownScheme(msg)
if scheme not in location.SCHEME_TO_CLS_BACKEND_MAP:
raise exceptions.UnknownScheme(scheme=scheme)
scheme_info = location.SCHEME_TO_CLS_BACKEND_MAP[scheme][store_identifier]
store = scheme_info['store']
if not store.is_capable(capabilities.BitMasks.DRIVER_REUSABLE):
# Driver instance isn't stateless so it can't
# be reused safely and need recreation.
store_entry = scheme_info['store_entry']
store = _load_multi_store(store.conf, store_entry, invoke_load=True,
backend=store_identifier)
store.configure()
try:
loc_cls = store.get_store_location_class()
for new_scheme in store.get_schemes():
if new_scheme not in scheme_map:
scheme_map[new_scheme] = {}
scheme_map[new_scheme][store_identifier] = {
'store': store,
'location_class': loc_cls,
'store_entry': store_entry
}
location.register_scheme_backend_map(scheme_map)
except NotImplementedError:
scheme_info['store'] = store
return store
def add(conf, image_id, data, size, backend, context=None,
verifier=None):
if not backend:
backend = conf.glance_store.default_backend
store = get_store_from_store_identifier(backend)
return store_add_to_backend(image_id, data, size, store, context,
verifier)
def add_with_multihash(conf, image_id, data, size, backend, hashing_algo,
scheme=None, context=None, verifier=None):
if not backend:
backend = conf.glance_store.default_backend
store = get_store_from_store_identifier(backend)
return store_add_to_backend_with_multihash(
image_id, data, size, hashing_algo, store, context, verifier)
def _check_metadata(store, metadata):
if not isinstance(metadata, dict):
msg = (_("The storage driver %(driver)s returned invalid "
" metadata %(metadata)s. This must be a dictionary type")
% dict(driver=str(store), metadata=str(metadata)))
LOG.error(msg)
raise exceptions.BackendException(msg)
try:
check_location_metadata(metadata)
except exceptions.BackendException as e:
e_msg = (_("A bad metadata structure was returned from the "
"%(driver)s storage driver: %(metadata)s. %(e)s.") %
dict(driver=encodeutils.exception_to_unicode(store),
metadata=encodeutils.exception_to_unicode(metadata),
e=encodeutils.exception_to_unicode(e)))
LOG.error(e_msg)
raise exceptions.BackendException(e_msg)
def store_add_to_backend(image_id, data, size, store, context=None,
verifier=None):
"""A wrapper around a call to each stores add() method.
This gives glance a common place to check the output.
:param image_id: The image add to which data is added
:param data: The data to be stored
:param size: The length of the data in bytes
:param store: The store to which the data is being added
:param context: The request context
:param verifier: An object used to verify signatures for images
:param backend: Name of the backend to store the image
:return: The url location of the file,
the size amount of data,
the checksum of the data
the storage systems metadata dictionary for the location
"""
(location, size, checksum, metadata) = store.add(image_id,
data,
size,
context=context,
verifier=verifier)
if metadata is not None:
_check_metadata(store, metadata)
return (location, size, checksum, metadata)
def store_add_to_backend_with_multihash(
image_id, data, size, hashing_algo, store,
context=None, verifier=None):
"""
A wrapper around a call to each store's add() method that requires
a hashing_algo identifier and returns a 5-tuple including the
"multihash" computed using the specified hashing_algo. (This
is an enhanced version of store_add_to_backend(), which is left
as-is for backward compatibility.)
:param image_id: The image add to which data is added
:param data: The data to be stored
:param size: The length of the data in bytes
:param store: The store to which the data is being added
:param hashing_algo: A hashlib algorithm identifier (string)
:param context: The request context
:param verifier: An object used to verify signatures for images
:return: The url location of the file,
the size amount of data,
the checksum of the data,
the multihash of the data,
the storage system's metadata dictionary for the location
:raises: ``glance_store.exceptions.BackendException``
``glance_store.exceptions.UnknownHashingAlgo``
"""
if hashing_algo not in hashlib.algorithms_available:
raise exceptions.UnknownHashingAlgo(algo=hashing_algo)
(location, size, checksum, multihash, metadata) = store.add(
image_id, data, size, hashing_algo, context=context, verifier=verifier)
if metadata is not None:
_check_metadata(store, metadata)
return (location, size, checksum, multihash, metadata)
def check_location_metadata(val, key=''):
if isinstance(val, dict):
for key in val:
check_location_metadata(val[key], key=key)
elif isinstance(val, list):
ndx = 0
for v in val:
check_location_metadata(v, key='%s[%d]' % (key, ndx))
ndx = ndx + 1
elif not isinstance(val, six.text_type):
raise exceptions.BackendException(_("The image metadata key %(key)s "
"has an invalid type of %(type)s. "
"Only dict, list, and unicode are "
"supported.")
% dict(key=key, type=type(val)))
def delete(uri, backend, context=None):
"""Removes chunks of data from backend specified by uri."""
if backend:
loc = location.get_location_from_uri_and_backend(
uri, backend, conf=CONF)
store = get_store_from_store_identifier(backend)
return store.delete(loc, context=context)
LOG.warning('Backend is not set to image, searching all backends based on '
'location URI.')
backends = CONF.enabled_backends
for backend in backends:
try:
if not uri.startswith(backends[backend]):
continue
loc = location.get_location_from_uri_and_backend(
uri, backend, conf=CONF)
store = get_store_from_store_identifier(backend)
return store.delete(loc, context=context)
except (exceptions.NotFound, exceptions.UnknownScheme):
continue
raise exceptions.NotFound(_("Image not found in any configured backend"))
def set_acls_for_multi_store(location_uri, backend, public=False,
read_tenants=[],
write_tenants=None, context=None):
if write_tenants is None:
write_tenants = []
loc = location.get_location_from_uri_and_backend(
location_uri, backend, conf=CONF)
store = get_store_from_store_identifier(backend)
try:
store.set_acls(loc, public=public,
read_tenants=read_tenants,
write_tenants=write_tenants,
context=context)
except NotImplementedError:
LOG.debug("Skipping store.set_acls... not implemented")
def get(uri, backend, offset=0, chunk_size=None, context=None):
"""Yields chunks of data from backend specified by uri."""
if backend:
loc = location.get_location_from_uri_and_backend(uri, backend,
conf=CONF)
store = get_store_from_store_identifier(backend)
return store.get(loc, offset=offset,
chunk_size=chunk_size,
context=context)
LOG.warning('Backend is not set to image, searching all backends based on '
'location URI.')
backends = CONF.enabled_backends
for backend in backends:
try:
if not uri.startswith(backends[backend]):
continue
loc = location.get_location_from_uri_and_backend(
uri, backend, conf=CONF)
store = get_store_from_store_identifier(backend)
data, size = store.get(loc, offset=offset,
chunk_size=chunk_size,
context=context)
if data:
return data, size
except (exceptions.NotFound, exceptions.UnknownScheme):
continue
raise exceptions.NotFound(_("Image not found in any configured backend"))
def get_known_schemes_for_multi_store():
"""Returns list of known schemes."""
return location.SCHEME_TO_CLS_BACKEND_MAP.keys()
def get_size_from_uri_and_backend(uri, backend, context=None):
"""Retrieves image size from backend specified by uri."""
loc = location.get_location_from_uri_and_backend(
uri, backend, conf=CONF)
store = get_store_from_store_identifier(backend)
return store.get_size(loc, context=context)