diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 04426419b..87e5ea821 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -704,6 +704,9 @@ class HardwareManager(object, metaclass=abc.ABCMeta): def get_bmc_address(self): raise errors.IncompatibleHardwareMethodError() + def get_bmc_mac(self): + raise errors.IncompatibleHardwareMethodError() + def get_bmc_v6address(self): raise errors.IncompatibleHardwareMethodError() @@ -829,6 +832,14 @@ class HardwareManager(object, metaclass=abc.ABCMeta): hardware_info['system_vendor'] = self.get_system_vendor_info() hardware_info['boot'] = self.get_boot_info() hardware_info['hostname'] = netutils.get_hostname() + + try: + hardware_info['bmc_mac'] = self.get_bmc_mac() + except errors.IncompatibleHardwareMethodError: + # if the hardware manager does not support obtaining the BMC MAC, + # we simply don't expose it. + pass + LOG.info('Inventory collected in %.2f second(s)', time.time() - start) return hardware_info @@ -1790,6 +1801,57 @@ class GenericHardwareManager(HardwareManager): return '0.0.0.0' + def get_bmc_mac(self): + """Attempt to detect BMC MAC address + + :return: MAC address of the first LAN channel or 00:00:00:00:00:00 in + case none of them has one or is configured properly + """ + # These modules are rarely loaded automatically + il_utils.try_execute('modprobe', 'ipmi_msghandler') + il_utils.try_execute('modprobe', 'ipmi_devintf') + il_utils.try_execute('modprobe', 'ipmi_si') + + try: + # From all the channels 0-15, only 1-11 can be assigned to + # different types of communication media and protocols and + # effectively used + for channel in range(1, 12): + out, e = utils.execute( + "ipmitool lan print {} | awk '/(IP|MAC) Address[ \\t]*:/" + " {{print $4}}'".format(channel), shell=True) + if e.startswith("Invalid channel"): + continue + + try: + ip, mac = out.strip().split("\n") + except ValueError: + LOG.warning('Invalid ipmitool output %(output)s', + {'output': out}) + continue + + if ip == "0.0.0.0": + # disabled, ignore + continue + + if not re.match("^[0-9a-f]{2}(:[0-9a-f]{2}){5}$", mac, re.I): + LOG.warning('Invalid MAC address %(output)s', + {'output': mac}) + continue + + # In case we get 00:00:00:00:00:00 on a valid channel, we need + # to keep querying + if mac != '00:00:00:00:00:00': + return mac + + except (processutils.ProcessExecutionError, OSError) as e: + # Not error, because it's normal in virtual environment + LOG.warning("Cannot get BMC MAC address: %s", e) + return + + # no valid mac found, signal this clearly + raise errors.IncompatibleHardwareMethodError() + def get_bmc_v6address(self): """Attempt to detect BMC v6 address diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 610012602..44b2a87d2 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -1165,6 +1165,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): current_boot_mode='bios', pxe_interface='boot:if') self.hardware.get_bmc_address = mock.Mock() + self.hardware.get_bmc_mac = mock.Mock() self.hardware.get_bmc_v6address = mock.Mock() self.hardware.get_system_vendor_info = mock.Mock() @@ -2300,6 +2301,68 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_execute.return_value = '', '' self.assertEqual('0.0.0.0', self.hardware.get_bmc_address()) + @mock.patch.object(il_utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_mac_not_available(self, mocked_execute, mte): + mocked_execute.return_value = '', '' + self.assertRaises(errors.IncompatibleHardwareMethodError, + self.hardware.get_bmc_mac) + + @mock.patch.object(il_utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_mac(self, mocked_execute, mte): + mocked_execute.return_value = '192.1.2.3\n01:02:03:04:05:06', '' + self.assertEqual('01:02:03:04:05:06', self.hardware.get_bmc_mac()) + + @mock.patch.object(il_utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_mac_virt(self, mocked_execute, mte): + mocked_execute.side_effect = processutils.ProcessExecutionError() + self.assertIsNone(self.hardware.get_bmc_mac()) + + @mock.patch.object(il_utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_mac_zeroed(self, mocked_execute, mte): + mocked_execute.return_value = '0.0.0.0\n00:00:00:00:00:00', '' + self.assertRaises(errors.IncompatibleHardwareMethodError, + self.hardware.get_bmc_mac) + + @mock.patch.object(il_utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_mac_invalid(self, mocked_execute, mte): + # In case of invalid lan channel, stdout is empty and the error + # on stderr is "Invalid channel" + mocked_execute.return_value = '\n', 'Invalid channel: 55' + self.assertRaises(errors.IncompatibleHardwareMethodError, + self.hardware.get_bmc_mac) + + @mock.patch.object(il_utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_mac_random_error(self, mocked_execute, mte): + mocked_execute.return_value = ('192.1.2.3\n00:00:00:00:00:02', + 'Random error message') + self.assertEqual('00:00:00:00:00:02', self.hardware.get_bmc_mac()) + + @mock.patch.object(il_utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_mac_iterate_channels(self, mocked_execute, mte): + # For channel 1 we simulate unconfigured IP + # and for any other we return a correct IP address + def side_effect(*args, **kwargs): + if args[0].startswith("ipmitool lan print 1"): + return '', 'Invalid channel 1\n' + elif args[0].startswith("ipmitool lan print 2"): + return '0.0.0.0\n00:00:00:00:23:42', '' + elif args[0].startswith("ipmitool lan print 3"): + return 'meow', '' + elif args[0].startswith("ipmitool lan print 4"): + return '192.1.2.3\n01:02:03:04:05:06', '' + else: + # this should never happen because the previous one was good + raise AssertionError + mocked_execute.side_effect = side_effect + self.assertEqual('01:02:03:04:05:06', self.hardware.get_bmc_mac()) + @mock.patch.object(il_utils, 'try_execute', autospec=True) @mock.patch.object(utils, 'execute', autospec=True) def test_get_bmc_v6address_not_enabled(self, mocked_execute, mte): diff --git a/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml b/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml new file mode 100644 index 000000000..296cf15cb --- /dev/null +++ b/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The introspection now includes the MAC address of the IPMI LAN channel + which has a valid IP address and MAC address assigned in the hardware + inventory data as ``bmc_mac``.