Skip to content

Commit 6e3f28d

Browse files
committed
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
1 parent 162c6ca commit 6e3f28d

5 files changed

Lines changed: 344 additions & 1 deletion

File tree

ironic_python_agent/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,15 @@
291291
'This is an advanced override setting which may only '
292292
'be useful if the environment requires API version '
293293
'auto-detection to be disabled or blocked.'),
294+
cfg.StrOpt('enable_vlan_interfaces',
295+
default=APARAMS.get('ipa-enable-vlan-interfaces', ''),
296+
help='Comma-separated list of VLAN interfaces to enable, '
297+
'in the format "interface.vlan". If only an '
298+
'interface is provided, then IPA should attempt to '
299+
'bring up all VLANs on that interface detected '
300+
'via lldp. If "all" is set then IPA should attempt '
301+
'to bring up all VLANs from lldp on all interfaces. '
302+
'By default, no VLANs will be brought up.'),
294303
]
295304

296305
CONF.register_cli_opts(cli_opts)

ironic_python_agent/hardware.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,12 @@ def get_bios_given_nic_name(self, interface_name):
10561056
as None.
10571057
"""
10581058
global WARN_BIOSDEVNAME_NOT_FOUND
1059+
1060+
if self._is_vlan(interface_name):
1061+
LOG.debug('Interface %s is a VLAN, biosdevname not called',
1062+
interface_name)
1063+
return
1064+
10591065
try:
10601066
stdout, _ = utils.execute('biosdevname', '-i',
10611067
interface_name)
@@ -1078,10 +1084,19 @@ def _is_device(self, interface_name):
10781084
interface_name)
10791085
return os.path.exists(device_path)
10801086

1087+
def _is_vlan(self, interface_name):
1088+
# A VLAN interface does not have /device, check naming convention
1089+
# used when adding VLAN interface
1090+
1091+
interface, sep, vlan = interface_name.partition('.')
1092+
1093+
return vlan.isdigit()
1094+
10811095
def list_network_interfaces(self):
10821096
network_interfaces_list = []
10831097
iface_names = os.listdir('{}/class/net'.format(self.sys_path))
1084-
iface_names = [name for name in iface_names if self._is_device(name)]
1098+
iface_names = [name for name in iface_names
1099+
if self._is_vlan(name) or self._is_device(name)]
10851100

10861101
if CONF.collect_lldp:
10871102
self.lldp_data = dispatch_to_managers('collect_lldp_data',
@@ -1093,6 +1108,16 @@ def list_network_interfaces(self):
10931108
result.lldp = self._get_lldp_data(iface_name)
10941109
network_interfaces_list.append(result)
10951110

1111+
# If configured, bring up vlan interfaces. If the actual vlans aren't
1112+
# defined they are derived from LLDP data
1113+
if CONF.enable_vlan_interfaces:
1114+
vlan_iface_names = netutils.bring_up_vlan_interfaces(
1115+
network_interfaces_list)
1116+
for vlan_iface_name in vlan_iface_names:
1117+
result = dispatch_to_managers(
1118+
'get_interface_info', interface_name=vlan_iface_name)
1119+
network_interfaces_list.append(result)
1120+
10961121
return network_interfaces_list
10971122

10981123
def get_cpus(self):

ironic_python_agent/netutils.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from oslo_log import log as logging
2525
from oslo_utils import netutils
2626

27+
from ironic_python_agent import utils
28+
2729
LOG = logging.getLogger(__name__)
2830
CONF = cfg.CONF
2931

@@ -33,6 +35,14 @@
3335
SIOCSIFFLAGS = 0x8914
3436
INFINIBAND_ADDR_LEN = 59
3537

38+
# LLDP definitions needed to extract vlan information
39+
LLDP_TLV_ORG_SPECIFIC = 127
40+
# 802.1Q defines from http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D
41+
LLDP_802dot1_OUI = "0080c2"
42+
# subtypes
43+
dot1_VLAN_NAME = "03"
44+
VLAN_ID_LEN = len(LLDP_802dot1_OUI + dot1_VLAN_NAME)
45+
3646

3747
class ifreq(ctypes.Structure):
3848
"""Class for setting flags on a socket."""
@@ -258,3 +268,110 @@ def get_wildcard_address():
258268
if netutils.is_ipv6_enabled():
259269
return "::"
260270
return "0.0.0.0"
271+
272+
273+
def _get_configured_vlans():
274+
return [x.strip() for x in CONF.enable_vlan_interfaces.split(',')
275+
if x.strip()]
276+
277+
278+
def _add_vlan_interface(interface, vlan, interfaces_list):
279+
280+
vlan_name = interface + '.' + vlan
281+
282+
# if any(x for x in interfaces_list if x.name == vlan_name):
283+
if any(x.name == vlan_name for x in interfaces_list):
284+
LOG.info("VLAN interface %s has already been added", vlan_name)
285+
return ''
286+
287+
try:
288+
LOG.info('Adding VLAN interface %s', vlan_name)
289+
# Add the interface
290+
utils.execute('ip', 'link', 'add', 'link', interface, 'name',
291+
vlan_name, 'type', 'vlan', 'id', vlan,
292+
check_exit_code=[0, 2])
293+
294+
# Bring up interface
295+
utils.execute('ip', 'link', 'set', 'dev', vlan_name, 'up')
296+
297+
except Exception as exc:
298+
LOG.warning('Exception when running ip commands to add VLAN '
299+
'interface: %s', exc)
300+
return ''
301+
302+
return vlan_name
303+
304+
305+
def _add_vlans_from_lldp(lldp, interface, interfaces_list):
306+
interfaces = []
307+
308+
# Get the lldp packets received on this interface
309+
if lldp:
310+
for type, value in lldp:
311+
if (type == LLDP_TLV_ORG_SPECIFIC
312+
and value.startswith(LLDP_802dot1_OUI
313+
+ dot1_VLAN_NAME)):
314+
vlan = str(int(value[VLAN_ID_LEN: VLAN_ID_LEN + 4], 16))
315+
name = _add_vlan_interface(interface, vlan,
316+
interfaces_list)
317+
if name:
318+
interfaces.append(name)
319+
else:
320+
LOG.debug('VLAN interface %s does not have lldp info', interface)
321+
322+
return interfaces
323+
324+
325+
def bring_up_vlan_interfaces(interfaces_list):
326+
"""Bring up vlan interfaces based on kernel params
327+
328+
Use the configured value of ``enable_vlan_interfaces`` to determine
329+
if VLAN interfaces should be brought up using ``ip`` commands. If
330+
``enable_vlan_interfaces`` defines a particular vlan then bring up
331+
that vlan. If it defines an interface or ``all`` then use LLDP info
332+
to figure out which VLANs should be brought up.
333+
334+
:param interfaces_list: List of current interfaces
335+
:return: List of vlan interface names that have been added
336+
"""
337+
interfaces = []
338+
vlan_interfaces = _get_configured_vlans()
339+
for vlan_int in vlan_interfaces:
340+
# TODO(bfournie) skip if pxe boot interface
341+
if '.' in vlan_int:
342+
# interface and vlan are provided
343+
interface, vlan = vlan_int.split('.', 1)
344+
if any(x.name == interface for x in interfaces_list):
345+
name = _add_vlan_interface(interface, vlan,
346+
interfaces_list)
347+
if name:
348+
interfaces.append(name)
349+
else:
350+
LOG.warning('Provided VLAN interface %s does not exist',
351+
interface)
352+
elif CONF.collect_lldp:
353+
# Get the vlans from lldp info
354+
if vlan_int == 'all':
355+
# Use all interfaces
356+
for iface in interfaces_list:
357+
names = _add_vlans_from_lldp(
358+
iface.lldp, iface.name, interfaces_list)
359+
if names:
360+
interfaces.extend(names)
361+
else:
362+
# Use provided interface
363+
lldp = next((x.lldp for x in interfaces_list
364+
if x.name == vlan_int), None)
365+
if lldp:
366+
names = _add_vlans_from_lldp(lldp, vlan_int,
367+
interfaces_list)
368+
if names:
369+
interfaces.extend(names)
370+
else:
371+
LOG.warning('Provided interface name %s was not found',
372+
vlan_int)
373+
else:
374+
LOG.warning('Attempting to add VLAN interfaces but specific '
375+
'interface not provided and LLDP not enabled')
376+
377+
return interfaces

ironic_python_agent/tests/unit/test_hardware.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,186 @@ def test_list_network_interfaces_with_vendor_info(self,
13181318
self.assertEqual('0x1014', interfaces[0].product)
13191319
self.assertEqual('em0', interfaces[0].biosdevname)
13201320

1321+
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
1322+
@mock.patch('netifaces.ifaddresses', autospec=True)
1323+
@mock.patch('os.listdir', autospec=True)
1324+
@mock.patch('os.path.exists', autospec=True)
1325+
@mock.patch('builtins.open', autospec=True)
1326+
@mock.patch.object(utils, 'execute', autospec=True)
1327+
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
1328+
@mock.patch.object(netutils, 'interface_has_carrier', autospec=True)
1329+
def test_list_network_vlan_interfaces(self,
1330+
mock_has_carrier,
1331+
mock_get_mac,
1332+
mocked_execute,
1333+
mocked_open,
1334+
mocked_exists,
1335+
mocked_listdir,
1336+
mocked_ifaddresses,
1337+
mockedget_managers):
1338+
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
1339+
CONF.set_override('enable_vlan_interfaces', 'eth0.100')
1340+
mocked_listdir.return_value = ['lo', 'eth0']
1341+
mocked_exists.side_effect = [False, True, False]
1342+
mocked_open.return_value.__enter__ = lambda s: s
1343+
mocked_open.return_value.__exit__ = mock.Mock()
1344+
read_mock = mocked_open.return_value.read
1345+
read_mock.side_effect = ['1']
1346+
mocked_ifaddresses.return_value = {
1347+
netifaces.AF_INET: [{'addr': '192.168.1.2'}],
1348+
netifaces.AF_INET6: [{'addr': 'fd00::101'}]
1349+
}
1350+
mocked_execute.return_value = ('em0\n', '')
1351+
mock_get_mac.mock_has_carrier = True
1352+
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
1353+
interfaces = self.hardware.list_network_interfaces()
1354+
self.assertEqual(2, len(interfaces))
1355+
self.assertEqual('eth0', interfaces[0].name)
1356+
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
1357+
self.assertEqual('192.168.1.2', interfaces[0].ipv4_address)
1358+
self.assertEqual('fd00::101', interfaces[0].ipv6_address)
1359+
self.assertIsNone(interfaces[0].lldp)
1360+
self.assertEqual('eth0.100', interfaces[1].name)
1361+
self.assertEqual('00:0c:29:8c:11:b1', interfaces[1].mac_address)
1362+
self.assertIsNone(interfaces[1].lldp)
1363+
1364+
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
1365+
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
1366+
@mock.patch('netifaces.ifaddresses', autospec=True)
1367+
@mock.patch('os.listdir', autospec=True)
1368+
@mock.patch('os.path.exists', autospec=True)
1369+
@mock.patch('builtins.open', autospec=True)
1370+
@mock.patch.object(utils, 'execute', autospec=True)
1371+
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
1372+
@mock.patch.object(netutils, 'interface_has_carrier', autospec=True)
1373+
def test_list_network_vlan_interfaces_using_lldp(self,
1374+
mock_has_carrier,
1375+
mock_get_mac,
1376+
mocked_execute,
1377+
mocked_open,
1378+
mocked_exists,
1379+
mocked_listdir,
1380+
mocked_ifaddresses,
1381+
mocked_lldp_info,
1382+
mockedget_managers):
1383+
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
1384+
CONF.set_override('collect_lldp', True)
1385+
CONF.set_override('enable_vlan_interfaces', 'eth0')
1386+
mocked_listdir.return_value = ['lo', 'eth0']
1387+
mocked_execute.return_value = ('em0\n', '')
1388+
mocked_exists.side_effect = [False, True, False]
1389+
mocked_open.return_value.__enter__ = lambda s: s
1390+
mocked_open.return_value.__exit__ = mock.Mock()
1391+
read_mock = mocked_open.return_value.read
1392+
read_mock.side_effect = ['1']
1393+
mocked_lldp_info.return_value = {'eth0': [
1394+
(0, b''),
1395+
(127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'),
1396+
(127, b'\x00\x80\xc2\x03\x00e\x08vlan-101')]
1397+
}
1398+
mock_has_carrier.return_value = True
1399+
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
1400+
interfaces = self.hardware.list_network_interfaces()
1401+
self.assertEqual(3, len(interfaces))
1402+
self.assertEqual('eth0', interfaces[0].name)
1403+
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
1404+
expected_lldp_info = [
1405+
(0, ''),
1406+
(127, "0080c203006408766c616e2d313030"),
1407+
(127, "0080c203006508766c616e2d313031")
1408+
]
1409+
self.assertEqual(expected_lldp_info, interfaces[0].lldp)
1410+
self.assertEqual('eth0.100', interfaces[1].name)
1411+
self.assertEqual('00:0c:29:8c:11:b1', interfaces[1].mac_address)
1412+
self.assertIsNone(interfaces[1].lldp)
1413+
self.assertEqual('eth0.101', interfaces[2].name)
1414+
self.assertEqual('00:0c:29:8c:11:b1', interfaces[2].mac_address)
1415+
self.assertIsNone(interfaces[2].lldp)
1416+
1417+
@mock.patch.object(netutils, 'LOG', autospec=True)
1418+
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
1419+
@mock.patch('netifaces.ifaddresses', autospec=True)
1420+
@mock.patch('os.listdir', autospec=True)
1421+
@mock.patch('os.path.exists', autospec=True)
1422+
@mock.patch('builtins.open', autospec=True)
1423+
@mock.patch.object(utils, 'execute', autospec=True)
1424+
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
1425+
@mock.patch.object(netutils, 'interface_has_carrier', autospec=True)
1426+
def test_list_network_vlan_invalid_int(self,
1427+
mock_has_carrier,
1428+
mock_get_mac,
1429+
mocked_execute,
1430+
mocked_open,
1431+
mocked_exists,
1432+
mocked_listdir,
1433+
mocked_ifaddresses,
1434+
mockedget_managers,
1435+
mocked_log):
1436+
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
1437+
CONF.set_override('collect_lldp', True)
1438+
CONF.set_override('enable_vlan_interfaces', 'enp0s1')
1439+
mocked_listdir.return_value = ['lo', 'eth0']
1440+
mocked_exists.side_effect = [False, True, False]
1441+
mocked_open.return_value.__enter__ = lambda s: s
1442+
mocked_open.return_value.__exit__ = mock.Mock()
1443+
read_mock = mocked_open.return_value.read
1444+
read_mock.side_effect = ['1']
1445+
mocked_ifaddresses.return_value = {
1446+
netifaces.AF_INET: [{'addr': '192.168.1.2'}],
1447+
netifaces.AF_INET6: [{'addr': 'fd00::101'}]
1448+
}
1449+
mocked_execute.return_value = ('em0\n', '')
1450+
mock_get_mac.mock_has_carrier = True
1451+
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
1452+
1453+
self.hardware.list_network_interfaces()
1454+
mocked_log.warning.assert_called_once_with(
1455+
'Provided interface name %s was not found', 'enp0s1')
1456+
1457+
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
1458+
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
1459+
@mock.patch('os.listdir', autospec=True)
1460+
@mock.patch('os.path.exists', autospec=True)
1461+
@mock.patch('builtins.open', autospec=True)
1462+
@mock.patch.object(utils, 'execute', autospec=True)
1463+
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
1464+
def test_list_network_vlan_interfaces_using_lldp_all(self,
1465+
mock_get_mac,
1466+
mocked_execute,
1467+
mocked_open,
1468+
mocked_exists,
1469+
mocked_listdir,
1470+
mocked_lldp_info,
1471+
mockedget_managers):
1472+
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
1473+
CONF.set_override('collect_lldp', True)
1474+
CONF.set_override('enable_vlan_interfaces', 'all')
1475+
mocked_listdir.return_value = ['lo', 'eth0', 'eth1']
1476+
mocked_execute.return_value = ('em0\n', '')
1477+
mocked_exists.side_effect = [False, True, True]
1478+
mocked_open.return_value.__enter__ = lambda s: s
1479+
mocked_open.return_value.__exit__ = mock.Mock()
1480+
read_mock = mocked_open.return_value.read
1481+
read_mock.side_effect = ['1']
1482+
mocked_lldp_info.return_value = {'eth0': [
1483+
(0, b''),
1484+
(127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'),
1485+
(127, b'\x00\x80\xc2\x03\x00e\x08vlan-101')],
1486+
'eth1': [
1487+
(0, b''),
1488+
(127, b'\x00\x80\xc2\x03\x00f\x08vlan-102'),
1489+
(127, b'\x00\x80\xc2\x03\x00g\x08vlan-103')]
1490+
}
1491+
1492+
interfaces = self.hardware.list_network_interfaces()
1493+
self.assertEqual(6, len(interfaces))
1494+
self.assertEqual('eth0', interfaces[0].name)
1495+
self.assertEqual('eth1', interfaces[1].name)
1496+
self.assertEqual('eth0.100', interfaces[2].name)
1497+
self.assertEqual('eth0.101', interfaces[3].name)
1498+
self.assertEqual('eth1.102', interfaces[4].name)
1499+
self.assertEqual('eth1.103', interfaces[5].name)
1500+
13211501
@mock.patch.object(os, 'readlink', autospec=True)
13221502
@mock.patch.object(os, 'listdir', autospec=True)
13231503
@mock.patch.object(hardware, 'get_cached_node', autospec=True)

0 commit comments

Comments
 (0)