diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 3ea1e45c7..0b6926554 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -17,6 +17,7 @@ import binascii import collections import contextlib import functools +import glob import io import ipaddress import json @@ -446,7 +447,7 @@ def md_restart(raid_device): def md_get_raid_devices(): """Get all discovered Software RAID (md) devices - :return: A python dict containing details about the discovered RAID + :returns: A python dict containing details about the discovered RAID devices """ # Note(Boushra): mdadm output is similar to lsblk, but not @@ -509,7 +510,7 @@ def list_all_block_devices(block_type='disk', the short and the long serial from udevadm if possible. - :return: A list of BlockDevices + :returns: A list of BlockDevices """ def _is_known_device(existing, new_device_name): @@ -925,7 +926,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): """List physical block devices :param include_partitions: If to include partitions - :return: A list of BlockDevices + :returns: A list of BlockDevices """ raise errors.IncompatibleHardwareMethodError @@ -936,7 +937,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): :param block_devices: a list of BlockDevices :param just_raids: a boolean to signify that only RAID devices are important - :return: A set of names of devices on the skip list + :returns: A set of names of devices on the skip list """ raise errors.IncompatibleHardwareMethodError @@ -948,7 +949,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): :param node: A node used to check the skip list :param include_partitions: If to include partitions - :return: A list of BlockDevices + :returns: A list of BlockDevices """ raise errors.IncompatibleHardwareMethodError @@ -981,7 +982,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): List all USB final devices, based on lshw information - :return: a dict, containing product, vendor, and handle information + :returns: a dict, containing product, vendor, and handle information """ raise errors.IncompatibleHardwareMethodError() @@ -1027,7 +1028,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): may require manual intervention due to the contents and operational risk which exists as it could also be a sign of an environmental misconfiguration. - :return: a dictionary in the form {device.name: erasure output} + :returns: a dictionary in the form {device.name: erasure output} """ erase_results = {} block_devices = self.list_block_devices_check_skip_list(node) @@ -1088,7 +1089,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): This inventory is sent to Ironic on lookup and to Inspector on inspection. - :return: a dictionary representing inventory + :returns: a dictionary representing inventory """ start = time.time() LOG.info('Collecting full inventory') @@ -1161,7 +1162,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): :param node: Ironic node object :param ports: list of Ironic port objects - :return: a list of cleaning steps, where each step is described as a + :returns: a list of cleaning steps, where each step is described as a dict as defined above """ @@ -1210,7 +1211,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): :param node: Ironic node object :param ports: list of Ironic port objects - :return: a list of deploying steps, where each step is described as a + :returns: a list of deploying steps, where each step is described as a dict as defined above """ @@ -1264,7 +1265,7 @@ class HardwareManager(object, metaclass=abc.ABCMeta): :param node: Ironic node object :param ports: list of Ironic port objects - :return: a list of service steps, where each step is described as a + :returns: a list of service steps, where each step is described as a dict as defined above """ @@ -1336,7 +1337,7 @@ class GenericHardwareManager(HardwareManager): This inventory is sent to Ironic on lookup and to Inspector on inspection. - :return: a dictionary representing inventory + :returns: a dictionary representing inventory """ with self._cached_lshw(): return super().list_hardware_info() @@ -1359,7 +1360,7 @@ class GenericHardwareManager(HardwareManager): Retrieves a json representation of the system from lshw and converts it to a python dict - :return: A python dict from the lshw json output + :returns: A python dict from the lshw json output """ if self._lshw_cache: return self._lshw_cache @@ -1379,7 +1380,7 @@ class GenericHardwareManager(HardwareManager): be converted for serialization purposes. :param interface_names: list of names of node's interfaces. - :return: a dict, containing the lldp data from every interface. + :returns: a dict, containing the lldp data from every interface. """ if interface_names is None: interface_names = netutils.list_interfaces() @@ -1471,7 +1472,7 @@ class GenericHardwareManager(HardwareManager): extra field named ``biosdevname``. :param interface_name: list of names of node's interfaces. - :return: the BIOS given NIC name of node's interfaces or default + :returns: the BIOS given NIC name of node's interfaces or default as None. """ global WARN_BIOSDEVNAME_NOT_FOUND @@ -1529,6 +1530,15 @@ class GenericHardwareManager(HardwareManager): return network_interfaces_list + def any_ipmi_device_exists(self): + '''Check for an IPMI device to confirm IPMI capability.''' + for pattern in ['/dev/ipmi*', '/dev/ipmi/*', '/dev/ipmidev/*']: + ipmi_files = glob.glob(pattern) + for device in ipmi_files: + if utils.is_char_device(device): + return True + return False + @staticmethod def create_cpu_info_dict(lines): cpu_info = {k.strip().lower(): v.strip() for k, v in @@ -2317,7 +2327,7 @@ class GenericHardwareManager(HardwareManager): """Attempt to clean the NVMe using the most secure supported method :param block_device: a BlockDevice object - :return: True if cleaning operation succeeded, False if it failed + :returns: True if cleaning operation succeeded, False if it failed :raises: BlockDeviceEraseError """ @@ -2382,9 +2392,12 @@ class GenericHardwareManager(HardwareManager): def get_bmc_address(self): """Attempt to detect BMC IP address - :return: IP address of lan channel or 0.0.0.0 in case none of them is + :returns: IP address of lan channel or 0.0.0.0 in case none of them is configured properly """ + if not self.any_ipmi_device_exists(): + return None + try: # From all the channels 0-15, only 1-11 can be assigned to # different types of communication media and protocols and @@ -2419,9 +2432,13 @@ class GenericHardwareManager(HardwareManager): 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 + :returns: 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 + :raises: IncompatibleHardwareMethodError if no valid mac is found. """ + if not self.any_ipmi_device_exists(): + return None + try: # From all the channels 0-15, only 1-11 can be assigned to # different types of communication media and protocols and @@ -2465,10 +2482,13 @@ class GenericHardwareManager(HardwareManager): def get_bmc_v6address(self): """Attempt to detect BMC v6 address - :return: IPv6 address of lan channel or ::/0 in case none of them is + :returns: IPv6 address of lan channel or ::/0 in case none of them is configured properly. May return None value if it cannot interact with system tools or critical error occurs. """ + if not self.any_ipmi_device_exists(): + return None + null_address_re = re.compile(r'^::(/\d{1,3})*$') def get_addr(channel, dynamic=False): diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 469d96254..d161f50c8 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -13,6 +13,7 @@ # limitations under the License. import binascii +import glob import json import logging import os @@ -2984,37 +2985,98 @@ class TestGenericHardwareManager(base.IronicAgentTest): mock_dev_file.side_effect = reads self.assertTrue(self.hardware._is_read_only_device(device)) + @mock.patch.object(os.path, 'exists', autospec=True) + @mock.patch.object(os, 'stat', autospec=True) + @mock.patch.object(glob, 'glob', autospec=True) + def test_ipmi_device_exists(self, mock_glob, mock_stat, mock_exists): + mock_stat_result = mock.Mock() + mock_stat_result.st_mode = stat.S_IFCHR + mock_stat.return_value = mock_stat_result + + # 1. Test when no device is found in default locations, + # but found in glob + mock_exists.side_effect = [False, False, False] + mock_glob.return_value = ['/dev/ipmi1'] + self.assertTrue(self.hardware.any_ipmi_device_exists()) + mock_stat.assert_called_once_with('/dev/ipmi1') + + mock_exists.reset_mock() + mock_exists.reset_mock() + mock_stat.reset_mock() + + # 2. Test when no device is found in default locations, + # but found in glob + mock_exists.side_effect = [False, False, False] + mock_glob.return_value = ['/dev/ipmidev/11'] + self.assertTrue(self.hardware.any_ipmi_device_exists()) + mock_stat.assert_called_once_with('/dev/ipmidev/11') + + # Reset mocks + mock_exists.reset_mock() + mock_stat.reset_mock() + mock_glob.reset_mock() + + # Test when no IPMI device is found at all + mock_exists.side_effect = [False, False, False] + mock_glob.return_value = [] + self.assertFalse(self.hardware.any_ipmi_device_exists()) + mock_stat.assert_not_called() + + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_address(self, mocked_execute): + def test_get_bmc_address(self, mocked_execute, mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.return_value = '192.1.2.3\n', '' self.assertEqual('192.1.2.3', self.hardware.get_bmc_address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_address_virt(self, mocked_execute): + def test_get_bmc_address_virt(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.side_effect = processutils.ProcessExecutionError() self.assertIsNone(self.hardware.get_bmc_address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_address_zeroed(self, mocked_execute): + def test_get_bmc_address_zeroed(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.return_value = '0.0.0.0\n', '' self.assertEqual('0.0.0.0', self.hardware.get_bmc_address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_address_invalid(self, mocked_execute): + def test_get_bmc_address_invalid(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True # 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('0.0.0.0', self.hardware.get_bmc_address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_address_random_error(self, mocked_execute): + def test_get_bmc_address_random_error(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.return_value = '192.1.2.3\n', 'Random error message' self.assertEqual('192.1.2.3', self.hardware.get_bmc_address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_address_iterate_channels(self, mocked_execute): + def test_get_bmc_address_iterate_channels(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True # 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' @@ -3027,51 +3089,82 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_execute.side_effect = side_effect self.assertEqual('192.1.2.3', self.hardware.get_bmc_address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_address_not_available(self, mocked_execute): + def test_get_bmc_address_not_available(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.return_value = '', '' self.assertEqual('0.0.0.0', self.hardware.get_bmc_address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_mac_not_available(self, mocked_execute): + def test_get_bmc_mac_not_available(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.return_value = '', '' self.assertRaises(errors.IncompatibleHardwareMethodError, self.hardware.get_bmc_mac) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_mac(self, mocked_execute): + def test_get_bmc_mac(self, mocked_execute, mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True 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(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_mac_virt(self, mocked_execute): + def test_get_bmc_mac_virt(self, mocked_execute, mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.side_effect = processutils.ProcessExecutionError() self.assertIsNone(self.hardware.get_bmc_mac()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_mac_zeroed(self, mocked_execute): + def test_get_bmc_mac_zeroed(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True 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(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_mac_invalid(self, mocked_execute): + def test_get_bmc_mac_invalid(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True # 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(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_mac_random_error(self, mocked_execute): + def test_get_bmc_mac_random_error(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True 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(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_mac_iterate_channels(self, mocked_execute): + def test_get_bmc_mac_iterate_channels(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True # 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' @@ -3087,13 +3180,21 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_execute.side_effect = side_effect self.assertEqual('01:02:03:04:05:06', self.hardware.get_bmc_mac()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_v6address_not_enabled(self, mocked_execute): + def test_get_bmc_v6address_not_enabled(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.side_effect = [('ipv4\n', '')] * 11 self.assertEqual('::/0', self.hardware.get_bmc_v6address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_v6address_dynamic_address(self, mocked_execute): + def test_get_bmc_v6address_dynamic_address(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.side_effect = [ ('ipv6\n', ''), (hws.IPMITOOL_LAN6_PRINT_DYNAMIC_ADDR, '') @@ -3101,8 +3202,12 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.assertEqual('2001:1234:1234:1234:1234:1234:1234:1234', self.hardware.get_bmc_v6address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_v6address_static_address_both(self, mocked_execute): + def test_get_bmc_v6address_static_address_both(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True dynamic_disabled = \ hws.IPMITOOL_LAN6_PRINT_DYNAMIC_ADDR.replace('active', 'disabled') mocked_execute.side_effect = [ @@ -3113,13 +3218,22 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.assertEqual('2001:5678:5678:5678:5678:5678:5678:5678', self.hardware.get_bmc_v6address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_v6address_virt(self, mocked_execute): + def test_get_bmc_v6address_virt(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True mocked_execute.side_effect = processutils.ProcessExecutionError() self.assertIsNone(self.hardware.get_bmc_v6address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_v6address_invalid_enables(self, mocked_execute): + def test_get_bmc_v6address_invalid_enables(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True + def side_effect(*args, **kwargs): if args[0].startswith('ipmitool lan6 print'): return '', 'Failed to get IPv6/IPv4 Addressing Enables' @@ -3127,8 +3241,13 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_execute.side_effect = side_effect self.assertEqual('::/0', self.hardware.get_bmc_v6address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_v6address_invalid_get_address(self, mocked_execute): + def test_get_bmc_v6address_invalid_get_address(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True + def side_effect(*args, **kwargs): if args[0].startswith('ipmitool lan6 print'): if args[0].endswith('dynamic_addr') \ @@ -3139,10 +3258,14 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_execute.side_effect = side_effect self.assertEqual('::/0', self.hardware.get_bmc_v6address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(hardware, 'LOG', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) def test_get_bmc_v6address_ipmitool_invalid_stdout_format( - self, mocked_execute, mocked_log): + self, mocked_execute, mocked_log, mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True + def side_effect(*args, **kwargs): if args[0].startswith('ipmitool lan6 print'): if args[0].endswith('dynamic_addr') \ @@ -3156,8 +3279,13 @@ class TestGenericHardwareManager(base.IronicAgentTest): 'command: %(e)s', mock.ANY) mocked_log.warning.assert_has_calls([one_call] * 14) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) - def test_get_bmc_v6address_channel_7(self, mocked_execute): + def test_get_bmc_v6address_channel_7(self, mocked_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = True + def side_effect(*args, **kwargs): if not args[0].startswith('ipmitool lan6 print 7'): # ipv6 is not enabled for channels 1-6 @@ -3175,6 +3303,33 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.assertEqual('2001:5678:5678:5678:5678:5678:5678:5678', self.hardware.get_bmc_v6address()) + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_get_bmc_address_no_ipmi_device(self, mock_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = False + self.assertIsNone(self.hardware.get_bmc_address()) + mock_execute.assert_not_called() + + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_get_bmc_mac_no_ipmi_device(self, mock_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = False + self.assertIsNone(self.hardware.get_bmc_mac()) + mock_execute.assert_not_called() + + @mock.patch.object(hardware.GenericHardwareManager, + 'any_ipmi_device_exists', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_get_bmc_v6address_no_ipmi_device(self, mock_execute, + mock_ipmi_device_exists): + mock_ipmi_device_exists.return_value = False + self.assertIsNone(self.hardware.get_bmc_v6address()) + mock_execute.assert_not_called() + @mock.patch.object(efi_utils, 'clean_boot_records', autospec=True) def test_clean_uefi_nvram_defaults(self, mock_efi_utils): self.hardware.clean_uefi_nvram(self.node, []) diff --git a/ironic_python_agent/utils.py b/ironic_python_agent/utils.py index cf0d3d955..0d8b7f78a 100644 --- a/ironic_python_agent/utils.py +++ b/ironic_python_agent/utils.py @@ -23,6 +23,7 @@ import json import os import re import shutil +import stat import subprocess import sys import tarfile @@ -972,3 +973,13 @@ def _unmount_any_config_drives(): _early_log('Issuing an umount command for /mnt/config...') execute('umount', '/mnt/config') time.sleep(1) + + +def is_char_device(path): + '''Check if the specified path is a character device.''' + try: + return stat.S_ISCHR(os.stat(path).st_mode) + except OSError: + # Likely because of insufficient permission, + # race conditions or I/O related errors. + return False diff --git a/releasenotes/notes/check-for-ipmi-device-before-invocation-45b00d15c94edd00.yaml b/releasenotes/notes/check-for-ipmi-device-before-invocation-45b00d15c94edd00.yaml new file mode 100644 index 000000000..2ff562d16 --- /dev/null +++ b/releasenotes/notes/check-for-ipmi-device-before-invocation-45b00d15c94edd00.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Adds a preliminary check for IPMI device files before the BMC detection + attempts, optimizing `ipmitool` command calls on systems without IPMI + capabilities.