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:
Gorka Eguileor
2020-06-29 15:22:26 +02:00
parent 3a1ff321e8
commit a37a93154f
5 changed files with 167 additions and 39 deletions

View File

@@ -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()

View File

@@ -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):

View File

@@ -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)

View File

@@ -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
-----

View File

@@ -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.