diff --git a/driver-requirements.txt b/driver-requirements.txt index 939cc13160..370b8c4e62 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -12,7 +12,7 @@ python-oneviewclient<3.0.0,>=2.0.2 python-scciclient>=0.3.0 python-seamicroclient>=0.4.0 UcsSdk==0.8.2.2 -python-dracclient>=0.0.5 +python-dracclient>=0.1.0 # The amt driver imports a python module called "pywsman", but this does not # exist on pypi. diff --git a/ironic/drivers/drac.py b/ironic/drivers/drac.py index 0cb31b6b9b..a8f1275433 100644 --- a/ironic/drivers/drac.py +++ b/ironic/drivers/drac.py @@ -21,6 +21,7 @@ from ironic.common import exception from ironic.common.i18n import _ from ironic.drivers import base from ironic.drivers.modules.drac import deploy +from ironic.drivers.modules.drac import inspect as drac_inspect from ironic.drivers.modules.drac import management from ironic.drivers.modules.drac import power from ironic.drivers.modules.drac import raid @@ -56,4 +57,13 @@ class PXEDracDriver(base.BaseDriver): self.driver_passthru_mapping = {'lookup': self.iscsi_vendor} self.vendor = utils.MixinVendorInterface(self.mapping, self.driver_passthru_mapping) - self.inspect = inspector.Inspector.create_if_enabled('PXEDracDriver') + self.inspect = drac_inspect.DracInspect() + + +class PXEDracInspectorDriver(PXEDracDriver): + """Drac driver using PXE for deploy and OOB inspection interface.""" + + def __init__(self): + super(PXEDracInspectorDriver, self).__init__() + self.inspect = inspector.Inspector.create_if_enabled( + 'PXEDracInspectorDriver') diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py index d4c0c68816..f1ce9e1554 100644 --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -27,6 +27,7 @@ from ironic.drivers.modules.amt import management as amt_mgmt from ironic.drivers.modules.amt import power as amt_power from ironic.drivers.modules.cimc import management as cimc_mgmt from ironic.drivers.modules.cimc import power as cimc_power +from ironic.drivers.modules.drac import inspect as drac_inspect from ironic.drivers.modules.drac import management as drac_mgmt from ironic.drivers.modules.drac import power as drac_power from ironic.drivers.modules.drac import raid as drac_raid @@ -203,6 +204,7 @@ class FakeDracDriver(base.BaseDriver): self.management = drac_mgmt.DracManagement() self.raid = drac_raid.DracRAID() self.vendor = drac_vendor.DracVendorPassthru() + self.inspect = drac_inspect.DracInspect() class FakeSNMPDriver(base.BaseDriver): diff --git a/ironic/drivers/modules/drac/inspect.py b/ironic/drivers/modules/drac/inspect.py new file mode 100644 index 0000000000..1357652465 --- /dev/null +++ b/ironic/drivers/modules/drac/inspect.py @@ -0,0 +1,140 @@ +# +# 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. + +""" +DRAC inspection interface +""" + +from oslo_log import log as logging +from oslo_utils import importutils +from oslo_utils import units + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common.i18n import _LE +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.drac import common as drac_common +from ironic import objects + +drac_exceptions = importutils.try_import('dracclient.exceptions') + +LOG = logging.getLogger(__name__) + + +class DracInspect(base.InspectInterface): + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return drac_common.COMMON_PROPERTIES + + def validate(self, task): + """Validate the driver-specific info supplied. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver to + manage the node. + + :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. + + """ + return drac_common.parse_driver_info(task.node) + + def inspect_hardware(self, task): + """Inspect hardware. + + Inspect hardware to obtain the essential & additional hardware + properties. + + :param task: a TaskManager instance containing the node to act on. + :raises: HardwareInspectionFailure, if unable to get essential + hardware properties. + :returns: states.MANAGEABLE + """ + + node = task.node + client = drac_common.get_drac_client(node) + properties = {} + + try: + properties['memory_mb'] = sum( + [memory.size_mb for memory in client.list_memory()]) + cpus = client.list_cpus() + properties['cpus'] = len(cpus) + properties['cpu_arch'] = 'x86_64' if cpus[0].arch64 else 'x86' + + virtual_disks = client.list_virtual_disks() + root_disk = self._guess_root_disk(virtual_disks) + if root_disk: + properties['local_gb'] = int(root_disk.size_mb / units.Ki) + else: + physical_disks = client.list_physical_disks() + root_disk = self._guess_root_disk(physical_disks) + if root_disk: + properties['local_gb'] = int( + root_disk.size_mb / units.Ki) + except drac_exceptions.BaseClientException as exc: + LOG.error(_LE('DRAC driver failed to introspect node ' + '%(node_uuid)s. Reason: %(error)s.'), + {'node_uuid': node.uuid, 'error': exc}) + raise exception.HardwareInspectionFailure(error=exc) + + valid_keys = self.ESSENTIAL_PROPERTIES + missing_keys = valid_keys - set(properties) + if missing_keys: + error = (_('Failed to discover the following properties: ' + '%(missing_keys)s') % + {'missing_keys': ', '.join(missing_keys)}) + raise exception.HardwareInspectionFailure(error=error) + + node.properties = dict(node.properties, **properties) + node.save() + + try: + nics = client.list_nics() + except drac_exceptions.BaseClientException as exc: + LOG.error(_LE('DRAC driver failed to introspect node ' + '%(node_uuid)s. Reason: %(error)s.'), + {'node_uuid': node.uuid, 'error': exc}) + raise exception.HardwareInspectionFailure(error=exc) + + for nic in nics: + try: + port = objects.Port(task.context, address=nic.mac, + node_id=node.id) + port.create() + LOG.info(_LI('Port created with MAC address %(mac)s ' + 'for node %(node_uuid)s during inspection'), + {'mac': nic.mac, 'node_uuid': node.uuid}) + except exception.MACAlreadyExists: + LOG.warning(_LW('Failed to create a port with MAC address ' + '%(mac)s when inspecting the node ' + '%(node_uuid)s because the address is already ' + 'registered'), + {'mac': nic.mac, 'node_uuid': node.uuid}) + + LOG.info(_LI('Node %s successfully inspected.'), node.uuid) + return states.MANAGEABLE + + def _guess_root_disk(self, disks, min_size_required=4 * units.Ki): + disks.sort(key=lambda disk: disk.size_mb) + for disk in disks: + if disk.size_mb >= min_size_required: + return disk diff --git a/ironic/tests/unit/drivers/modules/drac/test_inspect.py b/ironic/tests/unit/drivers/modules/drac/test_inspect.py new file mode 100644 index 0000000000..34786e90e2 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/test_inspect.py @@ -0,0 +1,235 @@ +# +# 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 DRAC inspection interface +""" + +from dracclient import exceptions as drac_exceptions +import mock + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.drac import common as drac_common +from ironic.drivers.modules.drac import inspect as drac_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.modules.drac import utils as test_utils +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_drac_info() + + +class DracInspectionTestCase(db_base.DbTestCase): + + def setUp(self): + super(DracInspectionTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_drac') + self.node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + memory = [{'id': 'DIMM.Socket.A1', + 'size_mb': 16384, + 'speed': 2133, + 'manufacturer': 'Samsung', + 'model': 'DDR4 DIMM', + 'state': 'ok'}, + {'id': 'DIMM.Socket.B1', + 'size_mb': 16384, + 'speed': 2133, + 'manufacturer': 'Samsung', + 'model': 'DDR4 DIMM', + 'state': 'ok'}] + cpus = [{'id': 'CPU.Socket.1', + 'cores': 6, + 'speed': 2400, + 'model': 'Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz', + 'state': 'ok', + 'ht_enabled': True, + 'turbo_enabled': True, + 'vt_enabled': True, + 'arch64': True}, + {'id': 'CPU.Socket.2', + 'cores': 6, + 'speed': 2400, + 'model': 'Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz', + 'state': 'ok', + 'ht_enabled': True, + 'turbo_enabled': True, + 'vt_enabled': True, + 'arch64': True}] + virtual_disks = [ + {'id': 'Disk.Virtual.0:RAID.Integrated.1-1', + 'name': 'disk 0', + 'description': 'Virtual Disk 0 on Integrated RAID Controller 1', + 'controller': 'RAID.Integrated.1-1', + 'raid_level': '1', + 'size_mb': 1143552, + 'state': 'ok', + 'raid_state': 'online', + 'span_depth': 1, + 'span_length': 2, + 'pending_operations': None}] + physical_disks = [ + {'id': 'Disk.Bay.1:Enclosure.Internal.0-1:RAID.Integrated.1-1', + 'description': ('Disk 1 in Backplane 1 of ' + 'Integrated RAID Controller 1'), + 'controller': 'RAID.Integrated.1-1', + 'manufacturer': 'SEAGATE', + 'model': 'ST600MM0006', + 'media_type': 'hdd', + 'interface_type': 'sas', + 'size_mb': 571776, + 'free_size_mb': 571776, + 'serial_number': 'S0M3EY2Z', + 'firmware_version': 'LS0A', + 'state': 'ok', + 'raid_state': 'ready'}, + {'id': 'Disk.Bay.2:Enclosure.Internal.0-1:RAID.Integrated.1-1', + 'description': ('Disk 1 in Backplane 1 of ' + 'Integrated RAID Controller 1'), + 'controller': 'RAID.Integrated.1-1', + 'manufacturer': 'SEAGATE', + 'model': 'ST600MM0006', + 'media_type': 'hdd', + 'interface_type': 'sas', + 'size_mb': 285888, + 'free_size_mb': 285888, + 'serial_number': 'S0M3EY2Z', + 'firmware_version': 'LS0A', + 'state': 'ok', + 'raid_state': 'ready'}] + nics = [ + {'id': 'NIC.Embedded.1-1-1', + 'mac': 'B0:83:FE:C6:6F:A1', + 'model': 'Broadcom Gigabit Ethernet BCM5720 - B0:83:FE:C6:6F:A1', + 'speed': '1000 Mbps', + 'duplex': 'full duplex', + 'media_type': 'Base T'}, + {'id': 'NIC.Embedded.2-1-1', + 'mac': 'B0:83:FE:C6:6F:A2', + 'model': 'Broadcom Gigabit Ethernet BCM5720 - B0:83:FE:C6:6F:A2', + 'speed': '1000 Mbps', + 'duplex': 'full duplex', + 'media_type': 'Base T'}] + self.memory = [test_utils.dict_to_namedtuple(values=m) for m in memory] + self.cpus = [test_utils.dict_to_namedtuple(values=c) for c in cpus] + self.virtual_disks = [test_utils.dict_to_namedtuple(values=vd) + for vd in virtual_disks] + self.physical_disks = [test_utils.dict_to_namedtuple(values=pd) + for pd in physical_disks] + self.nics = [test_utils.dict_to_namedtuple(values=n) for n in nics] + + def test_get_properties(self): + expected = drac_common.COMMON_PROPERTIES + driver = drac_inspect.DracInspect() + self.assertEqual(expected, driver.get_properties()) + + @mock.patch.object(drac_common, 'get_drac_client', spec_set=True, + autospec=True) + @mock.patch.object(objects.Port, 'create', spec_set=True, autospec=True) + def test_inspect_hardware(self, mock_port_create, mock_get_drac_client): + expected_node_properties = { + 'memory_mb': 32768, + 'local_gb': 1116, + 'cpus': 2, + 'cpu_arch': 'x86_64'} + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_memory.return_value = self.memory + mock_client.list_cpus.return_value = self.cpus + mock_client.list_virtual_disks.return_value = self.virtual_disks + mock_client.list_nics.return_value = self.nics + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + return_value = task.driver.inspect.inspect_hardware(task) + + self.node.refresh() + self.assertEqual(expected_node_properties, self.node.properties) + self.assertEqual(states.MANAGEABLE, return_value) + self.assertEqual(2, mock_port_create.call_count) + + @mock.patch.object(drac_common, 'get_drac_client', spec_set=True, + autospec=True) + @mock.patch.object(objects.Port, 'create', spec_set=True, autospec=True) + def test_inspect_hardware_fail(self, mock_port_create, + mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_memory.return_value = self.memory + mock_client.list_cpus.return_value = self.cpus + mock_client.list_virtual_disks.side_effect = ( + drac_exceptions.BaseClientException('boom')) + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.HardwareInspectionFailure, + task.driver.inspect.inspect_hardware, task) + + @mock.patch.object(drac_common, 'get_drac_client', spec_set=True, + autospec=True) + @mock.patch.object(objects.Port, 'create', spec_set=True, autospec=True) + def test_inspect_hardware_no_virtual_disk(self, mock_port_create, + mock_get_drac_client): + expected_node_properties = { + 'memory_mb': 32768, + 'local_gb': 279, + 'cpus': 2, + 'cpu_arch': 'x86_64'} + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_memory.return_value = self.memory + mock_client.list_cpus.return_value = self.cpus + mock_client.list_virtual_disks.return_value = [] + mock_client.list_physical_disks.return_value = self.physical_disks + mock_client.list_nics.return_value = self.nics + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + return_value = task.driver.inspect.inspect_hardware(task) + + self.node.refresh() + self.assertEqual(expected_node_properties, self.node.properties) + self.assertEqual(states.MANAGEABLE, return_value) + self.assertEqual(2, mock_port_create.call_count) + + @mock.patch.object(drac_common, 'get_drac_client', spec_set=True, + autospec=True) + @mock.patch.object(objects.Port, 'create', spec_set=True, autospec=True) + def test_inspect_hardware_with_existing_ports(self, mock_port_create, + mock_get_drac_client): + expected_node_properties = { + 'memory_mb': 32768, + 'local_gb': 1116, + 'cpus': 2, + 'cpu_arch': 'x86_64'} + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_memory.return_value = self.memory + mock_client.list_cpus.return_value = self.cpus + mock_client.list_virtual_disks.return_value = self.virtual_disks + mock_client.list_nics.return_value = self.nics + mock_port_create.side_effect = exception.MACAlreadyExists("boom") + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + return_value = task.driver.inspect.inspect_hardware(task) + + self.node.refresh() + self.assertEqual(expected_node_properties, self.node.properties) + self.assertEqual(states.MANAGEABLE, return_value) + self.assertEqual(2, mock_port_create.call_count) diff --git a/ironic/tests/unit/drivers/modules/drac/utils.py b/ironic/tests/unit/drivers/modules/drac/utils.py index 406d7af4f7..ea0958ef20 100644 --- a/ironic/tests/unit/drivers/modules/drac/utils.py +++ b/ironic/tests/unit/drivers/modules/drac/utils.py @@ -20,4 +20,4 @@ def dict_to_namedtuple(name='GenericNamedTuple', values=None): if values is None: values = {} - return collections.namedtuple(name, values.keys())(**values) + return collections.namedtuple(name, list(values))(**values) diff --git a/releasenotes/notes/drac-inspection-interface-b0abbad98fec1c2e.yaml b/releasenotes/notes/drac-inspection-interface-b0abbad98fec1c2e.yaml new file mode 100644 index 0000000000..4555172a8e --- /dev/null +++ b/releasenotes/notes/drac-inspection-interface-b0abbad98fec1c2e.yaml @@ -0,0 +1,7 @@ +--- +features: + - Adds out-of-band inspection interface usable by DRAC drivers. +upgrade: + - The ``inspect`` interface of the ``pxe_drac`` driver has switched to use + out-of-band inspection. For inband inspection, the node should be updated + to use the ``pxe_drac_inspector`` driver instead. diff --git a/setup.cfg b/setup.cfg index 7745c73251..2d89debf52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,7 @@ ironic.drivers = pxe_iboot = ironic.drivers.pxe:PXEAndIBootDriver pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver pxe_drac = ironic.drivers.drac:PXEDracDriver + pxe_drac_inspector = ironic.drivers.drac:PXEDracInspectorDriver pxe_snmp = ironic.drivers.pxe:PXEAndSNMPDriver pxe_irmc = ironic.drivers.pxe:PXEAndIRMCDriver pxe_amt = ironic.drivers.pxe:PXEAndAMTDriver