From 6c0152afa141d05ee28cba81178622021574ae17 Mon Sep 17 00:00:00 2001 From: Vanou Ishii Date: Wed, 1 Jun 2022 15:25:25 +0900 Subject: [PATCH] Fix iRMC driver to use certification file in HTTPS This patch modifies iRMC driver to use certification file when it connects to iRMC via HTTPS Conflicts: doc/source/admin/drivers/irmc.rst driver-requirements.txt ironic/drivers/modules/irmc/common.py ironic/tests/unit/drivers/modules/irmc/test_common.py ironic/tests/unit/drivers/modules/irmc/test_power.py releasenotes/notes/irmc-add-certification-file-option-34e7a0062c768e58.yaml Change-Id: If69ce1cf2789d9d60fb8e544596cf7d29eab514d Co-authored-by: Kobayashi Daisuke Co-authored-by: Song Shukun Story: 2009801 Task: 44345 (cherry picked from commit 64d7a7f3077bc000a18c4a0c56f122941b262483) --- doc/source/admin/drivers/irmc.rst | 50 +++ driver-requirements.txt | 2 +- ironic/drivers/modules/irmc/common.py | 124 ++++++- .../unit/drivers/modules/irmc/test_boot.py | 1 + .../unit/drivers/modules/irmc/test_common.py | 327 ++++++++++++++++-- .../unit/drivers/modules/irmc/test_power.py | 4 +- .../unit/drivers/modules/irmc/test_raid.py | 4 +- lower-constraints.txt | 1 + ...fication-file-option-34e7a0062c768e58.yaml | 18 + requirements.txt | 1 + 10 files changed, 485 insertions(+), 47 deletions(-) create mode 100644 releasenotes/notes/irmc-add-certification-file-option-34e7a0062c768e58.yaml diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst index 7e53bf8bb3..4e9bf44343 100644 --- a/doc/source/admin/drivers/irmc.rst +++ b/doc/source/admin/drivers/irmc.rst @@ -111,6 +111,9 @@ Here is a command example to enroll a node with ``irmc`` hardware type. Node configuration ^^^^^^^^^^^^^^^^^^ +Configuration via ``driver_info`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Each node is configured for ``irmc`` hardware type by setting the following ironic node object's properties: @@ -126,6 +129,44 @@ Node configuration UEFI Secure Boot is required. Please refer to `UEFI Secure Boot Support`_ for more information. +* If ``port`` in ``[irmc]`` section of ``/etc/ironic/ironic.conf`` or + ``driver_info/irmc_port`` is set to 443, ``driver_info/irmc_verify_ca`` + will take effect: + + ``driver_info/irmc_verify_ca`` property takes one of 4 value (default value + is ``True``): + + - ``True``: When set to ``True``, which certification file iRMC driver uses + is determined by ``requests`` Python module. + + Value of ``driver_info/irmc_verify_ca`` is passed to ``verify`` argument + of functions defined in ``requests`` Python module. So which certification + will be used is depend on behavior of ``requests`` module. + (maybe certification provided by ``certifi`` Python module) + + - ``False``: When set to ``False``, iRMC driver won't verify server + certification with certification file during HTTPS connection with iRMC. + Just stop to verify server certification, but does HTTPS. + + .. warning:: + When set to ``False``, user must notice that it can result in + vulnerable situation. Stopping verification of server certification + during HTTPS connection means it cannot prevent Man-in-the-middle + attack. When set to ``False``, Ironic user must take enough care + around infrastructure environment in terms of security. + (e.g. make sure network between Ironic conductor and iRMC is secure) + + - string representing filesystem path to directory which contains + certification file: In this case, iRMC driver uses certification file + stored at specified directory. Ironic conductor must be able to access + that directory. For iRMC to recongnize certification file, Ironic user + must run ``openssl rehash ``. + + - string representing filesystem path to certification file: In this case, + iRMC driver uses certification file specified. Ironic conductor must have + access to that file. + + * The following properties are also required if ``irmc-virtual-media`` boot interface is used: @@ -140,6 +181,10 @@ Node configuration ``irmc_deploy_iso`` and ``irmc_boot_iso`` accordingly before the Xena release. + +Configuration via ``ironic.conf`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * All of the nodes are configured by setting the following configuration options in the ``[irmc]`` section of ``/etc/ironic/ironic.conf``: @@ -176,6 +221,10 @@ Node configuration - ``snmp_security``: SNMP security name required for version ``v3``. Optional. + +Override ``ironic.conf`` configuration via ``driver_info`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Each node can be further configured by setting the following ironic node object's properties which override the parameter values in ``[irmc]`` section of ``/etc/ironic/ironic.conf``: @@ -189,6 +238,7 @@ Node configuration - ``driver_info/irmc_snmp_community`` property overrides ``snmp_community``. - ``driver_info/irmc_snmp_security`` property overrides ``snmp_security``. + Optional functionalities for the ``irmc`` hardware type ======================================================= diff --git a/driver-requirements.txt b/driver-requirements.txt index da312468e6..e54698fbe6 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -6,7 +6,7 @@ # These are available on pypi proliantutils>=2.13.0 pysnmp>=4.3.0,<5.0.0 -python-scciclient>=0.8.0 +python-scciclient>=0.8.0,<0.13.0 python-dracclient>=5.1.0,<9.0.0 python-xclarityclient>=0.1.6 diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py index 24adf0da8d..5b4ae385d1 100644 --- a/ironic/drivers/modules/irmc/common.py +++ b/ironic/drivers/modules/irmc/common.py @@ -15,8 +15,12 @@ """ Common functionalities shared between different iRMC modules. """ +import os + from oslo_log import log as logging from oslo_utils import importutils +from oslo_utils import strutils +from packaging import version from ironic.common import exception from ironic.common.i18n import _ @@ -25,6 +29,7 @@ from ironic.conf import CONF scci = importutils.try_import('scciclient.irmc.scci') elcm = importutils.try_import('scciclient.irmc.elcm') +scci_mod = importutils.try_import('scciclient') LOG = logging.getLogger(__name__) REQUIRED_PROPERTIES = { @@ -53,9 +58,36 @@ OPTIONAL_PROPERTIES = { 'irmc_snmp_security': _("SNMP security name required for version 'v3'. " "Optional."), } +OPTIONAL_DRIVER_INFO_PROPERTIES = { + 'irmc_verify_ca': _('Either a Boolean value, a path to a CA_BUNDLE ' + 'file or directory with certificates of trusted ' + 'CAs. If set to True the driver will verify the ' + 'host certificates; if False the driver will ' + 'ignore verifying the SSL certificate. If it\'s ' + 'a path the driver will use the specified ' + 'certificate or one of the certificates in the ' + 'directory. Defaults to True. Optional'), +} COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) +COMMON_PROPERTIES.update(OPTIONAL_DRIVER_INFO_PROPERTIES) + +SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES = [ + {'min': '0.8.2', 'upp': '0.9.0'}, + {'min': '0.9.4', 'upp': '0.10.0'}, + {'min': '0.10.1', 'upp': '0.11.0'}, + {'min': '0.11.3', 'upp': '0.12.0'}, + {'min': '0.12.0', 'upp': '0.13.0'}] + + +def scci_support_certification(): + scciclient_version = version.parse(scci_mod.__version__) + for rangev in SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES: + if (version.parse(rangev['min']) <= scciclient_version + < version.parse(rangev['upp'])): + return True + return False def parse_driver_info(node): @@ -84,7 +116,9 @@ def parse_driver_info(node): # corresponding config names don't have 'irmc_' prefix opt = {param: info.get(param, CONF.irmc.get(param[len('irmc_'):])) for param in OPTIONAL_PROPERTIES} - d_info = dict(req, **opt) + opt_driver_info = {param: info.get(param) + for param in OPTIONAL_DRIVER_INFO_PROPERTIES} + d_info = dict(req, **opt, **opt_driver_info) d_info['irmc_port'] = utils.validate_network_port( d_info['irmc_port'], 'irmc_port') @@ -128,6 +162,38 @@ def parse_driver_info(node): else: error_msgs.append( _("'irmc_snmp_security' has to be set for SNMP version 3.")) + + verify_ca = d_info.get('irmc_verify_ca') + if verify_ca is None: + d_info['irmc_verify_ca'] = verify_ca = CONF.webserver_verify_ca + + # Check if verify_ca is a Boolean or a file/directory in the file-system + if isinstance(verify_ca, str): + if ((os.path.isdir(verify_ca) and os.path.isabs(verify_ca)) + or (os.path.isfile(verify_ca) and os.path.isabs(verify_ca))): + # If it's fullpath and dir/file, we don't need to do anything + pass + else: + try: + d_info['irmc_verify_ca'] = strutils.bool_from_string( + verify_ca, strict=True) + except ValueError: + error_msgs.append( + _('Invalid value type set in driver_info/' + 'irmc_verify_ca on node %(node)s. ' + 'The value should be a Boolean or the path ' + 'to a file/directory, not "%(value)s"' + ) % {'value': verify_ca, 'node': node.uuid}) + elif isinstance(verify_ca, bool): + # If it's a boolean it's grand, we don't need to do anything + pass + else: + error_msgs.append( + _('Invalid value type set in driver_info/irmc_verify_ca ' + 'on node %(node)s. The value should be a Boolean or the path ' + 'to a file/directory, not "%(value)s"') % {'value': verify_ca, + 'node': node.uuid}) + if error_msgs: msg = (_("The following errors were encountered while parsing " "driver_info:\n%s") % "\n".join(error_msgs)) @@ -147,16 +213,30 @@ def get_irmc_client(node): :raises: InvalidParameterValue on invalid inputs. :raises: MissingParameterValue if some mandatory information is missing on the node + :raises: IRMCOperationError if iRMC operation failed """ driver_info = parse_driver_info(node) - scci_client = scci.get_client( - driver_info['irmc_address'], - driver_info['irmc_username'], - driver_info['irmc_password'], - port=driver_info['irmc_port'], - auth_method=driver_info['irmc_auth_method'], - client_timeout=driver_info['irmc_client_timeout']) + if scci_support_certification(): + scci_client = scci.get_client( + driver_info['irmc_address'], + driver_info['irmc_username'], + driver_info['irmc_password'], + port=driver_info['irmc_port'], + auth_method=driver_info['irmc_auth_method'], + verify=driver_info.get('irmc_verify_ca'), + client_timeout=driver_info['irmc_client_timeout']) + else: + if driver_info['irmc_port'] == 443: + LOG.warning("Installed version of python-scciclient doesn't " + "support certification on HTTPS connection.") + scci_client = scci.get_client( + driver_info['irmc_address'], + driver_info['irmc_username'], + driver_info['irmc_password'], + port=driver_info['irmc_port'], + auth_method=driver_info['irmc_auth_method'], + client_timeout=driver_info['irmc_client_timeout']) return scci_client @@ -192,13 +272,27 @@ def get_irmc_report(node): """ driver_info = parse_driver_info(node) - return scci.get_report( - driver_info['irmc_address'], - driver_info['irmc_username'], - driver_info['irmc_password'], - port=driver_info['irmc_port'], - auth_method=driver_info['irmc_auth_method'], - client_timeout=driver_info['irmc_client_timeout']) + if scci_support_certification(): + report = scci.get_report( + driver_info['irmc_address'], + driver_info['irmc_username'], + driver_info['irmc_password'], + port=driver_info['irmc_port'], + auth_method=driver_info['irmc_auth_method'], + verify=driver_info.get('irmc_verify_ca'), + client_timeout=driver_info['irmc_client_timeout']) + else: + if driver_info['irmc_port'] == 443: + LOG.warning("Installed version of python-scciclient doesn't " + "support certification on HTTPS connection.") + report = scci.get_report( + driver_info['irmc_address'], + driver_info['irmc_username'], + driver_info['irmc_password'], + port=driver_info['irmc_port'], + auth_method=driver_info['irmc_auth_method'], + client_timeout=driver_info['irmc_client_timeout']) + return report def get_secure_boot_mode(node): diff --git a/ironic/tests/unit/drivers/modules/irmc/test_boot.py b/ironic/tests/unit/drivers/modules/irmc/test_boot.py index e84404f751..54f92967e5 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_boot.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_boot.py @@ -63,6 +63,7 @@ PARSED_IFNO = { 'irmc_snmp_version': 'v2c', 'irmc_snmp_security': None, 'irmc_sensor_method': 'ipmitool', + 'irmc_verify_ca': True, } diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py index 3f0c3d94a9..c5c70bf953 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_common.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py @@ -16,6 +16,7 @@ Test class for common methods used by iRMC modules. """ +import os from unittest import mock from oslo_config import cfg @@ -68,6 +69,7 @@ class IRMCValidateParametersTestCase(BaseIRMCTest): self.assertEqual(161, info['irmc_snmp_port']) self.assertEqual('public', info['irmc_snmp_community']) self.assertFalse(info['irmc_snmp_security']) + self.assertTrue(info['irmc_verify_ca']) def test_parse_driver_option_default(self): self.node.driver_info = { @@ -81,6 +83,7 @@ class IRMCValidateParametersTestCase(BaseIRMCTest): self.assertEqual(443, info['irmc_port']) self.assertEqual(60, info['irmc_client_timeout']) self.assertEqual('ipmitool', info['irmc_sensor_method']) + self.assertEqual(True, info['irmc_verify_ca']) def test_parse_driver_info_missing_address(self): del self.node.driver_info['irmc_address'] @@ -153,25 +156,176 @@ class IRMCValidateParametersTestCase(BaseIRMCTest): self.assertRaises(exception.InvalidParameterValue, irmc_common.parse_driver_info, self.node) + @mock.patch.object(os.path, 'isabs', return_value=True, autospec=True) + @mock.patch.object(os.path, 'isdir', return_value=True, autospec=True) + def test_parse_driver_info_dir_path_verify_ca(self, mock_isdir, + mock_isabs): + fake_path = 'absolute/path/to/a/valid/CA' + self.node.driver_info['irmc_verify_ca'] = fake_path + info = irmc_common.parse_driver_info(self.node) + self.assertEqual(fake_path, info['irmc_verify_ca']) + mock_isdir.assert_called_once_with(fake_path) + mock_isabs.assert_called_once_with(fake_path) + + @mock.patch.object(os.path, 'isabs', return_value=True, autospec=True) + @mock.patch.object(os.path, 'isfile', return_value=True, autospec=True) + def test_parse_driver_info_file_path_verify_ca(self, mock_isfile, + mock_isabs): + fake_path = 'absolute/path/to/a/valid/ca.pem' + self.node.driver_info['irmc_verify_ca'] = fake_path + info = irmc_common.parse_driver_info(self.node) + self.assertEqual(fake_path, info['irmc_verify_ca']) + mock_isfile.assert_called_once_with(fake_path) + mock_isabs.assert_called_once_with(fake_path) + + def test_parse_driver_info_string_bool_verify_ca(self): + self.node.driver_info['irmc_verify_ca'] = "False" + info = irmc_common.parse_driver_info(self.node) + self.assertFalse(info['irmc_verify_ca']) + + def test_parse_driver_info_invalid_verify_ca(self): + self.node.driver_info['irmc_verify_ca'] = "1234" + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + self.node.driver_info['irmc_verify_ca'] = 1234 + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + class IRMCCommonMethodsTestCase(BaseIRMCTest): + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) @mock.patch.object(irmc_common, 'scci', spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) - def test_get_irmc_client(self, mock_scci): - self.info['irmc_port'] = 80 - self.info['irmc_auth_method'] = 'digest' - self.info['irmc_client_timeout'] = 60 - mock_scci.get_client.return_value = 'get_client' - returned_mock_scci_get_client = irmc_common.get_irmc_client(self.node) - mock_scci.get_client.assert_called_with( - self.info['irmc_address'], - self.info['irmc_username'], - self.info['irmc_password'], - port=self.info['irmc_port'], - auth_method=self.info['irmc_auth_method'], - client_timeout=self.info['irmc_client_timeout']) - self.assertEqual('get_client', returned_mock_scci_get_client) + def test_get_irmc_client_cert_support_http(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.2', '0.8.3.1', '0.9.4', '0.10.1', '0.10.2', + '0.11.3', '0.11.4', '0.12.0', '0.12.1'] + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 80 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + self.info['irmc_verify_ca'] = True + mock_scci.get_client.return_value = 'get_client' + returned_mock_scci_get_client = irmc_common.get_irmc_client( + self.node) + mock_scci.get_client.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + verify=self.info['irmc_verify_ca'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_client', returned_mock_scci_get_client) + mock_LOG.warning.assert_not_called() + mock_LOG.warning.reset_mock() + + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_client_cert_support_https(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.2', '0.8.3.1', '0.9.4', '0.10.1', '0.10.2', + '0.11.3', '0.11.4', '0.12.0', '0.12.1'] + self.node.driver_info = { + "irmc_address": "1.2.3.4", + "irmc_username": "admin0", + "irmc_password": "fake0", + "irmc_port": "443", + "irmc_auth_method": "digest", + } + + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 443 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + self.info['irmc_verify_ca'] = True + mock_scci.get_client.return_value = 'get_client' + returned_mock_scci_get_client = irmc_common.get_irmc_client( + self.node) + mock_scci.get_client.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + verify=self.info['irmc_verify_ca'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_client', returned_mock_scci_get_client) + mock_LOG.warning.assert_not_called() + mock_LOG.warning.reset_mock() + + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_client_no_cert_support_http(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.0', '0.8.1', '0.9.0', '0.9.3', '0.10.0', + '0.11.2'] + + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 80 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + mock_scci.get_client.return_value = 'get_client' + returned_mock_scci_get_client = irmc_common.get_irmc_client( + self.node) + mock_scci.get_client.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_client', returned_mock_scci_get_client) + mock_LOG.warning.assert_not_called() + mock_LOG.warning.reset_mock() + + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_client_no_cert_support_https(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.0', '0.8.1', '0.9.0', '0.9.3', '0.10.0', + '0.11.2'] + self.node.driver_info = { + "irmc_address": "1.2.3.4", + "irmc_username": "admin0", + "irmc_password": "fake0", + "irmc_port": "443", + "irmc_auth_method": "digest", + } + + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 443 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + mock_scci.get_client.return_value = 'get_client' + returned_mock_scci_get_client = irmc_common.get_irmc_client( + self.node) + mock_scci.get_client.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_client', returned_mock_scci_get_client) + mock_LOG.warning.assert_called_once() + mock_LOG.warning.reset_mock() def test_update_ipmi_properties(self): with task_manager.acquire(self.context, self.node.uuid, @@ -187,22 +341,139 @@ class IRMCCommonMethodsTestCase(BaseIRMCTest): expected_info = dict(self.info, **ipmi_info) self.assertEqual(expected_info, actual_info) + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) @mock.patch.object(irmc_common, 'scci', spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) - def test_get_irmc_report(self, mock_scci): - self.info['irmc_port'] = 80 - self.info['irmc_auth_method'] = 'digest' - self.info['irmc_client_timeout'] = 60 - mock_scci.get_report.return_value = 'get_report' - returned_mock_scci_get_report = irmc_common.get_irmc_report(self.node) - mock_scci.get_report.assert_called_with( - self.info['irmc_address'], - self.info['irmc_username'], - self.info['irmc_password'], - port=self.info['irmc_port'], - auth_method=self.info['irmc_auth_method'], - client_timeout=self.info['irmc_client_timeout']) - self.assertEqual('get_report', returned_mock_scci_get_report) + def test_get_irmc_report_cert_support_http(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.2', '0.8.3.1', '0.9.4', '0.10.1', '0.10.2', + '0.11.3', '0.11.4', '0.12.0', '0.12.1'] + + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 80 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + self.info['irmc_verify_ca'] = True + mock_scci.get_report.return_value = 'get_report' + returned_mock_scci_get_report = irmc_common.get_irmc_report( + self.node) + mock_scci.get_report.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + verify=self.info['irmc_verify_ca'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_report', returned_mock_scci_get_report) + mock_LOG.warning.assert_not_called() + mock_LOG.warning.reset_mock() + + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_report_cert_support_https(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.2', '0.8.3.1', '0.9.4', '0.10.1', '0.10.2', + '0.11.3', '0.11.4', '0.12.0', '0.12.1'] + self.node.driver_info = { + "irmc_address": "1.2.3.4", + "irmc_username": "admin0", + "irmc_password": "fake0", + "irmc_port": "443", + "irmc_auth_method": "digest", + } + + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 443 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + self.info['irmc_verify_ca'] = True + mock_scci.get_report.return_value = 'get_report' + returned_mock_scci_get_report = irmc_common.get_irmc_report( + self.node) + mock_scci.get_report.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + verify=self.info['irmc_verify_ca'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_report', returned_mock_scci_get_report) + mock_LOG.warning.assert_not_called() + mock_LOG.warning.reset_mock() + + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_report_no_cert_support_http(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.0', '0.8.1', '0.9.0', '0.9.3', '0.10.0', + '0.11.2'] + + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 80 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + mock_scci.get_report.return_value = 'get_report' + returned_mock_scci_get_report = irmc_common.get_irmc_report( + self.node) + mock_scci.get_report.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_report', returned_mock_scci_get_report) + mock_LOG.warning.assert_not_called() + mock_LOG.warning.reset_mock() + + @mock.patch.object(irmc_common, 'LOG', autospec=True) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_report_no_cert_support_https(self, mock_scci, + mock_scciclient, mock_LOG): + scci_version_list = ['0.8.0', '0.8.1', '0.9.0', '0.9.3', '0.10.0', + '0.11.2'] + self.node.driver_info = { + "irmc_address": "1.2.3.4", + "irmc_username": "admin0", + "irmc_password": "fake0", + "irmc_port": "443", + "irmc_auth_method": "digest", + } + + for ver in scci_version_list: + with self.subTest(ver=ver): + mock_scciclient.__version__ = ver + self.info['irmc_port'] = 443 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + mock_scci.get_report.return_value = 'get_report' + returned_mock_scci_get_report = irmc_common.get_irmc_report( + self.node) + mock_scci.get_report.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_report', returned_mock_scci_get_report) + mock_LOG.warning.assert_called_once() + mock_LOG.warning.reset_mock() def test_out_range_port(self): self.assertRaises(ValueError, cfg.CONF.set_override, diff --git a/ironic/tests/unit/drivers/modules/irmc/test_power.py b/ironic/tests/unit/drivers/modules/irmc/test_power.py index c4142202c7..1449bc8adb 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_power.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_power.py @@ -211,6 +211,7 @@ class IRMCPowerInternalMethodsTestCase(test_common.BaseIRMCTest): _wait_power_state_mock.assert_called_once_with(task, target_state, timeout=None) + @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__']) @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, autospec=True) @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed', @@ -218,7 +219,8 @@ class IRMCPowerInternalMethodsTestCase(test_common.BaseIRMCTest): def test__set_power_state_invalid_target_state( self, attach_boot_iso_if_needed_mock, - _wait_power_state_mock): + _wait_power_state_mock, mock_scciclient): + mock_scciclient.__version__ = '0.8.2' with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: self.assertRaises(exception.InvalidParameterValue, diff --git a/ironic/tests/unit/drivers/modules/irmc/test_raid.py b/ironic/tests/unit/drivers/modules/irmc/test_raid.py index eefe7ff3a5..54a0a0dd62 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_raid.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_raid.py @@ -702,8 +702,8 @@ class IRMCRaidConfigurationInternalMethodsTestCase(test_common.BaseIRMCTest): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: raid._commit_raid_config(task) - get_raid_adapter_mock.assert_called_once_with( - irmc_common.parse_driver_info(task.node)) + irmc_info = irmc_common.parse_driver_info(task.node) + get_raid_adapter_mock.assert_called_once_with(irmc_info) update_raid_info_mock.assert_called_once_with( task.node, task.node.raid_config) set_async_step_flags_mock.assert_called_once_with( diff --git a/lower-constraints.txt b/lower-constraints.txt index b51252aa28..72fced77ed 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -33,6 +33,7 @@ oslo.utils==4.5.0 oslo.versionedobjects==1.31.2 osprofiler==1.5.0 os-traits==0.4.0 +packaging==16.5 pbr==3.1.1 pecan==1.0.0 psutil==3.2.2 diff --git a/releasenotes/notes/irmc-add-certification-file-option-34e7a0062c768e58.yaml b/releasenotes/notes/irmc-add-certification-file-option-34e7a0062c768e58.yaml new file mode 100644 index 0000000000..47b2aa6707 --- /dev/null +++ b/releasenotes/notes/irmc-add-certification-file-option-34e7a0062c768e58.yaml @@ -0,0 +1,18 @@ +--- +fixes: + - | + Adds ``driver_info/irmc_verify_ca`` option to specify certification file. + Default value of driver_info/irmc_verify_ca is True. +security: + - | + Modifies the ``irmc`` hardware type to include a capability to control + enforcement of HTTPS certificate verification. By default this is enforced. + python-scciclient version must be one of >=0.8.2,<0.9.0, >=0.9.4,<0.10.0, + >=0.10.1,<0.11.0, >=0.11.3,<0.12.0 or >=0.12.0,<0.13.0 + Or certificate verification will not occur. +upgrade: + - | + On Yoga release, to use certification file on HTTPS connection, + iRMC driver requires python-scciclient version to be one of >=0.8.2,<0.9.0, + >=0.9.4,<0.10.0, >=0.10.1,<0.11.0, >=0.11.3,<0.12.0 or >=0.12.0,<0.13.0 + and packaging >=16.5 diff --git a/requirements.txt b/requirements.txt index ea11b920d0..4523817c83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -44,3 +44,4 @@ futurist>=1.2.0 # Apache-2.0 tooz>=2.7.0 # Apache-2.0 openstacksdk>=0.48.0 # Apache-2.0 sushy>=3.10.0 +packaging>=16.5 # Apache-2.0