diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index e615ca3b5..4ad9b4e16 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -18,6 +18,7 @@ import functools import json from multiprocessing.pool import ThreadPool import os +import re import shlex import time @@ -32,6 +33,7 @@ import psutil import pyudev import six import stevedore +import yaml from ironic_python_agent import encoding from ironic_python_agent import errors @@ -380,6 +382,9 @@ class HardwareManager(object): def get_bmc_address(self): raise errors.IncompatibleHardwareMethodError() + def get_bmc_v6address(self): + raise errors.IncompatibleHardwareMethodError() + def get_boot_info(self): raise errors.IncompatibleHardwareMethodError() @@ -493,6 +498,7 @@ class HardwareManager(object): hardware_info['disks'] = self.list_block_devices() hardware_info['memory'] = self.get_memory() hardware_info['bmc_address'] = self.get_bmc_address() + hardware_info['bmc_v6address'] = self.get_bmc_v6address() hardware_info['system_vendor'] = self.get_system_vendor_info() hardware_info['boot'] = self.get_boot_info() return hardware_info @@ -1138,6 +1144,76 @@ class GenericHardwareManager(HardwareManager): return '0.0.0.0' + 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 + configured properly. May return None value if it cannot + interract with system tools or critical error occurs. + """ + # These modules are rarely loaded automatically + utils.try_execute('modprobe', 'ipmi_msghandler') + utils.try_execute('modprobe', 'ipmi_devintf') + utils.try_execute('modprobe', 'ipmi_si') + + null_address_re = re.compile(r'^::(/\d{1,3})*$') + + def get_addr(channel, dynamic=False): + cmd = "ipmitool lan6 print {} {}_addr".format( + channel, 'dynamic' if dynamic else 'static') + try: + out, e = utils.execute(cmd, shell=True) + except processutils.ProcessExecutionError: + return + + # NOTE: More likely ipmitool was not intended to return + # stdout in yaml format. Fortunately, output of + # dynamic_addr and static_addr commands is a valid yaml. + try: + out = yaml.safe_load(out.strip()) + except yaml.YAMLError as e: + LOG.warning('Cannot process output of "%(cmd)s" ' + 'command: %(e)s', {'cmd': cmd, 'e': e}) + return + + for addr_dict in out.values(): + address = addr_dict['Address'] + if dynamic: + enabled = addr_dict['Source/Type'] in ['DHCPv6', 'SLAAC'] + else: + enabled = addr_dict['Enabled'] + + if addr_dict['Status'] == 'active' and enabled \ + and not null_address_re.match(address): + return address + + try: + # From all the channels 0-15, only 1-7 can be assigned to different + # types of communication media and protocols and effectively used + for channel in range(1, 8): + addr_mode, e = utils.execute( + r"ipmitool lan6 print {} enables | " + r"awk '/IPv6\/IPv4 Addressing Enables[ \t]*:/" + r"{{print $NF}}'".format(channel), shell=True) + if addr_mode.strip() not in ['ipv6', 'both']: + continue + + address = get_addr(channel, dynamic=True) or get_addr(channel) + if not address: + continue + + try: + return str(netaddr.IPNetwork(address).ip) + except netaddr.AddrFormatError: + LOG.warning('Invalid IP address: %s', address) + continue + except (processutils.ProcessExecutionError, OSError) as e: + # Not error, because it's normal in virtual environment + LOG.warning("Cannot get BMC v6 address: %s", e) + return + + return '::/0' + def get_clean_steps(self, node, ports): return [ { diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 6b7873a34..3ea5d3137 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -392,6 +392,37 @@ ATA Security is: Unavailable """) # noqa +IPMITOOL_LAN6_PRINT_DYNAMIC_ADDR = """ +IPv6 Dynamic Address 0: + Source/Type: DHCPv6 + Address: 2001:1234:1234:1234:1234:1234:1234:1234/64 + Status: active +IPv6 Dynamic Address 1: + Source/Type: DHCPv6 + Address: ::/0 + Status: active +IPv6 Dynamic Address 2: + Source/Type: DHCPv6 + Address: ::/0 + Status: active +""" + +IPMITOOL_LAN6_PRINT_STATIC_ADDR = """ +IPv6 Static Address 0: + Enabled: yes + Address: 2001:5678:5678:5678:5678:5678:5678:5678/64 + Status: active +IPv6 Static Address 1: + Enabled: no + Address: ::/0 + Status: disabled +IPv6 Static Address 2: + Enabled: no + Address: ::/0 + Status: disabled +""" + + class FakeHardwareManager(hardware.GenericHardwareManager): def __init__(self, hardware_support): self._hardware_support = hardware_support @@ -1126,6 +1157,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): current_boot_mode='bios', pxe_interface='boot:if') self.hardware.get_bmc_address = mock.Mock() + self.hardware.get_bmc_v6address = mock.Mock() self.hardware.get_system_vendor_info = mock.Mock() hardware_info = self.hardware.list_hardware_info() @@ -2085,6 +2117,101 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_execute.return_value = '', '' self.assertEqual('0.0.0.0', self.hardware.get_bmc_address()) + @mock.patch.object(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', '')] * 7 + self.assertEqual('::/0', self.hardware.get_bmc_v6address()) + + @mock.patch.object(utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_v6address_dynamic_address(self, mocked_execute, mte): + mocked_execute.side_effect = [ + ('ipv6\n', ''), + (IPMITOOL_LAN6_PRINT_DYNAMIC_ADDR, '') + ] + self.assertEqual('2001:1234:1234:1234:1234:1234:1234:1234', + self.hardware.get_bmc_v6address()) + + @mock.patch.object(utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_v6address_static_address_both(self, mocked_execute, mte): + dynamic_disabled = \ + IPMITOOL_LAN6_PRINT_DYNAMIC_ADDR.replace('active', 'disabled') + mocked_execute.side_effect = [ + ('both\n', ''), + (dynamic_disabled, ''), + (IPMITOOL_LAN6_PRINT_STATIC_ADDR, '') + ] + self.assertEqual('2001:5678:5678:5678:5678:5678:5678:5678', + self.hardware.get_bmc_v6address()) + + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_v6address_virt(self, mocked_execute): + mocked_execute.side_effect = processutils.ProcessExecutionError() + self.assertIsNone(self.hardware.get_bmc_v6address()) + + @mock.patch.object(utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_v6address_invalid_enables(self, mocked_execute, mte): + def side_effect(*args, **kwargs): + if args[0].startswith('ipmitool lan6 print'): + return '', 'Failed to get IPv6/IPv4 Addressing Enables' + + mocked_execute.side_effect = side_effect + self.assertEqual('::/0', self.hardware.get_bmc_v6address()) + + @mock.patch.object(utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_v6address_invalid_get_address(self, mocked_execute, mte): + def side_effect(*args, **kwargs): + if args[0].startswith('ipmitool lan6 print'): + if args[0].endswith('dynamic_addr') \ + or args[0].endswith('static_addr'): + raise processutils.ProcessExecutionError() + return 'ipv6', '' + + mocked_execute.side_effect = side_effect + self.assertEqual('::/0', self.hardware.get_bmc_v6address()) + + @mock.patch.object(hardware, 'LOG', autospec=True) + @mock.patch.object(utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_v6address_impitool_invalid_stdout_format( + self, mocked_execute, mte, mocked_log): + def side_effect(*args, **kwargs): + if args[0].startswith('ipmitool lan6 print'): + if args[0].endswith('dynamic_addr') \ + or args[0].endswith('static_addr'): + return 'Invalid\n\tyaml', '' + return 'ipv6', '' + + mocked_execute.side_effect = side_effect + self.assertEqual('::/0', self.hardware.get_bmc_v6address()) + one_call = mock.call('Cannot process output of "%(cmd)s" ' + 'command: %(e)s', mock.ANY) + mocked_log.warning.assert_has_calls([one_call] * 14) + + @mock.patch.object(utils, 'try_execute', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_bmc_v6address_channel_7(self, mocked_execute, mte): + def side_effect(*args, **kwargs): + if not args[0].startswith('ipmitool lan6 print 7'): + # ipv6 is not enabled for channels 1-6 + if 'enables |' in args[0]: + return '', '' + else: + if 'enables |' in args[0]: + return 'ipv6', '' + if args[0].endswith('dynamic_addr'): + raise processutils.ProcessExecutionError() + elif args[0].endswith('static_addr'): + return IPMITOOL_LAN6_PRINT_STATIC_ADDR, '' + + mocked_execute.side_effect = side_effect + self.assertEqual('2001:5678:5678:5678:5678:5678:5678:5678', + self.hardware.get_bmc_v6address()) + @mock.patch.object(utils, 'execute', autospec=True) def test_get_system_vendor_info(self, mocked_execute): mocked_execute.return_value = LSHW_JSON_OUTPUT diff --git a/releasenotes/notes/discover-ipv6-bmc-address-b3b357ff6c5d822c.yaml b/releasenotes/notes/discover-ipv6-bmc-address-b3b357ff6c5d822c.yaml new file mode 100644 index 000000000..73ebfabe3 --- /dev/null +++ b/releasenotes/notes/discover-ipv6-bmc-address-b3b357ff6c5d822c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Discover IPv6 BMC address and store it in "bmc_v6address" + field of hardware inventory sent back to inspector.