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 <nicolas.belouin@suse.com>
(cherry picked from commit 48422a532f)
This commit is contained in:
Nicolas Belouin
2025-04-07 15:36:14 +02:00
parent 0ee9ca4518
commit 797c3f3e24
3 changed files with 118 additions and 14 deletions

View File

@@ -34,6 +34,13 @@ LLDP_ETHERTYPE = 0x88cc
IFF_PROMISC = 0x100 IFF_PROMISC = 0x100
SIOCGIFFLAGS = 0x8913 SIOCGIFFLAGS = 0x8913
SIOCSIFFLAGS = 0x8914 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 INFINIBAND_ADDR_LEN = 59
# LLDP definitions needed to extract vlan information # 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) 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 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), _fields_ = [("ifr_ifrn", ctypes.c_char * 16),
("ifr_flags", ctypes.c_short)] ("ifr_data", ifreq_data)]
class RawPromiscuousSockets(object): class RawPromiscuousSockets(object):
@@ -236,6 +258,23 @@ def get_ipv6_addr(interface_id):
def get_mac_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) return get_default_ip_addr(socket.AF_PACKET, interface_id)

View File

@@ -6279,6 +6279,7 @@ class TestCollectSystemLogs(base.IronicAgentTest):
FakeAddr = namedtuple('FakeAddr', ('family', 'address')) FakeAddr = namedtuple('FakeAddr', ('family', 'address'))
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
@mock.patch.object(hardware.GenericHardwareManager, '_get_system_lshw_dict', @mock.patch.object(hardware.GenericHardwareManager, '_get_system_lshw_dict',
autospec=True, return_value={'id': 'host'}) autospec=True, return_value={'id': 'host'})
@mock.patch.object(hardware, 'get_managers', autospec=True, @mock.patch.object(hardware, 'get_managers', autospec=True,
@@ -6303,7 +6304,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
mocked_lshw.return_value = json.loads(hws.LSHW_JSON_OUTPUT_V2[0]) mocked_lshw.return_value = json.loads(hws.LSHW_JSON_OUTPUT_V2[0])
mocked_listdir.return_value = ['lo', 'eth0', 'foobar'] mocked_listdir.return_value = ['lo', 'eth0', 'foobar']
mocked_exists.side_effect = [False, False, True, True] mocked_exists.side_effect = [False, False, True, True]
@@ -6327,6 +6329,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
FakeAddr(socket.AF_INET6, 'fd00:1000::101') 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', '') mocked_execute.return_value = ('em0\n', '')
mock_has_carrier.return_value = True mock_has_carrier.return_value = True
interfaces = self.hardware.list_network_interfaces() interfaces = self.hardware.list_network_interfaces()
@@ -6348,7 +6354,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, False, True] mocked_exists.side_effect = [False, False, True]
mocked_open.return_value.__enter__ = lambda s: s 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') 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', '') mocked_execute.return_value = ('em0\n', '')
mock_has_carrier.return_value = True mock_has_carrier.return_value = True
interfaces = self.hardware.list_network_interfaces() interfaces = self.hardware.list_network_interfaces()
@@ -6390,7 +6401,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
CONF.set_override('collect_lldp', True) CONF.set_override('collect_lldp', True)
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, False, True] 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') 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': [ mocked_lldp_info.return_value = {'eth0': [
(0, b''), (0, b''),
(1, b'\x04\x88Z\x92\xecTY'), (1, b'\x04\x88Z\x92\xecTY'),
@@ -6444,7 +6460,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
CONF.set_override('collect_lldp', True) CONF.set_override('collect_lldp', True)
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, False, True] 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') 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_lldp_info.side_effect = Exception('Boom!')
mocked_execute.return_value = ('em0\n', '') mocked_execute.return_value = ('em0\n', '')
mock_has_carrier.return_value = True mock_has_carrier.return_value = True
@@ -6485,7 +6506,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
mockedget_managers.return_value = [hardware.GenericHardwareManager()] mockedget_managers.return_value = [hardware.GenericHardwareManager()]
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
@@ -6506,6 +6528,10 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') 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', '') mocked_execute.return_value = ('em0\n', '')
mock_has_carrier.return_value = False mock_has_carrier.return_value = False
interfaces = self.hardware.list_network_interfaces() interfaces = self.hardware.list_network_interfaces()
@@ -6526,7 +6552,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, False, True] mocked_exists.side_effect = [False, False, True]
mocked_open.return_value.__enter__ = lambda s: s 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') 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', '') mocked_execute.return_value = ('em0\n', '')
mock_has_carrier.return_value = True mock_has_carrier.return_value = True
interfaces = self.hardware.list_network_interfaces() interfaces = self.hardware.list_network_interfaces()
@@ -6567,7 +6598,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
mocked_listdir.return_value = ['lo', 'bond0'] mocked_listdir.return_value = ['lo', 'bond0']
mocked_exists.side_effect = [False, False, True] mocked_exists.side_effect = [False, False, True]
mocked_open.return_value.__enter__ = lambda s: s 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') 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', '') mocked_execute.return_value = ('\n', '')
mock_has_carrier.return_value = True mock_has_carrier.return_value = True
interfaces = self.hardware.list_network_interfaces() interfaces = self.hardware.list_network_interfaces()
@@ -6610,7 +6646,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, False, True] mocked_exists.side_effect = [False, False, True]
mocked_open.return_value.__enter__ = lambda s: s 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') 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', '') mocked_execute.return_value = ('em0\n', '')
mock_has_carrier.return_value = True mock_has_carrier.return_value = True
mock_get_pci.return_value = '0000:02:00.0' mock_get_pci.return_value = '0000:02:00.0'
@@ -6654,7 +6695,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
CONF.set_override('enable_vlan_interfaces', 'eth0.100') CONF.set_override('enable_vlan_interfaces', 'eth0.100')
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
mocked_exists.side_effect = [False, False, True] 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') 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', '') mocked_execute.return_value = ('em0\n', '')
mock_has_carrier.return_value = True mock_has_carrier.return_value = True
interfaces = self.hardware.list_network_interfaces() interfaces = self.hardware.list_network_interfaces()
@@ -6702,7 +6749,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
CONF.set_override('collect_lldp', True) CONF.set_override('collect_lldp', True)
CONF.set_override('enable_vlan_interfaces', 'eth0') CONF.set_override('enable_vlan_interfaces', 'eth0')
mocked_listdir.return_value = ['lo', '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') 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': [ mocked_lldp_info.return_value = {'eth0': [
(0, b''), (0, b''),
(127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'), (127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'),
@@ -6767,7 +6821,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
CONF.set_override('collect_lldp', True) CONF.set_override('collect_lldp', True)
CONF.set_override('enable_vlan_interfaces', 'enp0s1') CONF.set_override('enable_vlan_interfaces', 'enp0s1')
mocked_listdir.return_value = ['lo', 'eth0'] mocked_listdir.return_value = ['lo', 'eth0']
@@ -6805,7 +6860,8 @@ class TestListNetworkInterfaces(base.IronicAgentTest):
mocked_listdir, mocked_listdir,
mocked_net_if_addrs, mocked_net_if_addrs,
mockedget_managers, mockedget_managers,
mocked_lshw): mocked_lshw,
mocked_get_mac_addr):
CONF.set_override('collect_lldp', True) CONF.set_override('collect_lldp', True)
CONF.set_override('enable_vlan_interfaces', 'all') CONF.set_override('enable_vlan_interfaces', 'all')
mocked_listdir.return_value = ['lo', 'eth0', 'eth1'] mocked_listdir.return_value = ['lo', 'eth0', 'eth1']

View File

@@ -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.