Skip to content

Commit bd45db1

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Bring up VLAN interfaces and include in introspection report" into stable/victoria
2 parents 91955ff + 67db6fe commit bd45db1

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
@@ -287,7 +287,15 @@
287287
'ipa-image-download-connection-retry-interval', 10),
288288
help='Interval (in seconds) between two attempts to establish '
289289
'connection when downloading an image.'),
290-
290+
cfg.StrOpt('enable_vlan_interfaces',
291+
default=APARAMS.get('ipa-enable-vlan-interfaces', ''),
292+
help='Comma-separated list of VLAN interfaces to enable, '
293+
'in the format "interface.vlan". If only an '
294+
'interface is provided, then IPA should attempt to '
295+
'bring up all VLANs on that interface detected '
296+
'via lldp. If "all" is set then IPA should attempt '
297+
'to bring up all VLANs from lldp on all interfaces. '
298+
'By default, no VLANs will be brought up.'),
291299
]
292300

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