Improve cinder supported drivers output
Method list_supported_drivers was intended for human consumption, so all values for the options in driver_options where strings, which makes it harder to parse by automation tools since they have to: - Convert values from 'None' to ``None``, 'False' to ``False``, etc. - Parse a string with type of the option to determine the additional options, such as choices, max and min values. This patch adds the possibility to get an output intended for automation tools. To maintain backward compatibility the method accepts ``output_version`` parameter that defaults to ``1`` (old output) but can get ``2`` for the new output. Change-Id: I2ade5d2e6b2c97d905e09fee509c81c2cfb602ed
This commit is contained in:
@@ -371,66 +371,87 @@ class Backend(object):
|
||||
self.volumes
|
||||
|
||||
@staticmethod
|
||||
def list_supported_drivers():
|
||||
"""Returns dictionary with driver classes names as keys."""
|
||||
def list_supported_drivers(output_version=1):
|
||||
"""Returns dictionary with driver classes names as keys.
|
||||
|
||||
def fix_cinderlib_options(driver_dict):
|
||||
The output of the method changes from version to version, so we can
|
||||
pass the output_version parameter to specify which version we are
|
||||
expecting.
|
||||
|
||||
Version 1: Original output intended for human consumption, where all
|
||||
dictionary values are strings.
|
||||
Version 2: Improved version intended for automated consumption.
|
||||
- type is now a dictionary with detailed information
|
||||
- Values retain their types, so we'll no longer get 'None'
|
||||
or 'False'.
|
||||
"""
|
||||
def get_vars(obj):
|
||||
return {k: v for k, v in vars(obj).items()
|
||||
if not k.startswith('_')}
|
||||
|
||||
def get_strs(obj):
|
||||
return {k: str(v) for k, v in vars(obj).items()
|
||||
if not k.startswith('_')}
|
||||
|
||||
def convert_oslo_config(oslo_option, output_version):
|
||||
if output_version != 2:
|
||||
return get_strs(oslo_option)
|
||||
|
||||
res = get_vars(oslo_option)
|
||||
type_class = res['type']
|
||||
res['type'] = get_vars(oslo_option.type)
|
||||
res['type']['type_class'] = type_class
|
||||
return res
|
||||
|
||||
def fix_cinderlib_options(driver_dict, output_version):
|
||||
# The rbd_keyring_conf option is deprecated and will be removed for
|
||||
# Cinder, because it's a security vulnerability there (OSSN-0085),
|
||||
# but it isn't for cinderlib, since the user of the library already
|
||||
# has access to all the credentials, and cinderlib needs it to work
|
||||
# with RBD, so we need to make sure that the config option is
|
||||
# there whether it's reported as deprecated or removed from Cinder.
|
||||
RBD_KEYRING_CONF = {
|
||||
'advanced': 'False', 'default': '', 'metavar': 'None',
|
||||
'deprecated_for_removal': 'False', 'deprecated_opts': '[]',
|
||||
'deprecated_reason': 'None', 'deprecated_since': 'None',
|
||||
'dest': 'rbd_keyring_conf', 'mutable': 'False',
|
||||
'help': 'Path to the ceph keyring file',
|
||||
'name': 'rbd_keyring_conf', 'positional': 'False',
|
||||
'required': 'False', 'sample_default': 'None',
|
||||
'secret': 'False', 'short': 'None', 'type': 'String'}
|
||||
RBD_KEYRING_CONF = cfg.StrOpt('rbd_keyring_conf',
|
||||
default='',
|
||||
help='Path to the ceph keyring file')
|
||||
|
||||
if driver_dict['class_name'] != 'RBDDriver':
|
||||
return
|
||||
rbd_opt = convert_oslo_config(RBD_KEYRING_CONF, output_version)
|
||||
for opt in driver_dict['driver_options']:
|
||||
if opt['dest'] == 'rbd_keyring_conf':
|
||||
opt.clear()
|
||||
opt.update(RBD_KEYRING_CONF)
|
||||
opt.update(rbd_opt)
|
||||
break
|
||||
else:
|
||||
driver_dict['driver_options'].append(RBD_KEYRING_CONF)
|
||||
driver_dict['driver_options'].append(rbd_opt)
|
||||
|
||||
def convert_oslo_config(oslo_options):
|
||||
options = []
|
||||
for opt in oslo_options:
|
||||
tmp_dict = {k: str(v) for k, v in vars(opt).items()
|
||||
if not k.startswith('_')}
|
||||
options.append(tmp_dict)
|
||||
return options
|
||||
|
||||
def list_drivers(queue):
|
||||
def list_drivers(queue, output_version):
|
||||
cwd = os.getcwd()
|
||||
# Go to the parent directory directory where Cinder is installed
|
||||
os.chdir(utils.__file__.rsplit(os.sep, 2)[0])
|
||||
try:
|
||||
drivers = cinder_interface_util.get_volume_drivers()
|
||||
mapping = {d.class_name: vars(d) for d in drivers}
|
||||
# Drivers contain class instances which are not serializable
|
||||
for driver in mapping.values():
|
||||
driver.pop('cls', None)
|
||||
if 'driver_options' in driver:
|
||||
driver['driver_options'] = convert_oslo_config(
|
||||
driver['driver_options'])
|
||||
fix_cinderlib_options(driver)
|
||||
driver['driver_options'] = [
|
||||
convert_oslo_config(opt, output_version)
|
||||
for opt in driver['driver_options']
|
||||
]
|
||||
fix_cinderlib_options(driver, output_version)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
queue.put(mapping)
|
||||
|
||||
if not (1 <= output_version <= 2):
|
||||
raise ValueError('Acceptable versions are 1 and 2')
|
||||
|
||||
# Use a different process to avoid having all driver classes loaded in
|
||||
# memory during our execution.
|
||||
queue = multiprocessing.Queue()
|
||||
p = multiprocessing.Process(target=list_drivers, args=(queue,))
|
||||
p = multiprocessing.Process(target=list_drivers,
|
||||
args=(queue, output_version))
|
||||
p.start()
|
||||
result = queue.get()
|
||||
p.join()
|
||||
|
||||
@@ -16,28 +16,44 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
import ddt
|
||||
|
||||
import cinderlib
|
||||
from cinderlib.tests.functional import base_tests
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class BaseFunctTestCase(base_tests.unittest.TestCase):
|
||||
def test_list_supported_drivers(self):
|
||||
@ddt.data([], [1], [2])
|
||||
def test_list_supported_drivers(self, args):
|
||||
is_v2 = args == [2]
|
||||
expected_type = dict if is_v2 else str
|
||||
expected_keys = {'version', 'class_name', 'supported', 'ci_wiki_name',
|
||||
'driver_options', 'class_fqn', 'desc'}
|
||||
|
||||
drivers = cinderlib.Backend.list_supported_drivers()
|
||||
drivers = cinderlib.Backend.list_supported_drivers(*args)
|
||||
self.assertNotEqual(0, len(drivers))
|
||||
for name, driver_info in drivers.items():
|
||||
self.assertEqual(expected_keys, set(driver_info.keys()))
|
||||
|
||||
# Ensure that the RBDDriver has the rbd_keyring_conf option and
|
||||
# it's not deprecated
|
||||
if name == 'RBDDriver':
|
||||
keyring_conf = [conf for conf in driver_info['driver_options']
|
||||
if conf['dest'] == 'rbd_keyring_conf']
|
||||
self.assertEqual(1, len(keyring_conf))
|
||||
self.assertEqual('False',
|
||||
expected_value = False if is_v2 else 'False'
|
||||
self.assertEqual(expected_value,
|
||||
keyring_conf[0]['deprecated_for_removal'])
|
||||
|
||||
for option in driver_info['driver_options']:
|
||||
self.assertIsInstance(option['type'], expected_type)
|
||||
if is_v2:
|
||||
self.assertTrue('type_class' in option['type'])
|
||||
else:
|
||||
for v in option.values():
|
||||
self.assertIsInstance(v, str)
|
||||
|
||||
|
||||
@base_tests.test_all_backends
|
||||
class BackendFunctBasic(base_tests.BaseFunctTestCase):
|
||||
|
||||
@@ -27,23 +27,36 @@ from cinderlib.tests.unit import base
|
||||
|
||||
@ddt.ddt
|
||||
class TestCinderlib(base.BaseTest):
|
||||
def test_list_supported_drivers(self):
|
||||
@ddt.data([], [1], [2])
|
||||
def test_list_supported_drivers(self, args):
|
||||
is_v2 = args == [2]
|
||||
expected_type = dict if is_v2 else str
|
||||
expected_keys = {'version', 'class_name', 'supported', 'ci_wiki_name',
|
||||
'driver_options', 'class_fqn', 'desc'}
|
||||
|
||||
drivers = cinderlib.Backend.list_supported_drivers()
|
||||
drivers = cinderlib.Backend.list_supported_drivers(*args)
|
||||
self.assertNotEqual(0, len(drivers))
|
||||
for name, driver_info in drivers.items():
|
||||
self.assertEqual(expected_keys, set(driver_info.keys()))
|
||||
|
||||
# Ensure that the RBDDriver has the rbd_keyring_conf option and
|
||||
# it's not deprecated
|
||||
if name == 'RBDDriver':
|
||||
keyring_conf = [conf for conf in driver_info['driver_options']
|
||||
if conf['dest'] == 'rbd_keyring_conf']
|
||||
self.assertEqual(1, len(keyring_conf))
|
||||
self.assertEqual('False',
|
||||
expected_value = False if is_v2 else 'False'
|
||||
self.assertEqual(expected_value,
|
||||
keyring_conf[0]['deprecated_for_removal'])
|
||||
|
||||
for option in driver_info['driver_options']:
|
||||
self.assertIsInstance(option['type'], expected_type)
|
||||
if is_v2:
|
||||
self.assertTrue('type_class' in option['type'])
|
||||
else:
|
||||
for v in option.values():
|
||||
self.assertIsInstance(v, str)
|
||||
|
||||
def test_lib_assignations(self):
|
||||
self.assertEqual(cinderlib.setup, cinderlib.Backend.global_setup)
|
||||
self.assertEqual(cinderlib.Backend, cinderlib.objects.Backend)
|
||||
|
||||
@@ -143,6 +143,15 @@ Available drivers for *cinderlib* depend on the Cinder version installed, so we
|
||||
have a method, called `list_supported_drivers` to list information about the
|
||||
drivers that are included with the Cinder release installed in the system.
|
||||
|
||||
The method accepts parameter ``output_version`` where we can specify the
|
||||
desired output format:
|
||||
|
||||
- ``1`` for human usage (default value).
|
||||
- ``2`` for automation tools.
|
||||
|
||||
The main difference are the values of the driver options and how the expected
|
||||
type of these options is described.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import cinderlib
|
||||
@@ -162,7 +171,70 @@ Here's the entry for the LVM driver:
|
||||
'class_name': 'LVMVolumeDriver',
|
||||
'desc': 'Executes commands relating to Volumes.',
|
||||
'supported': True,
|
||||
'version': '3.0.0'}}
|
||||
'version': '3.0.0',
|
||||
'driver_options': [
|
||||
{'advanced': 'False',
|
||||
'default': '64',
|
||||
'deprecated_for_removal': 'False',
|
||||
'deprecated_opts': '[]',
|
||||
'deprecated_reason': 'None',
|
||||
'deprecated_since': 'None',
|
||||
'dest': 'spdk_max_queue_depth',
|
||||
'help': 'Queue depth for rdma transport.',
|
||||
'metavar': 'None',
|
||||
'mutable': 'False',
|
||||
'name': 'spdk_max_queue_depth',
|
||||
'positional': 'False',
|
||||
'required': 'False',
|
||||
'sample_default': 'None',
|
||||
'secret': 'False',
|
||||
'short': 'None',
|
||||
'type': 'Integer(min=1, max=128)'},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
The equivalent for the LVM driver for automation would be:
|
||||
|
||||
.. code-block::
|
||||
|
||||
import cinderlib
|
||||
|
||||
drivers = cinderlib.list_supported_drivers(2)
|
||||
|
||||
{'LVMVolumeDriver':
|
||||
{'ci_wiki_name': 'Cinder_Jenkins',
|
||||
'class_fqn': 'cinder.volume.drivers.lvm.LVMVolumeDriver',
|
||||
'class_name': 'LVMVolumeDriver',
|
||||
'desc': 'Executes commands relating to Volumes.',
|
||||
'supported': True,
|
||||
'version': '3.0.0',
|
||||
'driver_options': [
|
||||
{'advanced': False,
|
||||
'default': 64,
|
||||
'deprecated_for_removal': False,
|
||||
'deprecated_opts': [],
|
||||
'deprecated_reason': None,
|
||||
'deprecated_since': None,
|
||||
'dest': 'spdk_max_queue_depth',
|
||||
'help': 'Queue depth for rdma transport.',
|
||||
'metavar': None,
|
||||
'mutable': False,
|
||||
'name': 'spdk_max_queue_depth',
|
||||
'positional': False,
|
||||
'required': False,
|
||||
'sample_default': None,
|
||||
'secret': False,
|
||||
'short': None,
|
||||
'type': {'choices': None,
|
||||
'max': 128,
|
||||
'min': 1,
|
||||
'num_type': <class 'int'>,
|
||||
'type_class': Integer(min=1, max=128),
|
||||
'type_name': 'integer value'}}
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
Stats
|
||||
-----
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Improve ``list_supported_drivers`` method to facilitate usage by automation
|
||||
tools. Method now accepts ``output_version`` parameter with value ``2`` to
|
||||
get Python objects instead of having all values converted to strings.
|
||||
Reference in New Issue
Block a user