Merge "Discover IPv6 BMC address"

This commit is contained in:
Zuul 2019-04-10 03:49:03 +00:00 committed by Gerrit Code Review
commit 55e3266c89
3 changed files with 208 additions and 0 deletions

View File

@ -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 [
{

View File

@ -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

View File

@ -0,0 +1,5 @@
---
features:
- |
Discover IPv6 BMC address and store it in "bmc_v6address"
field of hardware inventory sent back to inspector.