Add support for backend_defaults group

With the introduction of multi store support, we can configure
multiple stores of same or different type.
Along with multiple upsides, there is a downside that the config
options need to be repeated multiple times even though we want
to pass the same value. Take the following example:

[DEFAULT]
enabled_backends=lvmdriver-1:cinder, lvmdriver-2:cinder

[lvmdriver-1]
cinder_use_multipath=True
cinder_volume_type=lvmdriver-1

[lvmdriver-2]
cinder_use_multipath=True
cinder_volume_type=lvmdriver-2

Since we generally want multiple to be enabled for cinder backends
that support it, if we configure 10 iSCSI/FC cinder backends, we
need to specify the config option for all of them.

To mitigate the issue, we introduce a new [backend_defaults] section
that would allow us to configure the common config options at a
single place. Rewriting the above example with [backend_defaults]:

[DEFAULT]
enabled_backends=lvmdriver-1:cinder, lvmdriver-2:cinder

[backend_defaults]
cinder_use_multipath=True

[lvmdriver-1]
cinder_volume_type=lvmdriver-1

[lvmdriver-2]
cinder_volume_type=lvmdriver-2

Change-Id: I88859cdb979c66c807fef09b7e3e36b70877163b
This commit is contained in:
Rajat Dhasmana
2025-04-06 17:17:59 +05:30
parent 8afd981fba
commit 3578b4da61
10 changed files with 172 additions and 6 deletions

View File

@@ -493,7 +493,8 @@ class Store(glance_store.driver.Store):
self.mount = importlib.import_module('glance_store.common.fs_mount')
self._set_url_prefix()
if self.backend_group:
self.store_conf = getattr(self.conf, self.backend_group)
self.store_conf = glance_store.driver.BackendGroupConfiguration(
self.OPTIONS, self.backend_group, conf=self.conf)
else:
self.store_conf = self.conf.glance_store
self.volume_api = cinder_utils.API()

View File

@@ -288,7 +288,8 @@ class Store(glance_store.driver.Store):
def __init__(self, *args, **kargs):
super(Store, self).__init__(*args, **kargs)
if self.backend_group:
self.store_conf = getattr(self.conf, self.backend_group)
self.store_conf = glance_store.driver.BackendGroupConfiguration(
self.OPTIONS, self.backend_group, conf=self.conf)
else:
self.store_conf = self.conf.glance_store

View File

@@ -339,7 +339,8 @@ class Store(glance_store.driver.Store):
self.session = requests.Session()
if self.backend_group:
store_conf = getattr(self.conf, self.backend_group)
store_conf = glance_store.driver.BackendGroupConfiguration(
self.OPTIONS, self.backend_group, conf=self.conf)
else:
store_conf = self.conf.glance_store

View File

@@ -285,7 +285,8 @@ class Store(driver.Store):
def __init__(self, *args, **kargs):
super(Store, self).__init__(*args, **kargs)
if self.backend_group:
self.store_conf = getattr(self.conf, self.backend_group)
self.store_conf = driver.BackendGroupConfiguration(
self.OPTIONS, self.backend_group, conf=self.conf)
else:
self.store_conf = self.conf.glance_store

View File

@@ -465,7 +465,8 @@ class Store(glance_store.driver.Store):
def _option_get(self, param):
if self.backend_group:
store_conf = getattr(self.conf, self.backend_group)
store_conf = glance_store.driver.BackendGroupConfiguration(
self.OPTIONS, self.backend_group, conf=self.conf)
else:
store_conf = self.conf.glance_store

View File

@@ -364,7 +364,8 @@ class Store(glance_store.Store):
"the vmwareapi driver in nova was marked experimental and "
"may be removed in a future release.")
if self.backend_group:
self.store_conf = getattr(self.conf, self.backend_group)
self.store_conf = glance_store.driver.BackendGroupConfiguration(
self.OPTIONS, self.backend_group, conf=self.conf)
else:
self.store_conf = self.conf.glance_store

View File

@@ -31,6 +31,8 @@ from glance_store.i18n import _
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
SHARED_CONF_GROUP = 'backend_defaults'
_MULTI_BACKEND_OPTS = [
cfg.StrOpt('store_description',
@@ -80,6 +82,7 @@ class Store(capabilities.StoreCapability):
self.MULTI_BACKEND_OPTIONS, group=group)
self.conf.register_opts(self.OPTIONS, group=group)
self.conf.register_opts(self.OPTIONS, group=SHARED_CONF_GROUP)
except cfg.DuplicateOptError:
pass
@@ -300,3 +303,62 @@ def back_compat_add(store_add_fun):
metadata_dict)
return add_adapter
class BackendGroupConfiguration(object):
def __init__(self, store_opts, config_group=None, conf=None):
"""Initialize configuration.
This takes care of grafting the implementation's config
values into the config group and shared defaults. We will try to
pull values from the specified 'config_group', but fall back to
defaults from the SHARED_CONF_GROUP.
"""
self.config_group = config_group
self.conf = conf or CONF
# set the local conf so that __call__'s know what to use
self._ensure_config_values(store_opts)
self.backend_conf = self.conf._get(self.config_group)
self.shared_backend_conf = self.conf._get(SHARED_CONF_GROUP)
def _safe_register(self, opt, group):
try:
CONF.register_opt(opt, group=group)
except cfg.DuplicateOptError:
pass # If it's already registered ignore it
def _ensure_config_values(self, store_opts):
"""Register the options in the shared group.
When we go to get a config option we will try the backend specific
group first and fall back to the shared group. We override the default
from all the config options for the backend group so we can know if it
was set or not.
"""
for opt in store_opts:
self._safe_register(opt, SHARED_CONF_GROUP)
# Assuming they aren't the same groups, graft on the options into
# the backend group and override its default value.
if self.config_group != SHARED_CONF_GROUP:
self._safe_register(opt, self.config_group)
self.conf.set_default(opt.name, None, group=self.config_group)
def append_config_values(self, store_opts):
self._ensure_config_values(store_opts)
def set_default(self, opt_name, default):
self.conf.set_default(opt_name, default, group=SHARED_CONF_GROUP)
def get(self, key, default=None):
return getattr(self, key, default)
def __getattr__(self, opt_name):
# Don't use self.X to avoid reentrant call to __getattr__()
backend_conf = object.__getattribute__(self, 'backend_conf')
opt_value = getattr(backend_conf, opt_name)
if opt_value is None:
shared_conf = object.__getattribute__(self, 'shared_backend_conf')
opt_value = getattr(shared_conf, opt_name)
return opt_value

