summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2021-08-08 17:21:25 +0000
committerGerrit Code Review <review@openstack.org>2021-08-08 17:21:25 +0000
commitab026da95b54a2d62c93c20cf614bf9588364305 (patch)
tree4720dfb4f3aa2d770366a7f517c8815e30c35da7
parent9b42b08edd85933c3f96444a649b0d80f16ec8d0 (diff)
parent61af712fe52d474d688d677e7e678a45ca0336ab (diff)
downloadironic-python-agent-ab026da95b54a2d62c93c20cf614bf9588364305.tar.gz
Merge "Expose BMC MAC address in inventory data"
-rw-r--r--ironic_python_agent/hardware.py62
-rw-r--r--ironic_python_agent/tests/unit/test_hardware.py63
-rw-r--r--releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml6
3 files changed, 131 insertions, 0 deletions
diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py
index 04426419..87e5ea82 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 61001260..44b2a87d 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()
@@ -2302,6 +2303,68 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@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):
mocked_execute.side_effect = [('ipv4\n', '')] * 11
self.assertEqual('::/0', self.hardware.get_bmc_v6address())
diff --git a/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml b/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml
new file mode 100644
index 00000000..296cf15c
--- /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``.