summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--etc/ironic/ironic.conf.sample14
-rw-r--r--ironic/drivers/fake.py2
-rw-r--r--ironic/drivers/irmc.py3
-rw-r--r--ironic/drivers/modules/irmc/common.py44
-rw-r--r--ironic/drivers/modules/irmc/inspect.py187
-rw-r--r--ironic/drivers/modules/snmp.py28
-rw-r--r--ironic/drivers/pxe.py2
-rw-r--r--ironic/tests/unit/drivers/modules/irmc/test_common.py32
-rw-r--r--ironic/tests/unit/drivers/modules/irmc/test_inspect.py248
-rw-r--r--ironic/tests/unit/drivers/modules/test_snmp.py38
-rw-r--r--ironic/tests/unit/drivers/test_irmc.py2
-rw-r--r--ironic/tests/unit/drivers/third_party_driver_mock_specs.py1
-rw-r--r--releasenotes/notes/irmc-oob-inspection-6d072c60f6c88ecb.yaml3
13 files changed, 602 insertions, 2 deletions
diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample
index f80748dce..dc7a27ba9 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=<None>
+
[keystone]
diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py
index 619918eee..e4895881a 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 3ed6ddd23..4b5b4fc90 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 4b95315b2..689c4ce31 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 000000000..8eece97b8
--- /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 <property name>:<property description> 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 1eb7c9eaf..4787b5636 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 39f9671a8..e4b84c4a7 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 ded48def1..0143ba639 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 000000000..cf89b89e1
--- /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(
+ # <oslo_context.context.RequestContext object at 0x7f1a34f8c0d0>,
+ # 1)]
+ # Actual: [call.list_by_node_id(
+ # <oslo_context.context.RequestContext object at 0x7f1a34f8c0d0>,
+ # 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 1c798e912..d41fe8b9f 100644
--- a/ironic/tests/unit/drivers/modules/test_snmp.py
+++ b/ironic/tests/unit/drivers/modules/test_snmp.py
@@ -103,6 +103,19 @@ class SNMPClientTestCase(base.TestCase):
@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):
mock_transport.side_effect = snmp_error.PySnmpError
var_bind = (self.oid, self.value)
@@ -115,6 +128,19 @@ class SNMPClientTestCase(base.TestCase):
@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):
var_bind = (self.oid, self.value)
mock_cmdgenerator = mock_cmdgen.return_value
@@ -127,6 +153,18 @@ class SNMPClientTestCase(base.TestCase):
@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):
var_bind = (self.oid, self.value)
mock_cmdgenerator = mock_cmdgen.return_value
diff --git a/ironic/tests/unit/drivers/test_irmc.py b/ironic/tests/unit/drivers/test_irmc.py
index d2d7a80c4..c4549d56c 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 d1454469e..5a2da8a44 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 000000000..8a966d27e
--- /dev/null
+++ b/releasenotes/notes/irmc-oob-inspection-6d072c60f6c88ecb.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Adds out-Of-band inspection support for iRMC driver.