Bring up VLAN interfaces and include in introspection report

Add the ability to bring up VLAN interfaces and include them in the
introspection report.  A new configuration field is added -
``ipa-enable-vlan-interfaces``, which defines either the VLAN interface
to enable, the interface to use, or 'all' - which indicates all
interfaces.  If the particular VLAN is not provided, IPA will
use the lldp info for the interface to determine which VLANs should
be enabled.

Change-Id: Icb4f66a02b298b4d165ebb58134cd31029e535cc
Story: 2008298
Task: 41183
This commit is contained in:
Bob Fournier
2020-10-30 10:32:45 -04:00
parent 4fd3f25cd6
commit 67db6fe344
5 changed files with 345 additions and 2 deletions

View File

@@ -282,7 +282,15 @@ cli_opts = [
'ipa-image-download-connection-retry-interval', 10),
help='Interval (in seconds) between two attempts to establish '
'connection when downloading an image.'),
cfg.StrOpt('enable_vlan_interfaces',
default=APARAMS.get('ipa-enable-vlan-interfaces', ''),
help='Comma-separated list of VLAN interfaces to enable, '
'in the format "interface.vlan". If only an '
'interface is provided, then IPA should attempt to '
'bring up all VLANs on that interface detected '
'via lldp. If "all" is set then IPA should attempt '
'to bring up all VLANs from lldp on all interfaces. '
'By default, no VLANs will be brought up.'),
]
CONF.register_cli_opts(cli_opts)

View File

