diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 3ea1e45c..e6deb162 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -810,11 +810,13 @@ class NetworkInterface(encoding.SerializableComparable): serializable_fields = ('name', 'mac_address', 'ipv4_address', 'ipv6_address', 'has_carrier', 'lldp', 'vendor', 'product', 'client_id', - 'biosdevname', 'speed_mbps') + 'biosdevname', 'speed_mbps', 'pci_address', + 'driver') def __init__(self, name, mac_addr, ipv4_address=None, ipv6_address=None, has_carrier=True, lldp=None, vendor=None, product=None, - client_id=None, biosdevname=None, speed_mbps=None): + client_id=None, biosdevname=None, speed_mbps=None, + pci_address=None, driver=None): self.name = name self.mac_address = mac_addr self.ipv4_address = ipv4_address @@ -825,6 +827,8 @@ def __init__(self, name, mac_addr, ipv4_address=None, ipv6_address=None, self.product = product self.biosdevname = biosdevname self.speed_mbps = speed_mbps + self.pci_address = pci_address + self.driver = driver # client_id is used for InfiniBand only. we calculate the DHCP # client identifier Option to allow DHCP to work over InfiniBand. # see https://tools.ietf.org/html/rfc4390 @@ -1448,7 +1452,10 @@ def get_interface_info(self, interface_name): vendor=_get_device_info(interface_name, 'net', 'vendor'), product=_get_device_info(interface_name, 'net', 'device'), biosdevname=self.get_bios_given_nic_name(interface_name), - speed_mbps=self._get_network_speed(interface_name)) + speed_mbps=self._get_network_speed(interface_name), + pci_address=netutils.get_interface_pci_address(interface_name), + driver=netutils.get_interface_driver(interface_name) + ) def get_ipv4_addr(self, interface_id): return netutils.get_ipv4_addr(interface_id) diff --git a/ironic_python_agent/netutils.py b/ironic_python_agent/netutils.py index 2e289808..9eca6933 100644 --- a/ironic_python_agent/netutils.py +++ b/ironic_python_agent/netutils.py @@ -20,10 +20,10 @@ import struct import sys -import netifaces from oslo_config import cfg from oslo_log import log as logging from oslo_utils import netutils +import psutil from ironic_python_agent import utils @@ -34,6 +34,13 @@ IFF_PROMISC = 0x100 SIOCGIFFLAGS = 0x8913 SIOCSIFFLAGS = 0x8914 +# SIOCETHTOOL from linux/sockios.h +SIOCETHTOOL = 0x8946 +# ETHTOOL_GPERMADDR from linux/ethtool.h +ETHTOOL_GPERMADDR = 0x00000020 +# MAX_ADDR_LEN from linux/netdevice.h +MAX_ADDR_LEN = 32 + INFINIBAND_ADDR_LEN = 59 # LLDP definitions needed to extract vlan information @@ -45,10 +52,25 @@ VLAN_ID_LEN = len(LLDP_802dot1_OUI + dot1_VLAN_NAME) +class ethtoolPermAddr(ctypes.Structure): + """Class for getting interface permanent MAC address""" + _fields_ = [("cmd", ctypes.c_uint32), + ("size", ctypes.c_uint32), + ("data", ctypes.c_uint8 * MAX_ADDR_LEN)] + + +class ifreq_data(ctypes.Union): + _fields_ = [("ifr_flags", ctypes.c_short), + ( + "ifr_data_ethtool_perm_addr", + ctypes.POINTER(ethtoolPermAddr))] + + class ifreq(ctypes.Structure): - """Class for setting flags on a socket.""" + """Class for ioctl on socket.""" + _anonymous_ = ("ifr_data",) _fields_ = [("ifr_ifrn", ctypes.c_char * 16), - ("ifr_flags", ctypes.c_short)] + ("ifr_data", ifreq_data)] class RawPromiscuousSockets(object): @@ -214,31 +236,46 @@ def _get_lldp_info(interfaces): return lldp_info -def get_default_ip_addr(type, interface_id): - """Retrieve default IPv4 or IPv6 address.""" +def get_default_ip_addr(family, interface_id): + """Retrieve default IPv4, IPv6 or mac address.""" try: - addrs = netifaces.ifaddresses(interface_id) - return addrs[type][0]['addr'] - except (ValueError, IndexError, KeyError): + addrs = psutil.net_if_addrs()[interface_id] + for addr in addrs: + if addr.family == family: + return addr.address + except KeyError: # No default IP address found - return None + pass + return None def get_ipv4_addr(interface_id): - return get_default_ip_addr(netifaces.AF_INET, interface_id) + return get_default_ip_addr(socket.AF_INET, interface_id) def get_ipv6_addr(interface_id): - return get_default_ip_addr(netifaces.AF_INET6, interface_id) + return get_default_ip_addr(socket.AF_INET6, interface_id) def get_mac_addr(interface_id): + """Retrieve permanent mac address, if unable to fallback to default one""" try: - addrs = netifaces.ifaddresses(interface_id) - return addrs[netifaces.AF_LINK][0]['addr'] - except (ValueError, IndexError, KeyError): - # No mac address found - return None + data = ethtoolPermAddr(cmd=ETHTOOL_GPERMADDR, size=MAX_ADDR_LEN) + ifr = ifreq(ifr_ifrn=interface_id.encode()) + ifr.ifr_data_ethtool_perm_addr = ctypes.pointer(data) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) + fcntl.ioctl(sock.fileno(), SIOCETHTOOL, ifr) + # if not full of zeros + if any(data.data[:data.size]): + # kernel updates size to actual address size during ioctl call + permaddr = [f'{b:02x}' for b in data.data[:data.size]] + return ':'.join(permaddr) + except OSError: + pass + LOG.warning("Failed to get permanent mac address for interface %s, " + "falling back to default mac address", + interface_id) + return get_default_ip_addr(socket.AF_PACKET, interface_id) # Other options... @@ -282,6 +319,24 @@ def interface_has_carrier(interface_name): return False +def get_interface_pci_address(interface_name): + path = '/sys/class/net/{}/device'.format(interface_name) + try: + return os.path.basename(os.readlink(path)) + except FileNotFoundError: + LOG.debug('No bus address found for interface %s', + interface_name) + + +def get_interface_driver(interface_name): + path = '/sys/class/net/{}/device/driver'.format(interface_name) + try: + return os.path.basename(os.readlink(path)) + except FileNotFoundError: + LOG.debug('No driver found for interface %s', + interface_name) + + def wrap_ipv6(ip): if netutils.is_valid_ipv6(ip): return "[%s]" % ip diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 469d9625..bd030acc 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -13,17 +13,18 @@ # limitations under the License. import binascii +from collections import namedtuple import json import logging import os import re import shutil +import socket import stat import time from unittest import mock from ironic_lib import utils as il_utils -import netifaces from oslo_concurrency import processutils from oslo_config import cfg from oslo_utils import units @@ -5968,32 +5969,36 @@ def fake_execute(cmd, *args, **kwargs): self.assertGreaterEqual(len(io_dict), len(expected)) +FakeAddr = namedtuple('FakeAddr', ('family', 'address')) + + +@mock.patch.object(netutils, 'get_mac_addr', autospec=True) @mock.patch.object(hardware.GenericHardwareManager, '_get_system_lshw_dict', autospec=True, return_value={'id': 'host'}) @mock.patch.object(hardware, 'get_managers', autospec=True, return_value=[hardware.GenericHardwareManager()]) -@mock.patch('netifaces.ifaddresses', autospec=True) +@mock.patch('psutil.net_if_addrs', autospec=True) @mock.patch('os.listdir', autospec=True) @mock.patch('os.path.exists', autospec=True) @mock.patch('builtins.open', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) -@mock.patch.object(netutils, 'get_mac_addr', autospec=True) @mock.patch.object(netutils, 'interface_has_carrier', autospec=True) class TestListNetworkInterfaces(base.IronicAgentTest): + def setUp(self): super().setUp() self.hardware = hardware.GenericHardwareManager() def test_list_network_interfaces(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): mocked_lshw.return_value = json.loads(hws.LSHW_JSON_OUTPUT_V2[0]) mocked_listdir.return_value = ['lo', 'eth0', 'foobar'] mocked_exists.side_effect = [False, False, True, True] @@ -6001,16 +6006,28 @@ def test_list_network_interfaces(self, 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_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'foobar': [ + FakeAddr(socket.AF_INET, '192.168.2.2'), + FakeAddr(socket.AF_INET6, 'fd00:1000::101') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + }.get(iface) mocked_execute.return_value = ('em0\n', '') mock_has_carrier.return_value = True - mock_get_mac.side_effect = [ - '00:0c:29:8c:11:b1', - None, - ] interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6024,26 +6041,37 @@ def test_list_network_interfaces(self, def test_list_network_interfaces_with_biosdevname(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): mocked_listdir.return_value = ['lo', 'eth0'] mocked_exists.side_effect = [False, False, 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_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + }.get(iface) mocked_execute.return_value = ('em0\n', '') - mock_get_mac.return_value = '00:0c:29:8c:11:b1' mock_has_carrier.return_value = True interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) @@ -6060,14 +6088,14 @@ def test_list_network_interfaces_with_biosdevname(self, def test_list_network_interfaces_with_lldp(self, mocked_lldp_info, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): CONF.set_override('collect_lldp', True) mocked_listdir.return_value = ['lo', 'eth0'] mocked_exists.side_effect = [False, False, True] @@ -6075,10 +6103,22 @@ def test_list_network_interfaces_with_lldp(self, 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_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + }.get(iface) mocked_lldp_info.return_value = {'eth0': [ (0, b''), (1, b'\x04\x88Z\x92\xecTY'), @@ -6086,7 +6126,6 @@ def test_list_network_interfaces_with_lldp(self, (3, b'\x00x')] } mock_has_carrier.return_value = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' mocked_execute.return_value = ('em0\n', '') interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) @@ -6105,10 +6144,17 @@ def test_list_network_interfaces_with_lldp(self, self.assertEqual('em0', interfaces[0].biosdevname) @mock.patch.object(netutils, 'get_lldp_info', autospec=True) - def test_list_network_interfaces_with_lldp_error( - self, mocked_lldp_info, mock_has_carrier, mock_get_mac, - mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, mockedget_managers, mocked_lshw): + def test_list_network_interfaces_with_lldp_error(self, + mocked_lldp_info, + mock_has_carrier, + mocked_execute, + mocked_open, + mocked_exists, + mocked_listdir, + mocked_net_if_addrs, + mockedget_managers, + mocked_lshw, + mocked_get_mac_addr): CONF.set_override('collect_lldp', True) mocked_listdir.return_value = ['lo', 'eth0'] mocked_exists.side_effect = [False, False, True] @@ -6116,14 +6162,25 @@ def test_list_network_interfaces_with_lldp_error( 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_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + }.get(iface) mocked_lldp_info.side_effect = Exception('Boom!') mocked_execute.return_value = ('em0\n', '') 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(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6136,14 +6193,14 @@ def test_list_network_interfaces_with_lldp_error( def test_list_network_interfaces_no_carrier(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): mockedget_managers.return_value = [hardware.GenericHardwareManager()] mocked_listdir.return_value = ['lo', 'eth0'] @@ -6152,13 +6209,24 @@ def test_list_network_interfaces_no_carrier(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = [OSError('boom')] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + }.get(iface) mocked_execute.return_value = ('em0\n', '') mock_has_carrier.return_value = False - mock_get_mac.return_value = '00:0c:29:8c:11:b1' interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6171,14 +6239,14 @@ def test_list_network_interfaces_no_carrier(self, def test_list_network_interfaces_with_vendor_info(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): mocked_listdir.return_value = ['lo', 'eth0'] mocked_exists.side_effect = [False, False, True] mocked_open.return_value.__enter__ = lambda s: s @@ -6186,13 +6254,24 @@ def test_list_network_interfaces_with_vendor_info(self, read_mock = mocked_open.return_value.read mac = '00:0c:29:8c:11:b1' read_mock.side_effect = ['0x15b3\n', '0x1014\n'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + }.get(iface) mocked_execute.return_value = ('em0\n', '') mock_has_carrier.return_value = True - mock_get_mac.return_value = mac interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6206,30 +6285,38 @@ def test_list_network_interfaces_with_vendor_info(self, def test_list_network_interfaces_with_bond(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): mocked_listdir.return_value = ['lo', 'bond0'] mocked_exists.side_effect = [False, False, 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_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'bond0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'bond0': '00:0c:29:8c:11:b1', + }.get(iface) mocked_execute.return_value = ('\n', '') mock_has_carrier.return_value = True - mock_get_mac.side_effect = [ - '00:0c:29:8c:11:b1', - None, - ] interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('bond0', interfaces[0].name) @@ -6240,16 +6327,69 @@ def test_list_network_interfaces_with_bond(self, self.assertTrue(interfaces[0].has_carrier) self.assertEqual('', interfaces[0].biosdevname) + @mock.patch.object(netutils, 'get_interface_driver', autospec=True) + @mock.patch.object(netutils, 'get_interface_pci_address', autospec=True) + def test_list_network_interfaces_with_pci_address(self, + mock_get_pci, + mock_get_driver, + mock_has_carrier, + mocked_execute, + mocked_open, + mocked_exists, + mocked_listdir, + mocked_net_if_addrs, + mockedget_managers, + mocked_lshw, + mocked_get_mac_addr): + mocked_listdir.return_value = ['lo', 'eth0'] + mocked_exists.side_effect = [False, False, 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_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] + } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + }.get(iface) + mocked_execute.return_value = ('em0\n', '') + mock_has_carrier.return_value = True + mock_get_pci.return_value = '0000:02:00.0' + mock_get_driver.return_value = 'e1000e' + interfaces = self.hardware.list_network_interfaces() + self.assertEqual(1, 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.assertTrue(interfaces[0].has_carrier) + self.assertEqual('em0', interfaces[0].biosdevname) + self.assertIsNone(interfaces[0].speed_mbps) + self.assertEqual('0000:02:00.0', interfaces[0].pci_address) + self.assertEqual('e1000e', interfaces[0].driver) + def test_list_network_vlan_interfaces(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): CONF.set_override('enable_vlan_interfaces', 'eth0.100') mocked_listdir.return_value = ['lo', 'eth0'] mocked_exists.side_effect = [False, False, True] @@ -6257,13 +6397,30 @@ def test_list_network_vlan_interfaces(self, 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_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'eth0.100': [ + FakeAddr(socket.AF_INET, '192.168.2.2'), + FakeAddr(socket.AF_INET6, 'fd00::1000::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + 'eth0.100': '00:0c:29:8c:11:b1', + }.get(iface) mocked_execute.return_value = ('em0\n', '') - mock_get_mac.mock_has_carrier = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' + mock_has_carrier.return_value = True interfaces = self.hardware.list_network_interfaces() self.assertEqual(2, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6279,14 +6436,14 @@ def test_list_network_vlan_interfaces(self, def test_list_network_vlan_interfaces_using_lldp(self, mocked_lldp_info, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): CONF.set_override('collect_lldp', True) CONF.set_override('enable_vlan_interfaces', 'eth0') mocked_listdir.return_value = ['lo', 'eth0'] @@ -6296,13 +6453,40 @@ def test_list_network_vlan_interfaces_using_lldp(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'eth0.100': [ + FakeAddr(socket.AF_INET, '192.168.100.2'), + FakeAddr(socket.AF_INET6, 'fd00:0100::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c1') + ], + 'eth0.101': [ + FakeAddr(socket.AF_INET, '192.168.101.2'), + FakeAddr(socket.AF_INET6, 'fd00:0101::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c2') + ] + } + mocked_get_mac_addr.side_effect = lambda iface: { + 'lo': '00:00:00:00:00:00', + 'eth0': '00:0c:29:8c:11:b1', + 'eth0.100': '00:0c:29:8c:11:c1', + 'eth0.101': '00:0c:29:8c:11:c2', + }.get(iface) 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) @@ -6314,24 +6498,24 @@ def test_list_network_vlan_interfaces_using_lldp(self, ] 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.assertEqual('00:0c:29:8c:11:c1', 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.assertEqual('00:0c:29:8c:11:c2', interfaces[2].mac_address) self.assertIsNone(interfaces[2].lldp) @mock.patch.object(netutils, 'LOG', autospec=True) def test_list_network_vlan_invalid_int(self, mocked_log, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): CONF.set_override('collect_lldp', True) CONF.set_override('enable_vlan_interfaces', 'enp0s1') mocked_listdir.return_value = ['lo', 'eth0'] @@ -6340,13 +6524,20 @@ def test_list_network_vlan_invalid_int(self, 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_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_execute.return_value = ('em0\n', '') - mock_get_mac.mock_has_carrier = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' + mock_has_carrier.return_value = True self.hardware.list_network_interfaces() mocked_log.warning.assert_called_once_with( @@ -6356,14 +6547,14 @@ def test_list_network_vlan_invalid_int(self, def test_list_network_vlan_interfaces_using_lldp_all(self, mocked_lldp_info, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, - mocked_lshw): + mocked_lshw, + mocked_get_mac_addr): CONF.set_override('collect_lldp', True) CONF.set_override('enable_vlan_interfaces', 'all') mocked_listdir.return_value = ['lo', 'eth0', 'eth1'] @@ -6373,6 +6564,43 @@ def test_list_network_vlan_interfaces_using_lldp_all(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'eth1': [ + FakeAddr(socket.AF_INET, '192.168.2.2'), + FakeAddr(socket.AF_INET6, 'fd00:1000::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b2') + ], + 'eth0.100': [ + FakeAddr(socket.AF_INET, '192.168.100.2'), + FakeAddr(socket.AF_INET6, 'fd00:0100::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c1') + ], + 'eth0.101': [ + FakeAddr(socket.AF_INET, '192.168.101.2'), + FakeAddr(socket.AF_INET6, 'fd00:0101::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c2') + ], + 'eth1.102': [ + FakeAddr(socket.AF_INET, '192.168.102.2'), + FakeAddr(socket.AF_INET6, 'fd00:1102::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:d1') + ], + 'eth1.103': [ + FakeAddr(socket.AF_INET, '192.168.103.2'), + FakeAddr(socket.AF_INET6, 'fd00:1103::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:d2') + ] + } mocked_lldp_info.return_value = {'eth0': [ (0, b''), (127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'), diff --git a/ironic_python_agent/tests/unit/test_netutils.py b/ironic_python_agent/tests/unit/test_netutils.py index 21bf3a4a..255cbecb 100644 --- a/ironic_python_agent/tests/unit/test_netutils.py +++ b/ironic_python_agent/tests/unit/test_netutils.py @@ -394,3 +394,27 @@ def test_wrap_ipv6(self): def test_wrap_ipv6_with_ipv4(self): res = netutils.wrap_ipv6('1.2.3.4') self.assertEqual('1.2.3.4', res) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_pci_address(self, mock_read): + mock_read.return_value = '../../../0000:02:00.0' + addr = netutils.get_interface_pci_address('ens160') + self.assertEqual('0000:02:00.0', addr) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_pci_address_notfound(self, mock_read): + mock_read.side_effect = FileNotFoundError + addr = netutils.get_interface_pci_address('ens160') + self.assertIsNone(addr) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_driver(self, mock_read): + mock_read.return_value = '../../../../bus/pci/drivers/e1000e' + addr = netutils.get_interface_driver('ens160') + self.assertEqual('e1000e', addr) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_driver_notfound(self, mock_read): + mock_read.side_effect = FileNotFoundError + driver = netutils.get_interface_driver('ens160') + self.assertIsNone(driver) diff --git a/releasenotes/notes/fix-mac-permaddr-0bc7d688eee4b814.yaml b/releasenotes/notes/fix-mac-permaddr-0bc7d688eee4b814.yaml new file mode 100644 index 00000000..7edb156f --- /dev/null +++ b/releasenotes/notes/fix-mac-permaddr-0bc7d688eee4b814.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixes IPA collecting the effective MAC address of NICs instead of the + pesistent MAC address. In case it fails to fetch the persistent address + falls back to effective MAC address. + See https://bugs.launchpad.net/ironic-python-agent/+bug/2103450 for + details. + diff --git a/releasenotes/notes/report-intf-bus-and-driver-63ed0277b372c1d1.yaml b/releasenotes/notes/report-intf-bus-and-driver-63ed0277b372c1d1.yaml new file mode 100644 index 00000000..ead108a5 --- /dev/null +++ b/releasenotes/notes/report-intf-bus-and-driver-63ed0277b372c1d1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support to collect pci address and driver information for interfaces. diff --git a/requirements.txt b/requirements.txt index e7481c3e..b943d1f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ pbr>=2.0.0 # Apache-2.0 eventlet>=0.18.2 # MIT -netifaces>=0.10.4 # MIT oslo.config>=5.2.0 # Apache-2.0 oslo.concurrency>=3.26.0 # Apache-2.0 oslo.log>=4.6.1 # Apache-2.0