From 797c3f3e24744ba0669b396b4e2fc5d9e9bbf123 Mon Sep 17 00:00:00 2001 From: Nicolas Belouin Date: Mon, 7 Apr 2025 15:36:14 +0200 Subject: [PATCH] netutils: Use ethtool ioctl to get permanent mac address Fetching the permanent MAC address of the interface instead of the default one allows to get the right one in case it got changed during setup (likely with a bonding setup). In order to fetch the permanent MAC address of a given interface, one can either use Netlink (either rtnetlink or ethtool), or use ethtool ioctl. The use of ioctl feels simpler and requires no additional dependency. The implementation falls back to older behavior should an error occur. Closes-Bug: #2103450 Change-Id: I54151990e396ddcf775128ca24d3db08e45c256d Signed-off-by: Nicolas Belouin (cherry picked from commit 48422a532fe20e6f4fe93784a5a948f773fb5701) --- ironic_python_agent/netutils.py | 43 +++++++++- .../tests/unit/test_hardware.py | 80 ++++++++++++++++--- .../fix-mac-permaddr-0bc7d688eee4b814.yaml | 9 +++ 3 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/fix-mac-permaddr-0bc7d688eee4b814.yaml diff --git a/ironic_python_agent/netutils.py b/ironic_python_agent/netutils.py index 8f401855b..9eca69336 100644 --- a/ironic_python_agent/netutils.py +++ b/ironic_python_agent/netutils.py @@ -34,6 +34,13 @@ LLDP_ETHERTYPE = 0x88cc 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 @@ dot1_VLAN_NAME = "03" 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): @@ -236,6 +258,23 @@ def get_ipv6_addr(interface_id): def get_mac_addr(interface_id): + """Retrieve permanent mac address, if unable to fallback to default one""" + try: + 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) diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index db76de250..6cb38bf6a 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -6279,6 +6279,7 @@ class TestCollectSystemLogs(base.IronicAgentTest): 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, @@ -6303,7 +6304,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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] @@ -6327,6 +6329,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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 interfaces = self.hardware.list_network_interfaces() @@ -6348,7 +6354,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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 @@ -6367,6 +6374,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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 interfaces = self.hardware.list_network_interfaces() @@ -6390,7 +6401,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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] @@ -6410,6 +6422,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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'), @@ -6444,7 +6460,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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] @@ -6464,6 +6481,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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 @@ -6485,7 +6506,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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'] @@ -6506,6 +6528,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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 interfaces = self.hardware.list_network_interfaces() @@ -6526,7 +6552,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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 @@ -6546,6 +6573,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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 interfaces = self.hardware.list_network_interfaces() @@ -6567,7 +6598,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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 @@ -6586,6 +6618,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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 interfaces = self.hardware.list_network_interfaces() @@ -6610,7 +6646,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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 @@ -6629,6 +6666,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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' @@ -6654,7 +6695,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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] @@ -6679,6 +6721,11 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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_has_carrier.return_value = True interfaces = self.hardware.list_network_interfaces() @@ -6702,7 +6749,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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'] @@ -6734,6 +6782,12 @@ class TestListNetworkInterfaces(base.IronicAgentTest): 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'), @@ -6767,7 +6821,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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'] @@ -6805,7 +6860,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest): mocked_listdir, 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'] diff --git a/releasenotes/notes/fix-mac-permaddr-0bc7d688eee4b814.yaml b/releasenotes/notes/fix-mac-permaddr-0bc7d688eee4b814.yaml new file mode 100644 index 000000000..7edb156f0 --- /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. +