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>
This commit is contained in:
Nicolas Belouin
2025-04-07 15:36:14 +02:00
parent 7efe3dfc04
commit 48422a532f
3 changed files with 118 additions and 14 deletions

View File

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

View File

@@ -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']

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.