Skip to content

Commit 8f4a5a3

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Skip BMC detection when using out-of-band management"
2 parents 033a237 + ca6f4fb commit 8f4a5a3

File tree

4 files changed

+191
-8
lines changed

4 files changed

+191
-8
lines changed

ironic_python_agent/agent.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,15 @@ def process_lookup_data(self, content):
494494
self.node = content['node']
495495
LOG.info('Lookup succeeded, node UUID is %s',
496496
self.node['uuid'])
497+
498+
# Store skip_bmc_detect flag in the node before caching
499+
# This allows hardware managers to check if BMC detection is needed
500+
skip_bmc = content.get('config', {}).get('agent_skip_bmc_detect',
501+
False)
502+
if skip_bmc:
503+
LOG.info('Ironic has indicated BMC detection should be skipped')
504+
self.node['skip_bmc_detect'] = True
505+
497506
hardware.cache_node(self.node)
498507
self.heartbeat_timeout = content['config']['heartbeat_timeout']
499508

ironic_python_agent/hardware.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,18 +1151,52 @@ def list_hardware_info(self):
11511151
hardware_info['cpu'] = self.get_cpus()
11521152
hardware_info['disks'] = self.list_block_devices()
11531153
hardware_info['memory'] = self.get_memory()
1154-
hardware_info['bmc_address'] = self.get_bmc_address()
1155-
hardware_info['bmc_v6address'] = self.get_bmc_v6address()
1154+
1155+
# Check if Ironic has indicated BMC detection should be skipped
1156+
# This is set after lookup when using out-of-band
1157+
# management like Redfish
1158+
cached_node = get_cached_node()
1159+
skip_bmc = (cached_node and cached_node.get('skip_bmc_detect',
1160+
False))
1161+
1162+
if skip_bmc:
1163+
LOG.info('Skipping BMC detection as requested by Ironic')
1164+
hardware_info['bmc_address'] = None
1165+
hardware_info['bmc_v6address'] = None
1166+
else:
1167+
# Cache BMC information to avoid repeated expensive ipmitool calls
1168+
if not hasattr(self, '_bmc_cache'):
1169+
LOG.debug('Detecting BMC information (first time)')
1170+
self._bmc_cache = {
1171+
'bmc_address': self.get_bmc_address(),
1172+
'bmc_v6address': self.get_bmc_v6address(),
1173+
'bmc_mac': None # Populated below
1174+
}
1175+
else:
1176+
LOG.debug('Using cached BMC information')
1177+
1178+
hardware_info['bmc_address'] = self._bmc_cache['bmc_address']
1179+
hardware_info['bmc_v6address'] = self._bmc_cache['bmc_v6address']
1180+
11561181
hardware_info['system_vendor'] = self.get_system_vendor_info()
11571182
hardware_info['boot'] = self.get_boot_info()
11581183
hardware_info['hostname'] = netutils.get_hostname()
11591184

1160-
try:
1161-
hardware_info['bmc_mac'] = self.get_bmc_mac()
1162-
except errors.IncompatibleHardwareMethodError:
1163-
# if the hardware manager does not support obtaining the BMC MAC,
1164-
# we simply don't expose it.
1165-
pass
1185+
if not skip_bmc:
1186+
# Try to get BMC MAC, which may not be cached yet
1187+
if self._bmc_cache['bmc_mac'] is None:
1188+
try:
1189+
self._bmc_cache['bmc_mac'] = self.get_bmc_mac()
1190+
LOG.debug('Cached BMC MAC address')
1191+
except errors.IncompatibleHardwareMethodError:
1192+
# if the hardware manager does not support obtaining
1193+
# the BMC MAC, we simply don't expose it.
1194+
# Mark as unavailable to avoid retrying
1195+
self._bmc_cache['bmc_mac'] = 'unavailable'
1196+
1197+
# Only add to hardware_info if we successfully got it
1198+
if self._bmc_cache['bmc_mac'] not in (None, 'unavailable'):
1199+
hardware_info['bmc_mac'] = self._bmc_cache['bmc_mac']
11661200

11671201
LOG.info('Inventory collected in %.2f second(s)', time.time() - start)
11681202
return hardware_info

