From 37590a86338e94845c74e1b1f7d8d6f61003d447 Mon Sep 17 00:00:00 2001 From: Naohiro Tamura Date: Fri, 26 Jun 2015 00:00:42 +0900 Subject: [PATCH] Add hardware inspection module for iRMC driver This module enables iRMC out-of-band hardware inspection for FUJITSU PRIMERGY bare metal nodes having iRMC S4 and beyond. Change-Id: I8f406a9beb3fd3c01b15f764211ffd18494464f6 Closes-Bug: #1525108 --- etc/ironic/ironic.conf.sample | 14 + ironic/drivers/fake.py | 2 + ironic/drivers/irmc.py | 3 + ironic/drivers/modules/irmc/common.py | 44 +++- ironic/drivers/modules/irmc/inspect.py | 187 +++++++++++++ ironic/drivers/modules/snmp.py | 28 ++ ironic/drivers/pxe.py | 2 + .../unit/drivers/modules/irmc/test_common.py | 32 +++ .../unit/drivers/modules/irmc/test_inspect.py | 248 ++++++++++++++++++ .../tests/unit/drivers/modules/test_snmp.py | 38 +++ ironic/tests/unit/drivers/test_irmc.py | 2 + .../drivers/third_party_driver_mock_specs.py | 1 + .../irmc-oob-inspection-6d072c60f6c88ecb.yaml | 3 + 13 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 ironic/drivers/modules/irmc/inspect.py create mode 100644 ironic/tests/unit/drivers/modules/irmc/test_inspect.py create mode 100644 releasenotes/notes/irmc-oob-inspection-6d072c60f6c88ecb.yaml diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index f80748dce3..dc7a27ba9c 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -1283,6 +1283,20 @@ # (string value) #sensor_method=ipmitool +# SNMP protocol version, either "v1", "v2c" or "v3" (string +# value) +#snmp_version=v2c + +# SNMP port (port value) +#snmp_port=161 + +# SNMP community. Required for versions "v1" and "v2c" (string +# value) +#snmp_community=public + +# SNMP security name. Required for version "v3" (string value) +#snmp_security= + [keystone] diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py index 619918eee9..e4895881af 100644 --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -37,6 +37,7 @@ from ironic.drivers.modules.ilo import power as ilo_power from ironic.drivers.modules import inspector from ironic.drivers.modules import ipminative from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import inspect as irmc_inspect from ironic.drivers.modules.irmc import management as irmc_management from ironic.drivers.modules.irmc import power as irmc_power from ironic.drivers.modules import iscsi_deploy @@ -210,6 +211,7 @@ class FakeIRMCDriver(base.BaseDriver): self.power = irmc_power.IRMCPower() self.deploy = fake.FakeDeploy() self.management = irmc_management.IRMCManagement() + self.inspect = irmc_inspect.IRMCInspect() class FakeVirtualBoxDriver(base.BaseDriver): diff --git a/ironic/drivers/irmc.py b/ironic/drivers/irmc.py index 3ed6ddd235..4b5b4fc900 100644 --- a/ironic/drivers/irmc.py +++ b/ironic/drivers/irmc.py @@ -24,6 +24,7 @@ from ironic.drivers import base from ironic.drivers.modules import agent from ironic.drivers.modules import ipmitool from ironic.drivers.modules.irmc import boot +from ironic.drivers.modules.irmc import inspect from ironic.drivers.modules.irmc import management from ironic.drivers.modules.irmc import power from ironic.drivers.modules import iscsi_deploy @@ -50,6 +51,7 @@ class IRMCVirtualMediaIscsiDriver(base.BaseDriver): self.console = ipmitool.IPMIShellinaboxConsole() self.management = management.IRMCManagement() self.vendor = iscsi_deploy.VendorPassthru() + self.inspect = inspect.IRMCInspect() class IRMCVirtualMediaAgentDriver(base.BaseDriver): @@ -74,3 +76,4 @@ class IRMCVirtualMediaAgentDriver(base.BaseDriver): self.console = ipmitool.IPMIShellinaboxConsole() self.management = management.IRMCManagement() self.vendor = agent.AgentVendorInterface() + self.inspect = inspect.IRMCInspect() diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py index 4b95315b25..689c4ce31f 100644 --- a/ironic/drivers/modules/irmc/common.py +++ b/ironic/drivers/modules/irmc/common.py @@ -15,6 +15,7 @@ """ Common functionalities shared between different iRMC modules. """ +import six from oslo_config import cfg from oslo_log import log as logging @@ -41,6 +42,18 @@ opts = [ default='ipmitool', help=_('Sensor data retrieval method, either ' '"ipmitool" or "scci"')), + cfg.StrOpt('snmp_version', + default='v2c', + choices=['v1', 'v2c', 'v3'], + help=_('SNMP protocol version, either "v1", "v2c" or "v3"')), + cfg.PortOpt('snmp_port', + default=161, + help=_('SNMP port')), + cfg.StrOpt('snmp_community', + default='public', + help=_('SNMP community. Required for versions "v1" and "v2c"')), + cfg.StrOpt('snmp_security', + help=_('SNMP security name. Required for version "v3"')), ] CONF = cfg.CONF @@ -65,6 +78,14 @@ OPTIONAL_PROPERTIES = { 'irmc_sensor_method': _("Sensor data retrieval method; either " "'ipmitool' or 'scci'. The default value is " "'ipmitool'. Optional."), + 'irmc_snmp_version': _("SNMP protocol version; either 'v1', 'v2c', or " + "'v3'. The default value is 'v2c'. Optional."), + 'irmc_snmp_port': _("SNMP port. The default is 161. Optional."), + 'irmc_snmp_community': _("SNMP community required for versions 'v1' and " + "'v2c'. The default value is 'public'. " + "Optional."), + 'irmc_snmp_security': _("SNMP security name required for version 'v3'. " + "Optional."), } COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() @@ -97,7 +118,7 @@ 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(list(req.items()) + list(opt.items())) + d_info = dict(req, **opt) error_msgs = [] if (d_info['irmc_auth_method'].lower() not in ('basic', 'digest')): @@ -112,6 +133,25 @@ def parse_driver_info(node): if d_info['irmc_sensor_method'].lower() not in ('ipmitool', 'scci'): error_msgs.append( _("'irmc_sensor_method' has unsupported value.")) + if d_info['irmc_snmp_version'].lower() not in ('v1', 'v2c', 'v3'): + error_msgs.append( + _("'irmc_snmp_version' has unsupported value.")) + if not isinstance(d_info['irmc_snmp_port'], int): + error_msgs.append( + _("'irmc_snmp_port' is not integer type.")) + if (d_info['irmc_snmp_version'].lower() in ('v1', 'v2c') and + d_info['irmc_snmp_community'] and + not isinstance(d_info['irmc_snmp_community'], six.string_types)): + error_msgs.append( + _("'irmc_snmp_community' is not string type.")) + if d_info['irmc_snmp_version'].lower() == 'v3': + if d_info['irmc_snmp_security']: + if not isinstance(d_info['irmc_snmp_security'], six.string_types): + error_msgs.append( + _("'irmc_snmp_security' is not string type.")) + else: + error_msgs.append( + _("'irmc_snmp_security' has to be set for SNMP version 3.")) if error_msgs: msg = (_("The following type errors were encountered while parsing " "driver_info:\n%s") % "\n".join(error_msgs)) @@ -145,7 +185,7 @@ def get_irmc_client(node): def update_ipmi_properties(task): - """Update ipmi properties to node driver_info + """Update ipmi properties to node driver_info. :param task: A task from TaskManager. """ diff --git a/ironic/drivers/modules/irmc/inspect.py b/ironic/drivers/modules/irmc/inspect.py new file mode 100644 index 0000000000..8eece97b85 --- /dev/null +++ b/ironic/drivers/modules/irmc/inspect.py @@ -0,0 +1,187 @@ +# Copyright 2015 FUJITSU LIMITED +# +# 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. +""" +iRMC Inspect Interface +""" + +from oslo_log import log as logging +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common.i18n import _LI +from ironic.common.i18n import _LW +from ironic.common import states +from ironic.drivers import base +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules import snmp +from ironic import objects + +scci = importutils.try_import('scciclient.irmc.scci') + +LOG = logging.getLogger(__name__) + +""" +SC2.mib: sc2UnitNodeClass returns NIC type. + +sc2UnitNodeClass OBJECT-TYPE + SYNTAX INTEGER + { + unknown(1), + primary(2), + secondary(3), + management-blade(4), + secondary-remote(5), + secondary-remote-backup(6), + baseboard-controller(7) + } + ACCESS read-only + STATUS mandatory + DESCRIPTION "Management node class: + primary: local operating system interface + secondary: local management controller LAN interface + management-blade: management blade interface (in a blade server + chassis) + secondary-remote: remote management controller (in an RSB + concentrator environment) + secondary-remote-backup: backup remote management controller + baseboard-controller: local baseboard management controller (BMC)" + ::= { sc2ManagementNodes 8 } +""" +NODE_CLASS_OID_VALUE = { + 'unknown': 1, + 'primary': 2, + 'secondary': 3, + 'management-blade': 4, + 'secondary-remote': 5, + 'secondary-remote-backup': 6, + 'baseboard-controller': 7 +} + +NODE_CLASS_OID = '1.3.6.1.4.1.231.2.10.2.2.10.3.1.1.8.1' + +""" +SC2.mib: sc2UnitNodeMacAddress returns NIC MAC address + +sc2UnitNodeMacAddress OBJECT-TYPE + SYNTAX PhysAddress + ACCESS read-only + STATUS mandatory + DESCRIPTION "Management node hardware (MAC) address" + ::= { sc2ManagementNodes 9 } +""" +MAC_ADDRESS_OID = '1.3.6.1.4.1.231.2.10.2.2.10.3.1.1.9.1' + + +def _get_mac_addresses(node): + """Get mac addresses of the node. + + :param node: node object. + :raises: SNMPFailure if SNMP operation failed. + :returns: a list of mac addresses. + """ + d_info = irmc_common.parse_driver_info(node) + snmp_client = snmp.SNMPClient(d_info['irmc_address'], + d_info['irmc_snmp_port'], + d_info['irmc_snmp_version'], + d_info['irmc_snmp_community'], + d_info['irmc_snmp_security']) + + node_classes = snmp_client.get_next(NODE_CLASS_OID) + mac_addresses = snmp_client.get_next(MAC_ADDRESS_OID) + + return [a for c, a in zip(node_classes, mac_addresses) + if c == NODE_CLASS_OID_VALUE['primary']] + + +def _inspect_hardware(node): + """Inspect the node and get hardware information. + + :param node: node object. + :raises: HardwareInspectionFailure, if unable to get essential + hardware properties. + :returns: a pair of dictionary and list, the dictionary contains + keys as in IRMCInspect.ESSENTIAL_PROPERTIES and its inspected + values, the list contains mac addresses. + """ + try: + report = irmc_common.get_irmc_report(node) + props = scci.get_essential_properties( + report, IRMCInspect.ESSENTIAL_PROPERTIES) + macs = _get_mac_addresses(node) + except (scci.SCCIInvalidInputError, + scci.SCCIClientError, + exception.SNMPFailure) as e: + error = (_("Inspection failed for node %(node_id)s " + "with the following error: %(error)s") % + {'node_id': node.uuid, 'error': e}) + raise exception.HardwareInspectionFailure(error=error) + + return (props, macs) + + +class IRMCInspect(base.InspectInterface): + """Interface for out of band inspection.""" + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return irmc_common.COMMON_PROPERTIES + + def validate(self, task): + """Validate the driver-specific inspection information. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if required driver_info attribute + is missing or invalid on the node. + :raises: MissingParameterValue if a required parameter is missing. + """ + irmc_common.parse_driver_info(task.node) + + def inspect_hardware(self, task): + """Inspect hardware. + + Inspect hardware to obtain the essential hardware properties and + mac addresses. + + :param task: a task from TaskManager. + :raises: HardwareInspectionFailure, if hardware inspection failed. + :returns: states.MANAGEABLE, if hardware inspection succeeded. + """ + node = task.node + (props, macs) = _inspect_hardware(node) + node.properties = dict(node.properties, **props) + node.save() + + for mac in macs: + try: + new_port = objects.Port(task.context, + address=mac, node_id=node.id) + new_port.create() + LOG.info(_LI("Port created for MAC address %(address)s " + "for node %(node_uuid)s during inspection"), + {'address': mac, 'node_uuid': node.uuid}) + except exception.MACAlreadyExists: + LOG.warning(_LW("Port already existed for MAC address " + "%(address)s for node %(node_uuid)s " + "during inspection"), + {'address': mac, 'node_uuid': node.uuid}) + + LOG.info(_LI("Node %s inspected"), node.uuid) + return states.MANAGEABLE diff --git a/ironic/drivers/modules/snmp.py b/ironic/drivers/modules/snmp.py index 1eb7c9eaf4..4787b56368 100644 --- a/ironic/drivers/modules/snmp.py +++ b/ironic/drivers/modules/snmp.py @@ -175,6 +175,34 @@ class SNMPClient(object): name, val = var_binds[0] return val + def get_next(self, oid): + """Use PySNMP to perform an SNMP GET NEXT operation on a table object. + + :param oid: The OID of the object to get. + :raises: SNMPFailure if an SNMP request fails. + :returns: A list of values of the requested table object. + """ + try: + results = self.cmd_gen.nextCmd(self._get_auth(), + self._get_transport(), + oid) + except snmp_error.PySnmpError as e: + raise exception.SNMPFailure(operation="GET_NEXT", error=e) + + error_indication, error_status, error_index, var_bind_table = results + + if error_indication: + # SNMP engine-level error. + raise exception.SNMPFailure(operation="GET_NEXT", + error=error_indication) + + if error_status: + # SNMP PDU error. + raise exception.SNMPFailure(operation="GET_NEXT", + error=error_status.prettyPrint()) + + return [val for row in var_bind_table for name, val in row] + def set(self, oid, value): """Use PySNMP to perform an SNMP SET operation on a single object. diff --git a/ironic/drivers/pxe.py b/ironic/drivers/pxe.py index 39f9671a8d..e4b84c4a78 100644 --- a/ironic/drivers/pxe.py +++ b/ironic/drivers/pxe.py @@ -38,6 +38,7 @@ from ironic.drivers.modules.ilo import vendor as ilo_vendor from ironic.drivers.modules import inspector from ironic.drivers.modules import ipminative from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import inspect as irmc_inspect from ironic.drivers.modules.irmc import management as irmc_management from ironic.drivers.modules.irmc import power as irmc_power from ironic.drivers.modules import iscsi_deploy @@ -263,6 +264,7 @@ class PXEAndIRMCDriver(base.BaseDriver): self.deploy = iscsi_deploy.ISCSIDeploy() self.management = irmc_management.IRMCManagement() self.vendor = iscsi_deploy.VendorPassthru() + self.inspect = irmc_inspect.IRMCInspect() class PXEAndVirtualBoxDriver(base.BaseDriver): diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py index ded48def13..0143ba6390 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_common.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py @@ -48,6 +48,10 @@ class IRMCValidateParametersTestCase(db_base.DbTestCase): self.assertIsNotNone(info.get('irmc_port')) self.assertIsNotNone(info.get('irmc_auth_method')) self.assertIsNotNone(info.get('irmc_sensor_method')) + self.assertIsNotNone(info.get('irmc_snmp_version')) + self.assertIsNotNone(info.get('irmc_snmp_port')) + self.assertIsNotNone(info.get('irmc_snmp_community')) + self.assertFalse(info.get('irmc_snmp_security')) def test_parse_driver_option_default(self): self.node.driver_info = { @@ -107,6 +111,34 @@ class IRMCValidateParametersTestCase(db_base.DbTestCase): self.assertIn('irmc_password', str(e)) self.assertIn('irmc_address', str(e)) + def test_parse_driver_info_invalid_snmp_version(self): + self.node.driver_info['irmc_snmp_version'] = 'v3x' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_snmp_port(self): + self.node.driver_info['irmc_snmp_port'] = '161' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_snmp_community(self): + self.node.driver_info['irmc_snmp_version'] = 'v2c' + self.node.driver_info['irmc_snmp_community'] = 100 + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_snmp_security(self): + self.node.driver_info['irmc_snmp_version'] = 'v3' + self.node.driver_info['irmc_snmp_security'] = 100 + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_empty_snmp_security(self): + self.node.driver_info['irmc_snmp_version'] = 'v3' + self.node.driver_info['irmc_snmp_security'] = '' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + class IRMCCommonMethodsTestCase(db_base.DbTestCase): diff --git a/ironic/tests/unit/drivers/modules/irmc/test_inspect.py b/ironic/tests/unit/drivers/modules/irmc/test_inspect.py new file mode 100644 index 0000000000..cf89b89e19 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_inspect.py @@ -0,0 +1,248 @@ +# Copyright 2015 FUJITSU LIMITED +# +# 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. + +""" +Test class for iRMC Inspection Driver +""" + +import mock + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import inspect as irmc_inspect +from ironic import objects +from ironic.tests.unit.conductor import mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_irmc_info() + + +class IRMCInspectInternalMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCInspectInternalMethodsTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver='fake_irmc') + self.node = obj_utils.create_test_node(self.context, + driver='fake_irmc', + driver_info=driver_info) + + @mock.patch('ironic.drivers.modules.irmc.inspect.snmp.SNMPClient', + spec_set=True, autospec=True) + def test__get_mac_addresses(self, snmpclient_mock): + snmpclient_mock.return_value = mock.Mock( + **{'get_next.side_effect': [[2, 2, 7], + ['aa:aa:aa:aa:aa:aa', + 'bb:bb:bb:bb:bb:bb', + 'cc:cc:cc:cc:cc:cc']]}) + inspected_macs = ['aa:aa:aa:aa:aa:aa', 'bb:bb:bb:bb:bb:bb'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + result = irmc_inspect._get_mac_addresses(task.node) + self.assertEqual(inspected_macs, result) + + @mock.patch.object(irmc_inspect, '_get_mac_addresses', spec_set=True, + autospec=True) + @mock.patch.object(irmc_inspect, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + @mock.patch.object(irmc_common, 'get_irmc_report', spec_set=True, + autospec=True) + def test__inspect_hardware( + self, get_irmc_report_mock, scci_mock, _get_mac_addresses_mock): + inspected_props = { + 'memory_mb': '1024', + 'local_gb': 10, + 'cpus': 2, + 'cpu_arch': 'x86_64'} + inspected_macs = ['aa:aa:aa:aa:aa:aa', 'bb:bb:bb:bb:bb:bb'] + report = 'fake_report' + get_irmc_report_mock.return_value = report + scci_mock.get_essential_properties.return_value = inspected_props + _get_mac_addresses_mock.return_value = inspected_macs + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + result = irmc_inspect._inspect_hardware(task.node) + + get_irmc_report_mock.assert_called_once_with(task.node) + scci_mock.get_essential_properties.assert_called_once_with( + report, irmc_inspect.IRMCInspect.ESSENTIAL_PROPERTIES) + self.assertEqual((inspected_props, inspected_macs), result) + + @mock.patch.object(irmc_inspect, '_get_mac_addresses', spec_set=True, + autospec=True) + @mock.patch.object(irmc_inspect, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + @mock.patch.object(irmc_common, 'get_irmc_report', spec_set=True, + autospec=True) + def test__inspect_hardware_exception( + self, get_irmc_report_mock, scci_mock, _get_mac_addresses_mock): + report = 'fake_report' + get_irmc_report_mock.return_value = report + side_effect = exception.SNMPFailure("fake exception") + scci_mock.get_essential_properties.side_effect = side_effect + irmc_inspect.scci.SCCIInvalidInputError = Exception + irmc_inspect.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.HardwareInspectionFailure, + irmc_inspect._inspect_hardware, + task.node) + get_irmc_report_mock.assert_called_once_with(task.node) + self.assertFalse(_get_mac_addresses_mock.called) + + +class IRMCInspectTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCInspectTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver="fake_irmc") + self.node = obj_utils.create_test_node(self.context, + driver='fake_irmc', + driver_info=driver_info) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + properties = task.driver.get_properties() + for prop in irmc_common.COMMON_PROPERTIES: + self.assertIn(prop, properties) + + @mock.patch.object(irmc_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, parse_driver_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + parse_driver_info_mock.assert_called_once_with(task.node) + + @mock.patch.object(irmc_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate_fail(self, parse_driver_info_mock): + side_effect = exception.InvalidParameterValue("Invalid Input") + parse_driver_info_mock.side_effect = side_effect + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, + task) + + @mock.patch.object(irmc_inspect.LOG, 'info', spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.irmc.inspect.objects.Port', + spec_set=True, autospec=True) + @mock.patch.object(irmc_inspect, '_inspect_hardware', spec_set=True, + autospec=True) + def test_inspect_hardware(self, _inspect_hardware_mock, port_mock, + info_mock): + inspected_props = { + 'memory_mb': '1024', + 'local_gb': 10, + 'cpus': 2, + 'cpu_arch': 'x86_64'} + inspected_macs = ['aa:aa:aa:aa:aa:aa', 'bb:bb:bb:bb:bb:bb'] + _inspect_hardware_mock.return_value = (inspected_props, + inspected_macs) + new_port_mock1 = mock.MagicMock(spec=objects.Port) + new_port_mock2 = mock.MagicMock(spec=objects.Port) + + port_mock.side_effect = [new_port_mock1, new_port_mock2] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + result = task.driver.inspect.inspect_hardware(task) + + node_id = task.node.id + _inspect_hardware_mock.assert_called_once_with(task.node) + + # note (naohirot): + # as of mock 1.2, assert_has_calls has a bug which returns + # "AssertionError: Calls not found." if mock_calls has class + # method call such as below: + + # AssertionError: Calls not found. + # Expected: [call.list_by_node_id( + # , + # 1)] + # Actual: [call.list_by_node_id( + # , + # 1)] + # + # workaround, remove class method call from mock_calls list + del port_mock.mock_calls[0] + port_mock.assert_has_calls([ + # workaround, comment out class method call from expected list + # mock.call.list_by_node_id(task.context, node_id), + mock.call(task.context, address=inspected_macs[0], + node_id=node_id), + mock.call(task.context, address=inspected_macs[1], + node_id=node_id) + ]) + new_port_mock1.create.assert_called_once_with() + new_port_mock2.create.assert_called_once_with() + + self.assertTrue(info_mock.called) + task.node.refresh() + self.assertEqual(inspected_props, task.node.properties) + self.assertEqual(states.MANAGEABLE, result) + + @mock.patch('ironic.objects.Port', spec_set=True, autospec=True) + @mock.patch.object(irmc_inspect, '_inspect_hardware', spec_set=True, + autospec=True) + def test_inspect_hardware_inspect_exception( + self, _inspect_hardware_mock, port_mock): + side_effect = exception.HardwareInspectionFailure("fake exception") + _inspect_hardware_mock.side_effect = side_effect + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.HardwareInspectionFailure, + task.driver.inspect.inspect_hardware, + task) + self.assertFalse(port_mock.called) + + @mock.patch.object(irmc_inspect.LOG, 'warn', spec_set=True, autospec=True) + @mock.patch('ironic.objects.Port', spec_set=True, autospec=True) + @mock.patch.object(irmc_inspect, '_inspect_hardware', spec_set=True, + autospec=True) + def test_inspect_hardware_mac_already_exist( + self, _inspect_hardware_mock, port_mock, warn_mock): + inspected_props = { + 'memory_mb': '1024', + 'local_gb': 10, + 'cpus': 2, + 'cpu_arch': 'x86_64'} + inspected_macs = ['aa:aa:aa:aa:aa:aa', 'bb:bb:bb:bb:bb:bb'] + _inspect_hardware_mock.return_value = (inspected_props, + inspected_macs) + side_effect = exception.MACAlreadyExists("fake exception") + new_port_mock = port_mock.return_value + new_port_mock.create.side_effect = side_effect + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + result = task.driver.inspect.inspect_hardware(task) + + _inspect_hardware_mock.assert_called_once_with(task.node) + self.assertTrue(port_mock.call_count, 2) + task.node.refresh() + self.assertEqual(inspected_props, task.node.properties) + self.assertEqual(states.MANAGEABLE, result) diff --git a/ironic/tests/unit/drivers/modules/test_snmp.py b/ironic/tests/unit/drivers/modules/test_snmp.py index 1c798e912b..d41fe8b9fa 100644 --- a/ironic/tests/unit/drivers/modules/test_snmp.py +++ b/ironic/tests/unit/drivers/modules/test_snmp.py @@ -101,6 +101,19 @@ class SNMPClientTestCase(base.TestCase): mock_cmdgenerator.getCmd.assert_called_once_with(mock.ANY, mock.ANY, self.oid) + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_get_next(self, mock_auth, mock_transport, mock_cmdgen): + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.nextCmd.return_value = ( + "", None, 0, [[var_bind, var_bind]]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + val = client.get_next(self.oid) + self.assertEqual([self.value, self.value], val) + mock_cmdgenerator.nextCmd.assert_called_once_with(mock.ANY, mock.ANY, + self.oid) + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) def test_get_err_transport(self, mock_auth, mock_transport, mock_cmdgen): @@ -113,6 +126,19 @@ class SNMPClientTestCase(base.TestCase): self.assertRaises(exception.SNMPFailure, client.get, self.oid) self.assertFalse(mock_cmdgenerator.getCmd.called) + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_get_next_err_transport(self, mock_auth, mock_transport, + mock_cmdgen): + mock_transport.side_effect = snmp_error.PySnmpError + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.nextCmd.return_value = ("engine error", None, 0, + [[var_bind, var_bind]]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + self.assertRaises(exception.SNMPFailure, client.get_next, self.oid) + self.assertFalse(mock_cmdgenerator.nextCmd.called) + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) def test_get_err_engine(self, mock_auth, mock_transport, mock_cmdgen): @@ -125,6 +151,18 @@ class SNMPClientTestCase(base.TestCase): mock_cmdgenerator.getCmd.assert_called_once_with(mock.ANY, mock.ANY, self.oid) + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_get_next_err_engine(self, mock_auth, mock_transport, mock_cmdgen): + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.nextCmd.return_value = ("engine error", None, 0, + [[var_bind, var_bind]]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + self.assertRaises(exception.SNMPFailure, client.get_next, self.oid) + mock_cmdgenerator.nextCmd.assert_called_once_with(mock.ANY, mock.ANY, + self.oid) + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) def test_set(self, mock_auth, mock_transport, mock_cmdgen): diff --git a/ironic/tests/unit/drivers/test_irmc.py b/ironic/tests/unit/drivers/test_irmc.py index d2d7a80c43..c4549d56cb 100644 --- a/ironic/tests/unit/drivers/test_irmc.py +++ b/ironic/tests/unit/drivers/test_irmc.py @@ -49,6 +49,7 @@ class IRMCVirtualMediaIscsiTestCase(testtools.TestCase): self.assertIsInstance(driver.management, irmc.management.IRMCManagement) self.assertIsInstance(driver.vendor, iscsi_deploy.VendorPassthru) + self.assertIsInstance(driver.inspect, irmc.inspect.IRMCInspect) @mock.patch.object(irmc.importutils, 'try_import') def test___init___try_import_exception(self, mock_try_import): @@ -91,6 +92,7 @@ class IRMCVirtualMediaAgentTestCase(testtools.TestCase): self.assertIsInstance(driver.management, irmc.management.IRMCManagement) self.assertIsInstance(driver.vendor, irmc.agent.AgentVendorInterface) + self.assertIsInstance(driver.inspect, irmc.inspect.IRMCInspect) @mock.patch.object(irmc.importutils, 'try_import') def test___init___try_import_exception(self, mock_try_import): diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index d1454469ee..5a2da8a448 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -101,6 +101,7 @@ SCCICLIENT_IRMC_SCCI_SPEC = ( 'get_sensor_data', 'get_virtual_cd_set_params_cmd', 'get_virtual_fd_set_params_cmd', + 'get_essential_properties', ) ONEVIEWCLIENT_SPEC = ( diff --git a/releasenotes/notes/irmc-oob-inspection-6d072c60f6c88ecb.yaml b/releasenotes/notes/irmc-oob-inspection-6d072c60f6c88ecb.yaml new file mode 100644 index 0000000000..8a966d27e0 --- /dev/null +++ b/releasenotes/notes/irmc-oob-inspection-6d072c60f6c88ecb.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds out-Of-band inspection support for iRMC driver.