@@ -1038,6 +1038,12 @@ class GenericHardwareManager(HardwareManager):
as None.
"""
global WARN_BIOSDEVNAME_NOT_FOUND
if self._is_vlan(interface_name):
LOG.debug('Interface %s is a VLAN, biosdevname not called',
interface_name)
return
try:
stdout, _ = utils.execute('biosdevname', '-i',
interface_name)
@@ -1060,10 +1066,19 @@ class GenericHardwareManager(HardwareManager):
interface_name)
return os.path.exists(device_path)
def _is_vlan(self, interface_name):
# A VLAN interface does not have /device, check naming convention
# used when adding VLAN interface
interface, sep, vlan = interface_name.partition('.')
return vlan.isdigit()
def list_network_interfaces(self):
network_interfaces_list = []
iface_names = os.listdir('{}/class/net'.format(self.sys_path))
iface_names = [name for name in iface_names if self._is_device(name)]
iface_names = [name for name in iface_names
if self._is_vlan(name) or self._is_device(name)]
if CONF.collect_lldp:
self.lldp_data = dispatch_to_managers('collect_lldp_data',
@@ -1075,6 +1090,16 @@ class GenericHardwareManager(HardwareManager):
result.lldp = self._get_lldp_data(iface_name)
network_interfaces_list.append(result)
# If configured, bring up vlan interfaces. If the actual vlans aren't
# defined they are derived from LLDP data
if CONF.enable_vlan_interfaces:
vlan_iface_names = netutils.bring_up_vlan_interfaces(
network_interfaces_list)
for vlan_iface_name in vlan_iface_names:
result = dispatch_to_managers(
'get_interface_info', interface_name=vlan_iface_name)
network_interfaces_list.append(result)
return network_interfaces_list
def get_cpus(self):

View File

@@ -24,6 +24,8 @@ from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import netutils
from ironic_python_agent import utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
@@ -33,6 +35,14 @@ SIOCGIFFLAGS = 0x8913
SIOCSIFFLAGS = 0x8914
INFINIBAND_ADDR_LEN = 59
# LLDP definitions needed to extract vlan information
LLDP_TLV_ORG_SPECIFIC = 127
# 802.1Q defines from http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D
LLDP_802dot1_OUI = "0080c2"
# subtypes
dot1_VLAN_NAME = "03"
VLAN_ID_LEN = len(LLDP_802dot1_OUI + dot1_VLAN_NAME)
class ifreq(ctypes.Structure):
"""Class for setting flags on a socket."""
@@ -258,3 +268,110 @@ def get_wildcard_address():
if netutils.is_ipv6_enabled():
return "::"
return "0.0.0.0"
def _get_configured_vlans():
return [x.strip() for x in CONF.enable_vlan_interfaces.split(',')
if x.strip()]
def _add_vlan_interface(interface, vlan, interfaces_list):
vlan_name = interface + '.' + vlan
# if any(x for x in interfaces_list if x.name == vlan_name):
if any(x.name == vlan_name for x in interfaces_list):
LOG.info("VLAN interface %s has already been added", vlan_name)
return ''
try:
LOG.info('Adding VLAN interface %s', vlan_name)
# Add the interface
utils.execute('ip', 'link', 'add', 'link', interface, 'name',
vlan_name, 'type', 'vlan', 'id', vlan,
check_exit_code=[0, 2])
# Bring up interface
utils.execute('ip', 'link', 'set', 'dev', vlan_name, 'up')
except Exception as exc:
LOG.warning('Exception when running ip commands to add VLAN '
'interface: %s', exc)
return ''
return vlan_name
def _add_vlans_from_lldp(lldp, interface, interfaces_list):
interfaces = []
# Get the lldp packets received on this interface
if lldp:
for type, value in lldp:
if (type == LLDP_TLV_ORG_SPECIFIC
and value.startswith(LLDP_802dot1_OUI
+ dot1_VLAN_NAME)):
vlan = str(int(value[VLAN_ID_LEN: VLAN_ID_LEN + 4], 16))
name = _add_vlan_interface(interface, vlan,
interfaces_list)
if name:
interfaces.append(name)
else:
LOG.debug('VLAN interface %s does not have lldp info', interface)
return interfaces
def bring_up_vlan_interfaces(interfaces_list):
"""Bring up vlan interfaces based on kernel params
Use the configured value of ``enable_vlan_interfaces`` to determine
if VLAN interfaces should be brought up using ``ip`` commands. If
``enable_vlan_interfaces`` defines a particular vlan then bring up
that vlan. If it defines an interface or ``all`` then use LLDP info
to figure out which VLANs should be brought up.
:param interfaces_list: List of current interfaces
:return: List of vlan interface names that have been added
"""
interfaces = []
vlan_interfaces = _get_configured_vlans()
for vlan_int in vlan_interfaces:
# TODO(bfournie) skip if pxe boot interface
if '.' in vlan_int:
# interface and vlan are provided
interface, vlan = vlan_int.split('.', 1)
if any(x.name == interface for x in interfaces_list):
name = _add_vlan_interface(interface, vlan,
interfaces_list)
if name:
interfaces.append(name)
else:
LOG.warning('Provided VLAN interface %s does not exist',
interface)
elif CONF.collect_lldp:
# Get the vlans from lldp info
if vlan_int == 'all':
# Use all interfaces
for iface in interfaces_list:
names = _add_vlans_from_lldp(
iface.lldp, iface.name, interfaces_list)
if names:
interfaces.extend(names)
else:
# Use provided interface
lldp = next((x.lldp for x in interfaces_list
if x.name == vlan_int), None)
if lldp:
names = _add_vlans_from_lldp(lldp, vlan_int,
interfaces_list)
if names:
interfaces.extend(names)
else:
LOG.warning('Provided interface name %s was not found',
vlan_int)
else:
LOG.warning('Attempting to add VLAN interfaces but specific '
'interface not provided and LLDP not enabled')
return interfaces

View File

@@ -1318,6 +1318,186 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual('0x1014', interfaces[0].product)
self.assertEqual('em0', interfaces[0].biosdevname)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
@mock.patch.object(netutils, 'interface_has_carrier', autospec=True)
def test_list_network_vlan_interfaces(self,
mock_has_carrier,
mock_get_mac,
mocked_execute,
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
CONF.set_override('enable_vlan_interfaces', 'eth0.100')
mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, True, False]
mocked_open.return_value.__enter__ = lambda s: s
mocked_open.return_value.__exit__ = mock.Mock()
read_mock = mocked_open.return_value.read
read_mock.side_effect = ['1']
mocked_ifaddresses.return_value = {
netifaces.AF_INET: [{'addr': '192.168.1.2'}],
netifaces.AF_INET6: [{'addr': 'fd00::101'}]
}
mocked_execute.return_value = ('em0\n', '')
mock_get_mac.mock_has_carrier = True
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
interfaces = self.hardware.list_network_interfaces()
self.assertEqual(2, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
self.assertEqual('192.168.1.2', interfaces[0].ipv4_address)
self.assertEqual('fd00::101', interfaces[0].ipv6_address)
self.assertIsNone(interfaces[0].lldp)
self.assertEqual('eth0.100', interfaces[1].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[1].mac_address)
self.assertIsNone(interfaces[1].lldp)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
@mock.patch.object(netutils, 'interface_has_carrier', autospec=True)
def test_list_network_vlan_interfaces_using_lldp(self,
mock_has_carrier,
mock_get_mac,
mocked_execute,
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mocked_lldp_info,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
CONF.set_override('collect_lldp', True)
CONF.set_override('enable_vlan_interfaces', 'eth0')
mocked_listdir.return_value = ['lo', 'eth0']
mocked_execute.return_value = ('em0\n', '')
mocked_exists.side_effect = [False, True, False]
mocked_open.return_value.__enter__ = lambda s: s
mocked_open.return_value.__exit__ = mock.Mock()
read_mock = mocked_open.return_value.read
read_mock.side_effect = ['1']
mocked_lldp_info.return_value = {'eth0': [
(0, b''),
(127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'),
(127, b'\x00\x80\xc2\x03\x00e\x08vlan-101')]
}
mock_has_carrier.return_value = True
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
interfaces = self.hardware.list_network_interfaces()
self.assertEqual(3, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
expected_lldp_info = [
(0, ''),
(127, "0080c203006408766c616e2d313030"),
(127, "0080c203006508766c616e2d313031")
]
self.assertEqual(expected_lldp_info, interfaces[0].lldp)
self.assertEqual('eth0.100', interfaces[1].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[1].mac_address)
self.assertIsNone(interfaces[1].lldp)
self.assertEqual('eth0.101', interfaces[2].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[2].mac_address)
self.assertIsNone(interfaces[2].lldp)
@mock.patch.object(netutils, 'LOG', autospec=True)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
@mock.patch.object(netutils, 'interface_has_carrier', autospec=True)
def test_list_network_vlan_invalid_int(self,
mock_has_carrier,
mock_get_mac,
mocked_execute,
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers,
mocked_log):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
CONF.set_override('collect_lldp', True)
CONF.set_override('enable_vlan_interfaces', 'enp0s1')
mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, True, False]
mocked_open.return_value.__enter__ = lambda s: s
mocked_open.return_value.__exit__ = mock.Mock()
read_mock = mocked_open.return_value.read
read_mock.side_effect = ['1']
mocked_ifaddresses.return_value = {
netifaces.AF_INET: [{'addr': '192.168.1.2'}],
netifaces.AF_INET6: [{'addr': 'fd00::101'}]
}
mocked_execute.return_value = ('em0\n', '')
mock_get_mac.mock_has_carrier = True
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
self.hardware.list_network_interfaces()
mocked_log.warning.assert_called_once_with(
'Provided interface name %s was not found', 'enp0s1')
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
def test_list_network_vlan_interfaces_using_lldp_all(self,
mock_get_mac,
mocked_execute,
mocked_open,
mocked_exists,
mocked_listdir,
mocked_lldp_info,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
CONF.set_override('collect_lldp', True)
CONF.set_override('enable_vlan_interfaces', 'all')
mocked_listdir.return_value = ['lo', 'eth0', 'eth1']
mocked_execute.return_value = ('em0\n', '')
mocked_exists.side_effect = [False, True, True]
mocked_open.return_value.__enter__ = lambda s: s
mocked_open.return_value.__exit__ = mock.Mock()
read_mock = mocked_open.return_value.read
read_mock.side_effect = ['1']
mocked_lldp_info.return_value = {'eth0': [
(0, b''),
(127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'),
(127, b'\x00\x80\xc2\x03\x00e\x08vlan-101')],
'eth1': [
(0, b''),
(127, b'\x00\x80\xc2\x03\x00f\x08vlan-102'),
(127, b'\x00\x80\xc2\x03\x00g\x08vlan-103')]
}
interfaces = self.hardware.list_network_interfaces()
self.assertEqual(6, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('eth1', interfaces[1].name)
self.assertEqual('eth0.100', interfaces[2].name)
self.assertEqual('eth0.101', interfaces[3].name)
self.assertEqual('eth1.102', interfaces[4].name)
self.assertEqual('eth1.103', interfaces[5].name)
@mock.patch.object(os, 'readlink', autospec=True)
@mock.patch.object(os, 'listdir', autospec=True)
@mock.patch.object(hardware, 'get_cached_node', autospec=True)

View File

@@ -0,0 +1,13 @@
---
features:
- |
Adds the ability to bring up VLAN interfaces and include them in the
introspection report. This is needed in environments that require an
IP address to be configured on tagged VLANs. A new kernel params
field is added - ``ipa-enable-vlan-interfaces``, which defines
either the VLAN interface to enable, the interface to use, or 'all' -
which indicates all interfaces. If the particular VLAN is not provided,
IPA will use the LLDP information for the interface to determine which
VLANs should be enabled. See
`story 2008298 <https://storyboard.openstack.org/#!/story/2008298>`_.