View File

@@ -0,0 +1,77 @@
# Copyright (c) 2025 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.
"""Tests for the configuration wrapper in Glance Store drivers."""
from oslo_config import cfg
from oslotest import base
import glance_store.driver as driver
store_opts = [
cfg.StrOpt('str_opt', default='STR_OPT'),
cfg.BoolOpt('bool_opt', default=False)
]
more_store_opts = [
cfg.IntOpt('int_opt', default=1),
]
CONF = cfg.CONF
CONF.register_opts(store_opts)
CONF.register_opts(more_store_opts)
class BackendGroupConfigurationTest(base.BaseTestCase):
def override_config(self, name, override, group=None):
"""Cleanly override CONF variables."""
CONF.set_override(name, override, group)
self.addCleanup(CONF.clear_override, name, group)
def test_group_grafts_opts(self):
c = driver.BackendGroupConfiguration(store_opts, config_group='foo')
self.assertEqual(c.str_opt, 'STR_OPT')
self.assertEqual(c.bool_opt, False)
self.assertEqual(c.str_opt, CONF.backend_defaults.str_opt)
self.assertEqual(c.bool_opt, CONF.backend_defaults.bool_opt)
self.assertIsNone(CONF.foo.str_opt)
self.assertIsNone(CONF.foo.bool_opt)
def test_grafting_multiple_opts(self):
c = driver.BackendGroupConfiguration(store_opts, config_group='foo')
c.append_config_values(more_store_opts)
self.assertEqual(c.str_opt, 'STR_OPT')
self.assertEqual(c.bool_opt, False)
self.assertEqual(c.int_opt, 1)
# We get the right values, but they are coming from the
# backend_defaults group of CONF and not the 'foo' one.
self.assertEqual(c.str_opt, CONF.backend_defaults.str_opt)
self.assertEqual(c.bool_opt, CONF.backend_defaults.bool_opt)
self.assertEqual(c.int_opt, CONF.backend_defaults.int_opt)
self.assertIsNone(CONF.foo.str_opt)
self.assertIsNone(CONF.foo.bool_opt)
self.assertIsNone(CONF.foo.int_opt)
def test_backend_specific_value(self):
c = driver.BackendGroupConfiguration(store_opts, config_group='foo')
self.override_config('str_opt', 'bar', group='backend_defaults')
actual_value = c.str_opt
self.assertEqual('bar', actual_value)
self.override_config('str_opt', 'notbar', group='foo')
actual_value = c.str_opt
self.assertEqual('notbar', actual_value)

View File

@@ -450,6 +450,9 @@ class TestMultiStore(base.MultiStoreBaseTest,
self.conf.set_override('filesystem_store_datadir',
override=None,
group='file1')
self.conf.set_override('filesystem_store_datadir',
override=None,
group='backend_defaults')
self.conf.set_override('filesystem_store_datadirs',
[store_map[0] + ":100",
store_map[1] + ":200"],
@@ -568,6 +571,9 @@ class TestMultiStore(base.MultiStoreBaseTest,
self.conf.set_override('filesystem_store_datadir',
override=None,
group='file1')
self.conf.set_override('filesystem_store_datadir',
override=None,
group='backend_defaults')
self.conf.set_override('filesystem_store_datadirs',
[store_map[0] + ":100",
store_map[1] + ":200",
@@ -618,6 +624,9 @@ class TestMultiStore(base.MultiStoreBaseTest,
self.conf.set_override('filesystem_store_datadir',
override=None,
group='file1')
self.conf.set_override('filesystem_store_datadir',
override=None,
group='backend_defaults')
self.conf.set_override('filesystem_store_datadirs',
[store_map[0] + ":100",
@@ -670,6 +679,9 @@ class TestMultiStore(base.MultiStoreBaseTest,
self.conf.set_override('filesystem_store_datadir',
override=None,
group='file1')
self.conf.set_override('filesystem_store_datadir',
override=None,
group='backend_defaults')
self.conf.set_override('filesystem_store_datadirs',
[store_map[0] + ":100",
store_map[1] + ":200"],

View File

@@ -0,0 +1,9 @@
---
features:
- |
Added a new ``[backend_defaults]`` section in the glance
configuration file.
Now operators can add common configuration options in the
``[backend_defaults]`` section which will act as a fallback
mechanism for a configuration option not defined in the
main backend group.