da494d4abb
In Rocky we have added support for multiple backend as a EXPERIMENTAL feature. However configuration options related to multiple backend are not generated in sample config file due to some issue. We have added below 2 new config options for multiple backend. 1. enabled_backneds (added in glance) 2. default_backend (added in glance_store) Made provision to add option 2 from above to sample config file. Change-Id: I63571e4a8f85003e304f16653d60cbd38e6b6bde Partial-Bug: #1793057
504 lines
18 KiB
Python
504 lines
18 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
|
|
|
|
""")),
|
|
cfg.IntOpt('store_capabilities_update_min_interval',
|
|
default=0,
|
|
min=0,
|
|
deprecated_for_removal=True,
|
|
deprecated_since='Rocky',
|
|
deprecated_reason=_("""
|
|
This option configures a stub method that has not been implemented
|
|
for any existing store drivers. Hence it is non-operational, and
|
|
giving it a value does absolutely nothing.
|
|
|
|
This option is scheduled for removal early in the Stein development
|
|
cycle.
|
|
"""),
|
|
help=_("""
|
|
Minimum interval in seconds to execute updating dynamic storage
|
|
capabilities based on current backend status.
|
|
|
|
Provide an integer value representing time in seconds to set the
|
|
minimum interval before an update of dynamic storage capabilities
|
|
for a storage backend can be attempted. Setting
|
|
``store_capabilities_update_min_interval`` does not mean updates
|
|
occur periodically based on the set interval. Rather, the update
|
|
is performed at the elapse of this interval set, if an operation
|
|
of the store is triggered.
|
|
|
|
By default, this option is set to zero and is disabled. Provide an
|
|
integer value greater than zero to enable this option.
|
|
|
|
NOTE 1: For more information on store capabilities and their updates,
|
|
please visit: https://specs.openstack.org/openstack/glance-specs/\
|
|
specs/kilo/store-capabilities.html
|
|
|
|
For more information on setting up a particular store in your
|
|
deployment and help with the usage of this feature, please contact
|
|
the storage driver maintainers listed here:
|
|
https://docs.openstack.org/glance_store/latest/user/drivers.html
|
|
|
|
NOTE 2: The dynamic store update capability described above is not
|
|
implemented by any current store drivers. Thus, this option DOES
|
|
NOT DO ANYTHING (and it never has). It is DEPRECATED and scheduled
|
|
for removal early in the Stein development cycle.
|
|
|
|
Possible values:
|
|
* Zero
|
|
* Positive integer
|
|
|
|
Related Options:
|
|
* None
|
|
|
|
""")),
|
|
]
|
|
|
|
_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)
|