ironic_python_agent/tests/unit/test_hardware.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,133 @@ def test_list_hardware_info(self, mocked_get_hostname, mocked_lshw):
15091509
self.assertEqual('mock_hostname', hardware_info['hostname'])
15101510
mocked_lshw.assert_called_once_with(self.hardware)
15111511

1512+
@mock.patch.object(hardware, 'get_cached_node', autospec=True)
1513+
@mock.patch.object(hardware.GenericHardwareManager,
1514+
'_get_system_lshw_dict', autospec=True,
1515+
return_value={'id': 'host'})
1516+
@mock.patch.object(netutils, 'get_hostname', autospec=True)
1517+
def test_list_hardware_info_with_bmc_caching(
1518+
self, mocked_get_hostname, mocked_lshw, mocked_get_node):
1519+
"""Test that BMC information is cached after first detection."""
1520+
mocked_get_node.return_value = None # No skip_bmc_detect flag
1521+
1522+
# Mock all the hardware info methods
1523+
self.hardware.list_network_interfaces = mock.Mock(return_value=[])
1524+
self.hardware.get_cpus = mock.Mock()
1525+
self.hardware.get_memory = mock.Mock()
1526+
self.hardware.list_block_devices = mock.Mock(return_value=[])
1527+
self.hardware.get_boot_info = mock.Mock()
1528+
self.hardware.get_system_vendor_info = mock.Mock()
1529+
mocked_get_hostname.return_value = 'mock_hostname'
1530+
1531+
# Mock BMC methods
1532+
self.hardware.get_bmc_address = mock.Mock(return_value='192.168.1.1')
1533+
self.hardware.get_bmc_v6address = mock.Mock(return_value='fe80::1')
1534+
self.hardware.get_bmc_mac = mock.Mock(return_value='aa:bb:cc:dd:ee:ff')
1535+
1536+
# First call - should call BMC detection methods
1537+
hardware_info1 = self.hardware.list_hardware_info()
1538+
self.assertEqual('192.168.1.1', hardware_info1['bmc_address'])
1539+
self.assertEqual('fe80::1', hardware_info1['bmc_v6address'])
1540+
self.assertEqual('aa:bb:cc:dd:ee:ff', hardware_info1['bmc_mac'])
1541+
1542+
# Verify BMC methods were called
1543+
self.hardware.get_bmc_address.assert_called_once()
1544+
self.hardware.get_bmc_v6address.assert_called_once()
1545+
self.hardware.get_bmc_mac.assert_called_once()
1546+
1547+
# Second call - should use cached values
1548+
hardware_info2 = self.hardware.list_hardware_info()
1549+
self.assertEqual('192.168.1.1', hardware_info2['bmc_address'])
1550+
self.assertEqual('fe80::1', hardware_info2['bmc_v6address'])
1551+
self.assertEqual('aa:bb:cc:dd:ee:ff', hardware_info2['bmc_mac'])
1552+
1553+
# BMC methods should NOT be called again (still only once)
1554+
self.hardware.get_bmc_address.assert_called_once()
1555+
self.hardware.get_bmc_v6address.assert_called_once()
1556+
self.hardware.get_bmc_mac.assert_called_once()
1557+
1558+
@mock.patch.object(hardware, 'get_cached_node', autospec=True)
1559+
@mock.patch.object(hardware.GenericHardwareManager,
1560+
'_get_system_lshw_dict', autospec=True,
1561+
return_value={'id': 'host'})
1562+
@mock.patch.object(netutils, 'get_hostname', autospec=True)
1563+
def test_list_hardware_info_skip_bmc_detect(
1564+
self, mocked_get_hostname, mocked_lshw, mocked_get_node):
1565+
"""Test that BMC detection is skipped when flag is set."""
1566+
mocked_get_node.return_value = {'skip_bmc_detect': True}
1567+
1568+
# Mock all the hardware info methods
1569+
self.hardware.list_network_interfaces = mock.Mock(return_value=[])
1570+
self.hardware.get_cpus = mock.Mock()
1571+
self.hardware.get_memory = mock.Mock()
1572+
self.hardware.list_block_devices = mock.Mock(return_value=[])
1573+
self.hardware.get_boot_info = mock.Mock()
1574+
self.hardware.get_system_vendor_info = mock.Mock()
1575+
mocked_get_hostname.return_value = 'mock_hostname'
1576+
1577+
# Mock BMC methods - these should NOT be called
1578+
self.hardware.get_bmc_address = mock.Mock()
1579+
self.hardware.get_bmc_v6address = mock.Mock()
1580+
self.hardware.get_bmc_mac = mock.Mock()
1581+
1582+
# Call list_hardware_info
1583+
hardware_info = self.hardware.list_hardware_info()
1584+
1585+
# BMC info should be None
1586+
self.assertIsNone(hardware_info['bmc_address'])
1587+
self.assertIsNone(hardware_info['bmc_v6address'])
1588+
self.assertNotIn('bmc_mac', hardware_info)
1589+
1590+
# BMC detection methods should NOT have been called
1591+
self.hardware.get_bmc_address.assert_not_called()
1592+
self.hardware.get_bmc_v6address.assert_not_called()
1593+
self.hardware.get_bmc_mac.assert_not_called()
1594+
1595+
@mock.patch.object(hardware, 'get_cached_node', autospec=True)
1596+
@mock.patch.object(hardware.GenericHardwareManager,
1597+
'_get_system_lshw_dict', autospec=True,
1598+
return_value={'id': 'host'})
1599+
@mock.patch.object(netutils, 'get_hostname', autospec=True)
1600+
def test_list_hardware_info_bmc_mac_unavailable(
1601+
self, mocked_get_hostname, mocked_lshw, mocked_get_node):
1602+
"""Test BMC MAC marked unavailable when not supported."""
1603+
mocked_get_node.return_value = None
1604+
1605+
# Mock all the hardware info methods
1606+
self.hardware.list_network_interfaces = mock.Mock(return_value=[])
1607+
self.hardware.get_cpus = mock.Mock()
1608+
self.hardware.get_memory = mock.Mock()
1609+
self.hardware.list_block_devices = mock.Mock(return_value=[])
1610+
self.hardware.get_boot_info = mock.Mock()
1611+
self.hardware.get_system_vendor_info = mock.Mock()
1612+
mocked_get_hostname.return_value = 'mock_hostname'
1613+
1614+
# Mock BMC methods
1615+
self.hardware.get_bmc_address = mock.Mock(return_value='192.168.1.1')
1616+
self.hardware.get_bmc_v6address = mock.Mock(return_value='fe80::1')
1617+
# BMC MAC raises IncompatibleHardwareMethodError
1618+
self.hardware.get_bmc_mac = mock.Mock(
1619+
side_effect=errors.IncompatibleHardwareMethodError)
1620+
1621+
# First call
1622+
hardware_info1 = self.hardware.list_hardware_info()
1623+
self.assertEqual('192.168.1.1', hardware_info1['bmc_address'])
1624+
self.assertEqual('fe80::1', hardware_info1['bmc_v6address'])
1625+
self.assertNotIn('bmc_mac', hardware_info1) # Not in output
1626+
1627+
# Verify get_bmc_mac was called once
1628+
self.hardware.get_bmc_mac.assert_called_once()
1629+
1630+
# Second call - get_bmc_mac should NOT be called again
1631+
hardware_info2 = self.hardware.list_hardware_info()
1632+
self.assertEqual('192.168.1.1', hardware_info2['bmc_address'])
1633+
self.assertEqual('fe80::1', hardware_info2['bmc_v6address'])
1634+
self.assertNotIn('bmc_mac', hardware_info2)
1635+
1636+
# Still only one call (cached as 'unavailable')
1637+
self.hardware.get_bmc_mac.assert_called_once()
1638+
15121639
@mock.patch.object(hardware, 'list_all_block_devices', autospec=True)
15131640
def test_list_block_devices(self, list_mock):
15141641
device = hardware.BlockDevice('/dev/hdaa', 'small', 65535, False)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
features:
3+
- |
4+
Adds support for automatically skipping BMC detection via ipmitool
5+
when Ironic indicates the node uses out-of-band management interfaces
6+
(e.g., Redfish, iDRAC Redfish, iLO, iRMC). This reduces deployment
7+
time and avoids unnecessary ipmitool calls when the BMC information
8+
is already known to Ironic.
9+
- |
10+
BMC information (address, v6address, and MAC) is now cached after the
11+
first detection to avoid repeated expensive ipmitool calls during
12+
subsequent inventory collections. This improves performance during
13+
heartbeats and long-running operations like cleaning or rescue mode.

0 commit comments

Comments
 (0)