Add bmc_subnet information to introspection

Consolidated BMC information retrieval logic into a single function _get_bmc_info().
Add tests for bmc_subnet

Change-Id: If865e10f910d9e00a68106d97cc85bd53b4a86ed
This commit is contained in:
Maximilian Brandt 2024-05-06 21:12:40 +02:00
parent af907322f6
commit 50e682b5cd
No known key found for this signature in database
GPG Key ID: E81163E7C0A5AA0C
2 changed files with 96 additions and 16 deletions

View File

@ -142,7 +142,7 @@ def _load_multipath_modules():
will do the needful.
"""
if (os.path.isfile('/usr/sbin/mpathconf')
and not os.path.isfile('/etc/multipath.conf')):
and not os.path.isfile('/etc/multipath.conf')):
# For Centos/Rhel/Etc which uses mpathconf, this does
# a couple different things, including configuration generation...
# which is not *really* required.. at least *shouldn't* be.
@ -846,6 +846,7 @@ class CPU(encoding.SerializableComparable):
class Memory(encoding.SerializableComparable):
serializable_fields = ('total', 'physical_mb')
# physical = total + kernel binary + reserved space
def __init__(self, total, physical_mb=None):
@ -944,6 +945,9 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
def get_bmc_address(self):
raise errors.IncompatibleHardwareMethodError()
def get_bmc_subnet(self):
raise errors.IncompatibleHardwareMethodError()
def get_bmc_mac(self):
raise errors.IncompatibleHardwareMethodError()
@ -1082,6 +1086,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
hardware_info['disks'] = self.list_block_devices()
hardware_info['memory'] = self.get_memory()
hardware_info['bmc_address'] = self.get_bmc_address()
hardware_info['bmc_subnet'] = self.get_bmc_subnet()
hardware_info['bmc_v6address'] = self.get_bmc_v6address()
hardware_info['system_vendor'] = self.get_system_vendor_info()
hardware_info['boot'] = self.get_boot_info()
@ -1977,7 +1982,7 @@ class GenericHardwareManager(HardwareManager):
args = ('shred', '--force')
if info.get('agent_erase_devices_zeroize', True):
args += ('--zero', )
args += ('--zero',)
args += ('--verbose', '--iterations', str(npasses), block_device.name)
@ -2000,7 +2005,7 @@ class GenericHardwareManager(HardwareManager):
if os.path.exists(vm_device_label):
link = os.readlink(vm_device_label)
device = os.path.normpath(os.path.join(os.path.dirname(
vm_device_label), link))
vm_device_label), link))
if block_device.name == device:
return True
return False
@ -2048,7 +2053,7 @@ class GenericHardwareManager(HardwareManager):
except IOError as e:
# Check underlying device as the file may exist there
if (not partition and dev_name[-1].isdigit()
and 'nvme' not in dev_name):
and 'nvme' not in dev_name):
return self._is_read_only_device(block_device, partition=True)
LOG.warning("Could not determine if %(name)s is a"
@ -2259,9 +2264,9 @@ class GenericHardwareManager(HardwareManager):
# instead
crypto_caps = nvme_info['fna']
if crypto_caps & NVME_CLI_CRYPTO_FORMAT_SUPPORTED_FLAG:
format_mode = 2 # crypto erase
format_mode = 2 # crypto erase
else:
format_mode = 1 # user-data erase
format_mode = 1 # user-data erase
else:
msg = ('nvme-cli did not return any supported format modes '
'for device: {device}').format(
@ -2289,11 +2294,13 @@ class GenericHardwareManager(HardwareManager):
).format(block_device, e))
raise errors.BlockDeviceEraseError(msg)
def get_bmc_address(self):
"""Attempt to detect BMC IP address
def _get_bmc_address_info(self, type):
"""Attempt to detect BMC information
:return: IP address of lan channel or 0.0.0.0 in case none of them is
configured properly
:param type: Keyword to search for in IPMITool output
(e.g., 'IP Address', 'Subnet Mask')
:return: BMC information (IP address or subnet mask) or
default value if none found
"""
try:
# From all the channels 0-15, only 1-11 can be assigned to
@ -2301,8 +2308,8 @@ class GenericHardwareManager(HardwareManager):
# effectively used
for channel in range(1, 12):
out, e = il_utils.execute(
"ipmitool lan print {} | awk '/IP Address[ \\t]*:/"
" {{print $4}}'".format(channel), shell=True)
"ipmitool lan print {} | awk '/{}[ \\t]*:/"
" {{print $4}}'".format(channel, type), shell=True)
if e.startswith("Invalid channel"):
continue
out = out.strip()
@ -2310,7 +2317,8 @@ class GenericHardwareManager(HardwareManager):
try:
ipaddress.ip_address(out)
except ValueError as exc:
LOG.warning('Invalid IP address %(output)s: %(exc)s',
LOG.warning('Invalid IP or Subnet address %(output)s: '
'%(exc)s',
{'output': out, 'exc': exc})
continue
@ -2321,11 +2329,33 @@ class GenericHardwareManager(HardwareManager):
except (processutils.ProcessExecutionError, OSError) as e:
# Not error, because it's normal in virtual environment
LOG.warning("Cannot get BMC address: %s", e)
LOG.warning("Cannot get BMC %s: %s", type, e)
return
return '0.0.0.0'
def get_bmc_address(self):
"""Attempt to detect BMC subnet mask
:return: Subnet mask of the first
LAN channel or 0.0.0.0 if none
of them is configured properly
"""
bmc_address = self._get_bmc_address_info("IP Address")
return bmc_address
def get_bmc_subnet(self):
"""Attempt to detect BMC subnet mask
:return: Subnet mask of the first
LAN channel or 255.255.255.255 if none
of them is configured properly
"""
bmc_subnet = self._get_bmc_address_info("Subnet Mask")
if bmc_subnet == '0.0.0.0':
return '255.255.255.255'
return bmc_subnet
def get_bmc_mac(self):
"""Attempt to detect BMC MAC address
@ -2902,7 +2932,7 @@ class GenericHardwareManager(HardwareManager):
if not raid_devices:
break
else:
msg = "Unable to clean all softraid correctly. Remaining {}".\
msg = "Unable to clean all softraid correctly. Remaining {}". \
format([dev.name for dev in raid_devices])
LOG.error(msg)
raise errors.SoftwareRAIDError(msg)
@ -2969,7 +2999,7 @@ class GenericHardwareManager(HardwareManager):
continue
else:
msg = "Failed to examine device {}: {}".format(
component_device, e)
component_device, e)
raise errors.SoftwareRAIDError(msg)
LOG.debug('Deleting md superblock on %s', component_device)

View File

@ -1044,6 +1044,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
current_boot_mode='bios', pxe_interface='boot:if')
self.hardware.get_bmc_address = mock.Mock()
self.hardware.get_bmc_subnet = mock.Mock()
self.hardware.get_bmc_mac = mock.Mock()
self.hardware.get_bmc_v6address = mock.Mock()
self.hardware.get_system_vendor_info = mock.Mock()
@ -2908,6 +2909,55 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_execute.return_value = '', ''
self.assertEqual('0.0.0.0', self.hardware.get_bmc_address())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_subnet(self, mocked_execute):
mocked_execute.return_value = '255.255.255.0\n', ''
self.assertEqual('255.255.255.0', self.hardware.get_bmc_subnet())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_subnet_virt(self, mocked_execute):
mocked_execute.side_effect = processutils.ProcessExecutionError()
self.assertIsNone(self.hardware.get_bmc_subnet())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_subnet_zeroed(self, mocked_execute):
mocked_execute.return_value = '0.0.0.0\n', ''
self.assertEqual('255.255.255.255', self.hardware.get_bmc_subnet())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_subnet_invalid(self, mocked_execute):
# 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.assertEqual('255.255.255.255', self.hardware.get_bmc_subnet())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_subnet_random_error(self, mocked_execute):
mocked_execute.return_value = '255.255.255.0\n', 'Random error message'
self.assertEqual('255.255.255.0', self.hardware.get_bmc_subnet())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_subnet_iterate_channels(self, mocked_execute):
# For channel 1 we simulate unconfigured subnet
# and for any other we return a correct subnet 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 '255.255.255.255\n', ''
elif args[0].startswith("ipmitool lan print 3"):
return 'meow', ''
else:
return '255.255.255.0\n', ''
mocked_execute.side_effect = side_effect
self.assertEqual('255.255.255.255', self.hardware.get_bmc_subnet())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_subnet_not_available(self, mocked_execute):
mocked_execute.return_value = '', ''
self.assertEqual('255.255.255.255', self.hardware.get_bmc_subnet())
@mock.patch.object(il_utils, 'execute', autospec=True)
def test_get_bmc_mac_not_available(self, mocked_execute):
mocked_execute.return_value = '', ''