Fix configuration options
When fixing bug #1854188 (Change-ID:
I62e992804a3ae6aa0b4aa4f883807783197d4b33) we broke configuration
options, because now we cannot pass configuration options from the
DEFAULT section, and some ListOpt don't work because cinderlib was
incorrectly expecting all these options to have bounds=True, but some
don't define it and default to False.
Most of the "crazy" code we currently have around oslo config is because
we wanted to support drivers that add configuration options on runtime
and reload the configuration.
This made sense when cinderlib was not an OpenStack official project,
but now that it is it makes more sense to force Cinder drivers to do the
right thing.
Aligned with this, this patch removes all the faking of the cinder.conf
file as a StringIO object, and simply sets things in the configuration
object that is passed to the drivers, which is the only thing they
should be using.
This will make cinderlib code more robust, even if some drivers will now
need to be modified to work with cinderlib.
We are validating known configuration options and ignoring unknown ones,
like Cinder does.
The NetApp driver was one of the drivers that would get broken, but
since I have access to it and we can make a very small and targeted
workaround to avoid breaking it, we do it as well.
Closes-Bug: #1856556
Change-Id: I67fbf8e9d7ee79f3d6617b4d0ae755dae0e1987a
(cherry picked from commit fbd7a72c1b
)
This commit is contained in:
parent
734376fab5
commit
199ebd4f5a
@ -18,7 +18,6 @@ import json as json_lib
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import coordination
|
from cinder import coordination
|
||||||
from cinder.db import api as db_api
|
from cinder.db import api as db_api
|
||||||
@ -31,7 +30,7 @@ cinder_objects.register_all() # noqa
|
|||||||
from cinder.interface import util as cinder_interface_util
|
from cinder.interface import util as cinder_interface_util
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
from cinder.volume import configuration
|
from cinder.volume import configuration
|
||||||
from cinder.volume import manager
|
from cinder.volume import manager # noqa We need to import config options
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as oslo_logging
|
from oslo_log import log as oslo_logging
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
@ -76,7 +75,8 @@ class Backend(object):
|
|||||||
driver_cfg['volume_backend_name'] = volume_backend_name
|
driver_cfg['volume_backend_name'] = volume_backend_name
|
||||||
Backend.backends[volume_backend_name] = self
|
Backend.backends[volume_backend_name] = self
|
||||||
|
|
||||||
conf = self._set_backend_config(driver_cfg)
|
conf = self._get_backend_config(driver_cfg)
|
||||||
|
self._apply_backend_workarounds(conf)
|
||||||
self.driver = importutils.import_object(
|
self.driver = importutils.import_object(
|
||||||
conf.volume_driver,
|
conf.volume_driver,
|
||||||
configuration=conf,
|
configuration=conf,
|
||||||
@ -190,164 +190,59 @@ class Backend(object):
|
|||||||
# the persistence plugin.
|
# the persistence plugin.
|
||||||
db_api.IMPL = cls.persistence.db
|
db_api.IMPL = cls.persistence.db
|
||||||
|
|
||||||
# NOTE(geguileo): Staticmethod used instead of classmethod to make it work
|
|
||||||
# on Python3 when assigning the unbound method.
|
|
||||||
@staticmethod
|
|
||||||
def _config_parse(self):
|
|
||||||
"""Replacer oslo_config.cfg.ConfigParser.parse for in-memory cfg."""
|
|
||||||
res = super(cfg.ConfigParser, self).parse(Backend._config_string_io)
|
|
||||||
return res
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _update_cinder_config(cls):
|
|
||||||
"""Parse in-memory file to update OSLO configuration used by Cinder."""
|
|
||||||
cls._config_string_io.seek(0)
|
|
||||||
cls._parser.write(cls._config_string_io)
|
|
||||||
|
|
||||||
# Check if we have any multiopt
|
|
||||||
cls._config_string_io.seek(0)
|
|
||||||
current_cfg = cls._config_string_io.read()
|
|
||||||
if '\n\t' in current_cfg:
|
|
||||||
cls._config_string_io.seek(0)
|
|
||||||
cls._config_string_io.write(current_cfg.replace('\n\t', '\n'))
|
|
||||||
|
|
||||||
cls._config_string_io.seek(0)
|
|
||||||
cfg.CONF.reload_config_files()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _set_cinder_config(cls, host, locks_path, cinder_config_params):
|
def _set_cinder_config(cls, host, locks_path, cinder_config_params):
|
||||||
"""Setup the parser with all the known Cinder configuration."""
|
"""Setup the parser with all the known Cinder configuration."""
|
||||||
cfg.CONF.set_default('state_path', os.getcwd())
|
cfg.CONF.set_default('state_path', os.getcwd())
|
||||||
cfg.CONF.set_default('lock_path', '$state_path', 'oslo_concurrency')
|
cfg.CONF.set_default('lock_path', '$state_path', 'oslo_concurrency')
|
||||||
|
cfg.CONF.version = cinderlib.__version__
|
||||||
cls._parser = six.moves.configparser.SafeConfigParser()
|
|
||||||
cls._parser.set('DEFAULT', 'enabled_backends', '')
|
|
||||||
|
|
||||||
if locks_path:
|
if locks_path:
|
||||||
cls._parser.add_section('oslo_concurrency')
|
cfg.CONF.oslo_concurrency.lock_path = locks_path
|
||||||
cls._parser.set('oslo_concurrency', 'lock_path', locks_path)
|
cfg.CONF.coordination.backend_url = 'file://' + locks_path
|
||||||
cls._parser.add_section('coordination')
|
|
||||||
cls._parser.set('coordination',
|
|
||||||
'backend_url',
|
|
||||||
'file://' + locks_path)
|
|
||||||
if host:
|
if host:
|
||||||
cls._parser.set('DEFAULT', 'host', host)
|
cfg.CONF.host = host
|
||||||
|
|
||||||
# All other configuration options go into the DEFAULT section
|
cls._validate_options(cinder_config_params)
|
||||||
cls.__set_parser_kv(cinder_config_params, 'DEFAULT')
|
for k, v in cinder_config_params.items():
|
||||||
|
setattr(cfg.CONF, k, v)
|
||||||
# We replace the OSLO's default parser to read from a StringIO instead
|
|
||||||
# of reading from a file.
|
|
||||||
cls._config_string_io = six.moves.StringIO()
|
|
||||||
cfg.ConfigParser.parse = six.create_unbound_method(cls._config_parse,
|
|
||||||
cfg.ConfigParser)
|
|
||||||
|
|
||||||
# Replace command line arg parser so we ignore caller's args
|
# Replace command line arg parser so we ignore caller's args
|
||||||
cfg._CachedArgumentParser.parse_args = lambda *a, **kw: None
|
cfg._CachedArgumentParser.parse_args = lambda *a, **kw: None
|
||||||
|
|
||||||
# Update the configuration with the options we have configured
|
@classmethod
|
||||||
cfg.CONF(project='cinder', version=cinderlib.__version__,
|
def _validate_options(cls, kvs, group=None):
|
||||||
default_config_files=['in_memory_file'])
|
|
||||||
cls._update_cinder_config()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_options_types(kvs):
|
|
||||||
"""Get loaded oslo options and load driver if we know it."""
|
|
||||||
# Dynamically loading the driver triggers adding the specific
|
# Dynamically loading the driver triggers adding the specific
|
||||||
# configuration options to the backend_defaults section
|
# configuration options to the backend_defaults section
|
||||||
if 'volume_driver' in kvs:
|
if kvs.get('volume_driver'):
|
||||||
driver_ns = kvs['volume_driver'].rsplit('.', 1)[0]
|
driver_ns = kvs['volume_driver'].rsplit('.', 1)[0]
|
||||||
__import__(driver_ns)
|
__import__(driver_ns)
|
||||||
opts = configuration.CONF._groups['backend_defaults']._opts
|
group = group or 'backend_defaults'
|
||||||
return opts
|
|
||||||
|
|
||||||
@classmethod
|
for k, v in kvs.items():
|
||||||
def __val_to_str(cls, val):
|
try:
|
||||||
"""Convert an oslo config value to its string representation.
|
# set_override does the validation
|
||||||
|
cfg.CONF.set_override(k, v, group)
|
||||||
|
# for correctness, don't leave it there
|
||||||
|
cfg.CONF.clear_override(k, group)
|
||||||
|
except cfg.NoSuchOptError:
|
||||||
|
# Don't fail on unknown variables, behave like cinder
|
||||||
|
LOG.warning('Unknown config option %s', k)
|
||||||
|
|
||||||
Lists and tuples are treated as ListOpt classes and converted to
|
def _get_backend_config(self, driver_cfg):
|
||||||
"[value1,value2]" instead of the standard string representation of
|
# Create the group for the backend
|
||||||
"['value1','value2']".
|
|
||||||
|
|
||||||
Dictionaries are treated as DictOpt and converted to 'k1:v1,k2:v2"
|
|
||||||
instead of the standard representation of "{'k1':'v1','k2':'v2'}".
|
|
||||||
|
|
||||||
Anything else is converted to a string.
|
|
||||||
"""
|
|
||||||
if isinstance(val, six.string_types):
|
|
||||||
return val
|
|
||||||
|
|
||||||
# Recursion is used to to handle options like u4p_failover_target that
|
|
||||||
# is a MultiOpt where each entry is a dictionary.
|
|
||||||
if isinstance(val, (list, tuple)):
|
|
||||||
return '[' + ','.join((cls.__val_to_str(v) for v in val)) + ']'
|
|
||||||
|
|
||||||
if isinstance(val, dict):
|
|
||||||
return ','.join('%s:%s' % (k, cls.__val_to_str(v))
|
|
||||||
for k, v in val.items())
|
|
||||||
|
|
||||||
return six.text_type(val)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __convert_option_to_string(cls, key, val, opts):
|
|
||||||
"""Convert a Cinder driver cfg option into oslo config file format.
|
|
||||||
|
|
||||||
A single Python object represents multiple Oslo config types. For
|
|
||||||
example a list can be a ListOpt or a MultOpt, and their string
|
|
||||||
representations on a file are different.
|
|
||||||
|
|
||||||
This method uses the configuration option types to return the right
|
|
||||||
string representation.
|
|
||||||
"""
|
|
||||||
opt = opts[key]['opt']
|
|
||||||
|
|
||||||
# Convert to a list for ListOpt opts were the caller didn't pass a list
|
|
||||||
if (isinstance(opt, cfg.ListOpt) and
|
|
||||||
not isinstance(val, (list, tuple))):
|
|
||||||
val = [val]
|
|
||||||
|
|
||||||
# For MultiOpt we need multiple entries in the file but ConfigParser
|
|
||||||
# doesn't support repeating the same entry multiple times, so we hack
|
|
||||||
# our way around it
|
|
||||||
elif isinstance(opt, cfg.MultiOpt):
|
|
||||||
if not isinstance(val, (list, tuple)):
|
|
||||||
val = [val] if val else []
|
|
||||||
val = [cls.__val_to_str(v) for v in val]
|
|
||||||
if not val:
|
|
||||||
val = ''
|
|
||||||
elif len(val) == 1:
|
|
||||||
val = val[0]
|
|
||||||
else:
|
|
||||||
val = (('%s\n' % val[0]) +
|
|
||||||
'\n'.join('%s = %s' % (key, v) for v in val[1:]))
|
|
||||||
|
|
||||||
# This will handle DictOpt and ListOpt
|
|
||||||
if not isinstance(val, six.string_types):
|
|
||||||
val = cls.__val_to_str(val)
|
|
||||||
return val
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __set_parser_kv(cls, kvs, section):
|
|
||||||
"""Set Oslo configuration options in our parser.
|
|
||||||
|
|
||||||
The Oslo parser needs to have the configuration options like they are
|
|
||||||
in a file, but we have them as Python objects, so we need to set them
|
|
||||||
for the parser in the format it is expecting them, strings.
|
|
||||||
"""
|
|
||||||
opts = cls.__get_options_types(kvs)
|
|
||||||
for key, val in kvs.items():
|
|
||||||
string_val = cls.__convert_option_to_string(key, val, opts)
|
|
||||||
cls._parser.set(section, key, string_val)
|
|
||||||
|
|
||||||
def _set_backend_config(self, driver_cfg):
|
|
||||||
backend_name = driver_cfg['volume_backend_name']
|
backend_name = driver_cfg['volume_backend_name']
|
||||||
self._parser.add_section(backend_name)
|
cfg.CONF.register_group(cfg.OptGroup(backend_name))
|
||||||
self.__set_parser_kv(driver_cfg, backend_name)
|
|
||||||
self._parser.set('DEFAULT', 'enabled_backends',
|
# Validate and set config options
|
||||||
','.join(self.backends.keys()))
|
backend_group = getattr(cfg.CONF, backend_name)
|
||||||
self._update_cinder_config()
|
self._validate_options(driver_cfg)
|
||||||
config = configuration.Configuration(manager.volume_backend_opts,
|
for key, value in driver_cfg.items():
|
||||||
config_group=backend_name)
|
setattr(backend_group, key, value)
|
||||||
|
|
||||||
|
# Return the Configuration that will be passed to the driver
|
||||||
|
config = configuration.Configuration([], config_group=backend_name)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -384,6 +279,14 @@ class Backend(object):
|
|||||||
cls.global_initialization = True
|
cls.global_initialization = True
|
||||||
cls.output_all_backend_info = output_all_backend_info
|
cls.output_all_backend_info = output_all_backend_info
|
||||||
|
|
||||||
|
def _apply_backend_workarounds(self, config):
|
||||||
|
"""Apply workarounds for drivers that do bad stuff."""
|
||||||
|
if 'netapp' in config.volume_driver:
|
||||||
|
# Workaround NetApp's weird replication stuff that makes it reload
|
||||||
|
# config sections in get_backend_configuration. OK since we don't
|
||||||
|
# support replication.
|
||||||
|
cfg.CONF.list_all_sections = lambda: config.volume_backend_name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _set_logging(cls, disable_logs):
|
def _set_logging(cls, disable_logs):
|
||||||
if disable_logs:
|
if disable_logs:
|
||||||
|
@ -14,16 +14,18 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import os
|
||||||
|
|
||||||
|
import ddt
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import types
|
|
||||||
|
|
||||||
import cinderlib
|
import cinderlib
|
||||||
from cinderlib import objects
|
from cinderlib import objects
|
||||||
from cinderlib.tests.unit import base
|
from cinderlib.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
class TestCinderlib(base.BaseTest):
|
class TestCinderlib(base.BaseTest):
|
||||||
def test_list_supported_drivers(self):
|
def test_list_supported_drivers(self):
|
||||||
expected_keys = {'version', 'class_name', 'supported', 'ci_wiki_name',
|
expected_keys = {'version', 'class_name', 'supported', 'ci_wiki_name',
|
||||||
@ -40,10 +42,12 @@ class TestCinderlib(base.BaseTest):
|
|||||||
self.assertEqual(cinderlib.Backend,
|
self.assertEqual(cinderlib.Backend,
|
||||||
cinderlib.objects.Object.backend_class)
|
cinderlib.objects.Object.backend_class)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.Backend._apply_backend_workarounds')
|
||||||
@mock.patch('oslo_utils.importutils.import_object')
|
@mock.patch('oslo_utils.importutils.import_object')
|
||||||
@mock.patch('cinderlib.Backend._set_backend_config')
|
@mock.patch('cinderlib.Backend._get_backend_config')
|
||||||
@mock.patch('cinderlib.Backend.global_setup')
|
@mock.patch('cinderlib.Backend.global_setup')
|
||||||
def test_init(self, mock_global_setup, mock_config, mock_import):
|
def test_init(self, mock_global_setup, mock_config, mock_import,
|
||||||
|
mock_workarounds):
|
||||||
cfg.CONF.set_override('host', 'host')
|
cfg.CONF.set_override('host', 'host')
|
||||||
driver_cfg = {'k': 'v', 'k2': 'v2', 'volume_backend_name': 'Test'}
|
driver_cfg = {'k': 'v', 'k2': 'v2', 'volume_backend_name': 'Test'}
|
||||||
cinderlib.Backend.global_initialization = False
|
cinderlib.Backend.global_initialization = False
|
||||||
@ -74,8 +78,34 @@ class TestCinderlib(base.BaseTest):
|
|||||||
self.assertIsNone(backend._volumes)
|
self.assertIsNone(backend._volumes)
|
||||||
driver.get_volume_stats.assert_not_called()
|
driver.get_volume_stats.assert_not_called()
|
||||||
self.assertEqual(('default',), backend.pool_names)
|
self.assertEqual(('default',), backend.pool_names)
|
||||||
|
mock_workarounds.assert_called_once_with(mock_config.return_value)
|
||||||
|
|
||||||
@mock.patch('cinderlib.Backend._Backend__convert_option_to_string')
|
@mock.patch('cinderlib.Backend._validate_options')
|
||||||
|
@mock.patch.object(cfg, 'CONF')
|
||||||
|
def test__set_cinder_config(self, conf_mock, validate_mock):
|
||||||
|
cinder_cfg = {'debug': True}
|
||||||
|
|
||||||
|
objects.Backend._set_cinder_config('host', 'locks_path', cinder_cfg)
|
||||||
|
|
||||||
|
self.assertEqual(2, conf_mock.set_default.call_count)
|
||||||
|
conf_mock.set_default.assert_has_calls(
|
||||||
|
[mock.call('state_path', os.getcwd()),
|
||||||
|
mock.call('lock_path', '$state_path', 'oslo_concurrency')])
|
||||||
|
|
||||||
|
self.assertEqual(cinderlib.__version__, cfg.CONF.version)
|
||||||
|
|
||||||
|
self.assertEqual('locks_path', cfg.CONF.oslo_concurrency.lock_path)
|
||||||
|
self.assertEqual('file://locks_path',
|
||||||
|
cfg.CONF.coordination.backend_url)
|
||||||
|
self.assertEqual('host', cfg.CONF.host)
|
||||||
|
|
||||||
|
validate_mock.assert_called_once_with(cinder_cfg)
|
||||||
|
|
||||||
|
self.assertEqual(True, cfg.CONF.debug)
|
||||||
|
|
||||||
|
self.assertIsNone(cfg._CachedArgumentParser().parse_args())
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.Backend._set_cinder_config')
|
||||||
@mock.patch('urllib3.disable_warnings')
|
@mock.patch('urllib3.disable_warnings')
|
||||||
@mock.patch('cinder.coordination.COORDINATOR')
|
@mock.patch('cinder.coordination.COORDINATOR')
|
||||||
@mock.patch('cinderlib.Backend._set_priv_helper')
|
@mock.patch('cinderlib.Backend._set_priv_helper')
|
||||||
@ -84,13 +114,12 @@ class TestCinderlib(base.BaseTest):
|
|||||||
@mock.patch('cinderlib.Backend.set_persistence')
|
@mock.patch('cinderlib.Backend.set_persistence')
|
||||||
def test_global_setup(self, mock_set_pers, mock_serial, mock_log,
|
def test_global_setup(self, mock_set_pers, mock_serial, mock_log,
|
||||||
mock_sudo, mock_coord, mock_disable_warn,
|
mock_sudo, mock_coord, mock_disable_warn,
|
||||||
mock_convert_to_str):
|
mock_set_config):
|
||||||
mock_convert_to_str.side_effect = lambda *args: args[1]
|
|
||||||
cls = objects.Backend
|
cls = objects.Backend
|
||||||
cls.global_initialization = False
|
cls.global_initialization = False
|
||||||
cinder_cfg = {'k': 'v', 'k2': 'v2'}
|
cinder_cfg = {'k': 'v', 'k2': 'v2'}
|
||||||
|
|
||||||
cls.global_setup('file_locks',
|
cls.global_setup(mock.sentinel.locks_path,
|
||||||
mock.sentinel.root_helper,
|
mock.sentinel.root_helper,
|
||||||
mock.sentinel.ssl_warnings,
|
mock.sentinel.ssl_warnings,
|
||||||
mock.sentinel.disable_logs,
|
mock.sentinel.disable_logs,
|
||||||
@ -100,23 +129,21 @@ class TestCinderlib(base.BaseTest):
|
|||||||
mock.sentinel.user_id,
|
mock.sentinel.user_id,
|
||||||
mock.sentinel.pers_cfg,
|
mock.sentinel.pers_cfg,
|
||||||
mock.sentinel.fail_missing_backend,
|
mock.sentinel.fail_missing_backend,
|
||||||
'mock.sentinel.host',
|
mock.sentinel.host,
|
||||||
**cinder_cfg)
|
**cinder_cfg)
|
||||||
|
|
||||||
self.assertEqual('file_locks', cfg.CONF.oslo_concurrency.lock_path)
|
mock_set_config.assert_called_once_with(mock.sentinel.host,
|
||||||
self.assertEqual('file://file_locks',
|
mock.sentinel.locks_path,
|
||||||
cfg.CONF.coordination.backend_url)
|
cinder_cfg)
|
||||||
|
|
||||||
self.assertEqual(mock.sentinel.fail_missing_backend,
|
self.assertEqual(mock.sentinel.fail_missing_backend,
|
||||||
cls.fail_on_missing_backend)
|
cls.fail_on_missing_backend)
|
||||||
self.assertEqual(mock.sentinel.root_helper, cls.root_helper)
|
self.assertEqual(mock.sentinel.root_helper, cls.root_helper)
|
||||||
self.assertEqual(mock.sentinel.project_id, cls.project_id)
|
self.assertEqual(mock.sentinel.project_id, cls.project_id)
|
||||||
self.assertEqual(mock.sentinel.user_id, cls.user_id)
|
self.assertEqual(mock.sentinel.user_id, cls.user_id)
|
||||||
self.assertEqual(mock.sentinel.non_uuid_ids, cls.non_uuid_ids)
|
self.assertEqual(mock.sentinel.non_uuid_ids, cls.non_uuid_ids)
|
||||||
self.assertEqual('mock.sentinel.host', cfg.CONF.host)
|
|
||||||
mock_set_pers.assert_called_once_with(mock.sentinel.pers_cfg)
|
mock_set_pers.assert_called_once_with(mock.sentinel.pers_cfg)
|
||||||
|
|
||||||
self.assertEqual(cinderlib.__version__, cfg.CONF.version)
|
|
||||||
|
|
||||||
mock_serial.setup.assert_called_once_with(cls)
|
mock_serial.setup.assert_called_once_with(cls)
|
||||||
mock_log.assert_called_once_with(mock.sentinel.disable_logs)
|
mock_log.assert_called_once_with(mock.sentinel.disable_logs)
|
||||||
mock_sudo.assert_called_once_with(mock.sentinel.root_helper)
|
mock_sudo.assert_called_once_with(mock.sentinel.root_helper)
|
||||||
@ -127,6 +154,41 @@ class TestCinderlib(base.BaseTest):
|
|||||||
self.assertEqual(mock.sentinel.backend_info,
|
self.assertEqual(mock.sentinel.backend_info,
|
||||||
cls.output_all_backend_info)
|
cls.output_all_backend_info)
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.cinderlib.LOG.warning')
|
||||||
|
def test__validate_options(self, warning_mock):
|
||||||
|
# Validate default group config with Boolean and MultiStrOpt
|
||||||
|
self.backend._validate_options(
|
||||||
|
{'debug': True,
|
||||||
|
'osapi_volume_extension': ['a', 'b', 'c'],
|
||||||
|
})
|
||||||
|
# Test driver options with String, ListOpt, PortOpt
|
||||||
|
self.backend._validate_options(
|
||||||
|
{'volume_driver': 'cinder.volume.drivers.lvm.LVMVolumeDriver',
|
||||||
|
'volume_group': 'cinder-volumes',
|
||||||
|
'iscsi_secondary_ip_addresses': ['w.x.y.z', 'a.b.c.d'],
|
||||||
|
'target_port': 12345,
|
||||||
|
})
|
||||||
|
|
||||||
|
warning_mock.assert_not_called()
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
('debug', 'sure', None),
|
||||||
|
('target_port', 'abc', 'cinder.volume.drivers.lvm.LVMVolumeDriver'))
|
||||||
|
@ddt.unpack
|
||||||
|
def test__validate_options_failures(self, option, value, driver):
|
||||||
|
self.assertRaises(
|
||||||
|
ValueError,
|
||||||
|
self.backend._validate_options,
|
||||||
|
{'volume_driver': driver,
|
||||||
|
option: value})
|
||||||
|
|
||||||
|
@mock.patch('cinderlib.cinderlib.LOG.warning')
|
||||||
|
def test__validate_options_unkown(self, warning_mock):
|
||||||
|
self.backend._validate_options(
|
||||||
|
{'volume_driver': 'cinder.volume.drivers.lvm.LVMVolumeDriver',
|
||||||
|
'vmware_cluster_name': 'name'})
|
||||||
|
self.assertEqual(1, warning_mock.call_count)
|
||||||
|
|
||||||
def test_pool_names(self):
|
def test_pool_names(self):
|
||||||
pool_names = [mock.sentinel._pool_names]
|
pool_names = [mock.sentinel._pool_names]
|
||||||
self.backend._pool_names = pool_names
|
self.backend._pool_names = pool_names
|
||||||
@ -254,32 +316,6 @@ class TestCinderlib(base.BaseTest):
|
|||||||
self.backend.refresh()
|
self.backend.refresh()
|
||||||
self.persistence.get_volumes.assert_not_called()
|
self.persistence.get_volumes.assert_not_called()
|
||||||
|
|
||||||
def test___get_options_types(self):
|
|
||||||
# Before knowing the driver we don't have driver specific options.
|
|
||||||
opts = self.backend._Backend__get_options_types({})
|
|
||||||
self.assertNotIn('volume_group', opts)
|
|
||||||
# But we do have the basic options
|
|
||||||
self.assertIn('volume_driver', opts)
|
|
||||||
|
|
||||||
# When we know the driver the method can load it to retrieve its
|
|
||||||
# specific options.
|
|
||||||
cfg = {'volume_driver': 'cinder.volume.drivers.lvm.LVMVolumeDriver'}
|
|
||||||
opts = self.backend._Backend__get_options_types(cfg)
|
|
||||||
self.assertIn('volume_group', opts)
|
|
||||||
|
|
||||||
def test___val_to_str_integer(self):
|
|
||||||
res = self.backend._Backend__val_to_str(1)
|
|
||||||
self.assertEqual('1', res)
|
|
||||||
|
|
||||||
def test___val_to_str_list_tuple(self):
|
|
||||||
val = ['hola', 'hello']
|
|
||||||
expected = '[hola,hello]'
|
|
||||||
res = self.backend._Backend__val_to_str(val)
|
|
||||||
self.assertEqual(expected, res)
|
|
||||||
# Same with a tuple
|
|
||||||
res = self.backend._Backend__val_to_str(tuple(val))
|
|
||||||
self.assertEqual(expected, res)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def odict(*args):
|
def odict(*args):
|
||||||
res = collections.OrderedDict()
|
res = collections.OrderedDict()
|
||||||
@ -287,101 +323,16 @@ class TestCinderlib(base.BaseTest):
|
|||||||
res[args[i]] = args[i + 1]
|
res[args[i]] = args[i + 1]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def test___val_to_str_dict(self):
|
@mock.patch('cinderlib.cinderlib.cfg.CONF')
|
||||||
val = self.odict('k1', 'v1', 'k2', 2)
|
def test__apply_backend_workarounds(self, mock_conf):
|
||||||
res = self.backend._Backend__val_to_str(val)
|
cfg = mock.Mock(volume_driver='cinder.volume.drivers.netapp...')
|
||||||
self.assertEqual('k1:v1,k2:2', res)
|
self.backend._apply_backend_workarounds(cfg)
|
||||||
|
self.assertEqual(cfg.volume_backend_name,
|
||||||
|
mock_conf.list_all_sections())
|
||||||
|
|
||||||
def test___val_to_str_recursive(self):
|
@mock.patch('cinderlib.cinderlib.cfg.CONF')
|
||||||
val = self.odict('k1', ['hola', 'hello'], 'k2', [1, 2])
|
def test__apply_backend_workarounds_do_nothing(self, mock_conf):
|
||||||
expected = 'k1:[hola,hello],k2:[1,2]'
|
cfg = mock.Mock(volume_driver='cinder.volume.drivers.lvm...')
|
||||||
res = self.backend._Backend__val_to_str(val)
|
self.backend._apply_backend_workarounds(cfg)
|
||||||
self.assertEqual(expected, res)
|
self.assertEqual(mock_conf.list_all_sections.return_value,
|
||||||
|
mock_conf.list_all_sections())
|
||||||
@mock.patch('cinderlib.Backend._Backend__val_to_str')
|
|
||||||
def test___convert_option_to_string_int(self, convert_mock):
|
|
||||||
PortType = types.Integer(1, 65535)
|
|
||||||
opt = cfg.Opt('port', type=PortType, default=9292, help='Port number')
|
|
||||||
opts = {'port': {'opt': opt, 'cli': False}}
|
|
||||||
res = self.backend._Backend__convert_option_to_string(
|
|
||||||
'port', 12345, opts)
|
|
||||||
self.assertEqual(convert_mock.return_value, res)
|
|
||||||
convert_mock.assert_called_once_with(12345)
|
|
||||||
|
|
||||||
@mock.patch('cinderlib.Backend._Backend__val_to_str')
|
|
||||||
def test___convert_option_to_string_listopt(self, convert_mock):
|
|
||||||
opt = cfg.ListOpt('my_opt', help='my cool option')
|
|
||||||
opts = {'my_opt': {'opt': opt, 'cli': False}}
|
|
||||||
res = self.backend._Backend__convert_option_to_string(
|
|
||||||
'my_opt', [mock.sentinel.value], opts)
|
|
||||||
self.assertEqual(convert_mock.return_value, res)
|
|
||||||
convert_mock.assert_called_once_with([mock.sentinel.value])
|
|
||||||
|
|
||||||
# Same result if we don't pass a list, since method converts it
|
|
||||||
convert_mock.reset_mock()
|
|
||||||
res = self.backend._Backend__convert_option_to_string(
|
|
||||||
'my_opt', mock.sentinel.value, opts)
|
|
||||||
self.assertEqual(convert_mock.return_value, res)
|
|
||||||
convert_mock.assert_called_once_with([mock.sentinel.value])
|
|
||||||
|
|
||||||
@mock.patch('cinderlib.Backend._Backend__val_to_str')
|
|
||||||
def test___convert_option_to_string_multistr_one(self, convert_mock):
|
|
||||||
convert_mock.side_effect = lambda x: x
|
|
||||||
opt = cfg.MultiStrOpt('my_opt', default=['default'], help='help')
|
|
||||||
opts = {'my_opt': {'opt': opt, 'cli': False}}
|
|
||||||
res = self.backend._Backend__convert_option_to_string(
|
|
||||||
'my_opt', ['value1'], opts)
|
|
||||||
self.assertEqual('value1', res)
|
|
||||||
convert_mock.assert_called_once_with('value1')
|
|
||||||
|
|
||||||
# Same result if we don't pass a list, since method converts it
|
|
||||||
convert_mock.reset_mock()
|
|
||||||
res = self.backend._Backend__convert_option_to_string(
|
|
||||||
'my_opt', 'value1', opts)
|
|
||||||
self.assertEqual('value1', res)
|
|
||||||
convert_mock.assert_called_once_with('value1')
|
|
||||||
|
|
||||||
@mock.patch('cinderlib.Backend._Backend__val_to_str')
|
|
||||||
def test___convert_option_to_string_multistr(self, convert_mock):
|
|
||||||
convert_mock.side_effect = lambda x: x
|
|
||||||
opt = cfg.MultiStrOpt('my_opt', default=['default'], help='help')
|
|
||||||
opts = {'my_opt': {'opt': opt, 'cli': False}}
|
|
||||||
res = self.backend._Backend__convert_option_to_string(
|
|
||||||
'my_opt', ['value1', 'value2'], opts)
|
|
||||||
self.assertEqual('value1\nmy_opt = value2', res)
|
|
||||||
self.assertEqual(2, convert_mock.call_count)
|
|
||||||
convert_mock.assert_has_calls([mock.call('value1'),
|
|
||||||
mock.call('value2')])
|
|
||||||
|
|
||||||
def test___convert_option_to_string_multiopt(self):
|
|
||||||
opt = cfg.MultiOpt('my_opt', item_type=types.Dict())
|
|
||||||
opts = {'my_opt': {'opt': opt, 'cli': False}}
|
|
||||||
elem1 = self.odict('v1_k1', 'v1_v1', 'v1_k2', 'v1_v2')
|
|
||||||
elem2 = self.odict('v2_k1', 'v2_v1', 'v2_k2', 'v2_v2')
|
|
||||||
res = self.backend._Backend__convert_option_to_string(
|
|
||||||
'my_opt', [elem1, elem2], opts)
|
|
||||||
expect = 'v1_k1:v1_v1,v1_k2:v1_v2\nmy_opt = v2_k1:v2_v1,v2_k2:v2_v2'
|
|
||||||
self.assertEqual(expect, res)
|
|
||||||
|
|
||||||
@mock.patch('cinderlib.Backend._parser')
|
|
||||||
@mock.patch('cinderlib.Backend._Backend__convert_option_to_string')
|
|
||||||
@mock.patch('cinderlib.Backend._Backend__get_options_types')
|
|
||||||
def test___set_parser_kv(self, types_mock, convert_mock, parser_mock):
|
|
||||||
convert_mock.side_effect = [mock.sentinel.res1, mock.sentinel.res2]
|
|
||||||
section = mock.sentinel.section
|
|
||||||
kvs = self.odict('k1', 1, 'k2', 2)
|
|
||||||
|
|
||||||
self.backend._Backend__set_parser_kv(kvs, section)
|
|
||||||
|
|
||||||
types_mock.assert_called_once_with(kvs)
|
|
||||||
self.assertEqual(2, convert_mock.call_count)
|
|
||||||
convert_mock.assert_has_calls(
|
|
||||||
[mock.call('k1', 1, types_mock.return_value),
|
|
||||||
mock.call('k2', 2, types_mock.return_value)],
|
|
||||||
any_order=True)
|
|
||||||
|
|
||||||
self.assertEqual(2, parser_mock.set.call_count)
|
|
||||||
parser_mock.set.assert_has_calls(
|
|
||||||
[mock.call(section, 'k1', mock.sentinel.res1),
|
|
||||||
mock.call(section, 'k2', mock.sentinel.res2)],
|
|
||||||
any_order=True)
|
|
||||||
|
@ -223,12 +223,12 @@ VMAX
|
|||||||
hpe3par_api_url: https://w.x.y.z:8080/api/v1
|
hpe3par_api_url: https://w.x.y.z:8080/api/v1
|
||||||
hpe3par_username: user
|
hpe3par_username: user
|
||||||
hpe3par_password: toomanysecrets
|
hpe3par_password: toomanysecrets
|
||||||
hpe3par_cpg: CPG_name
|
hpe3par_cpg: [CPG_name]
|
||||||
san_ip: w.x.y.z
|
san_ip: w.x.y.z
|
||||||
san_login: user
|
san_login: user
|
||||||
san_password: toomanysecrets
|
san_password: toomanysecrets
|
||||||
volume_driver: cinder.volume.drivers.hpe.hpe_3par_iscsi.HPE3PARISCSIDriver
|
volume_driver: cinder.volume.drivers.hpe.hpe_3par_iscsi.HPE3PARISCSIDriver
|
||||||
hpe3par_iscsi_ips: w.x.y2.z2,w.x.y2.z3,w.x.y2.z4,w.x.y2.z4
|
hpe3par_iscsi_ips: [w.x.y2.z2,w.x.y2.z3,w.x.y2.z4,w.x.y2.z4]
|
||||||
hpe3par_debug: false
|
hpe3par_debug: false
|
||||||
hpe3par_iscsi_chap_enabled: false
|
hpe3par_iscsi_chap_enabled: false
|
||||||
hpe3par_snapshot_retention: 0
|
hpe3par_snapshot_retention: 0
|
||||||
|
Loading…
Reference in New Issue
Block a user