Skip to content

Commit 67db6fe

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 4fd3f25 commit 67db6fe

File tree

5 files changed

+345
-2
lines changed

5 files changed

+345
-2
lines changed

ironic_python_agent/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,15 @@
282282
'ipa-image-download-connection-retry-interval', 10),
283283
help='Interval (in seconds) between two attempts to establish '
284284
'connection when downloading an image.'),
285-
285+
cfg.StrOpt('enable_vlan_interfaces',
286+
default=APARAMS.get('ipa-enable-vlan-interfaces', ''),
287+
help='Comma-separated list of VLAN interfaces to enable, '
288+
'in the format "interface.vlan". If only an '
289+
'interface is provided, then IPA should attempt to '
290+
'bring up all VLANs on that interface detected '
291+
'via lldp. If "all" is set then IPA should attempt '
292+
'to bring up all VLANs from lldp on all interfaces. '
293+
'By default, no VLANs will be brought up.'),
286294
]
287295

288296
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
@@ -1038,6 +1038,12 @@ def get_bios_given_nic_name(self, interface_name):
10381038
as None.
10391039
"""
10401040
global WARN_BIOSDEVNAME_NOT_FOUND
1041+
1042+
if self._is_vlan(interface_name):
1043+
LOG.debug('Interface %s is a VLAN, biosdevname not called',
1044+
interface_name)
1045+
return
1046+
10411047
try:
10421048
stdout, _ = utils.execute('biosdevname', '-i',
10431049
interface_name)
@@ -1060,10 +1066,19 @@ def _is_device(self, interface_name):
10601066
interface_name)
10611067
return os.path.exists(device_path)
10621068

1069+
def _is_vlan(self, interface_name):
1070+
# A VLAN interface does not have /device, check naming convention
1071+
# used when adding VLAN interface
1072+
1073+
interface, sep, vlan = interface_name.partition('.')
1074+
1075+
return vlan.isdigit()
1076+
10631077
def list_network_interfaces(self):
10641078
network_interfaces_list = []
10651079
iface_names = os.listdir('{}/class/net'.format(self.sys_path))
1066-
iface_names = [name for name in iface_names if self._is_device(name)]
1080+
iface_names = [name for name in iface_names
1081+
if self._is_vlan(name) or self._is_device(name)]
10671082

10681083
if CONF.collect_lldp:
10691084
self.lldp_data = dispatch_to_managers('collect_lldp_data',
@@ -1075,6 +1090,16 @@ def list_network_interfaces(self):
10751090
result.lldp = self._get_lldp_data(iface_name)
10761091
network_interfaces_list.append(result)
10771092

1093+
# If configured, bring up vlan interfaces. If the actual vlans aren't
1094+
# defined they are derived from LLDP data
1095+
if CONF.enable_vlan_interfaces:
1096+
vlan_iface_names = netutils.bring_up_vlan_interfaces(
1097+
network_interfaces_list)
1098+
for vlan_iface_name in vlan_iface_names:
1099+
result = dispatch_to_managers(
1100+
'get_interface_info', interface_name=vlan_iface_name)
1101+
network_interfaces_list.append(result)
1102+
10781103
return network_interfaces_list
10791104

10801105
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)