Skip to content

Commit 660c770

Browse files
diconico07MahnoorAsghar
authored andcommitted
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 <[email protected]> (cherry picked from commit 48422a5)
1 parent f7eb261 commit 660c770

File tree

3 files changed

+118
-15
lines changed

3 files changed

+118
-15
lines changed

ironic_python_agent/netutils.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
IFF_PROMISC = 0x100
3535
SIOCGIFFLAGS = 0x8913
3636
SIOCSIFFLAGS = 0x8914
37+
# SIOCETHTOOL from linux/sockios.h
38+
SIOCETHTOOL = 0x8946
39+
# ETHTOOL_GPERMADDR from linux/ethtool.h
40+
ETHTOOL_GPERMADDR = 0x00000020
41+
# MAX_ADDR_LEN from linux/netdevice.h
42+
MAX_ADDR_LEN = 32
43+
3744
INFINIBAND_ADDR_LEN = 59
3845

3946
# LLDP definitions needed to extract vlan information
@@ -45,10 +52,25 @@
4552
VLAN_ID_LEN = len(LLDP_802dot1_OUI + dot1_VLAN_NAME)
4653

4754

55+
class ethtoolPermAddr(ctypes.Structure):
56+
"""Class for getting interface permanent MAC address"""
57+
_fields_ = [("cmd", ctypes.c_uint32),
58+
("size", ctypes.c_uint32),
59+
("data", ctypes.c_uint8 * MAX_ADDR_LEN)]
60+
61+
62+
class ifreq_data(ctypes.Union):
63+
_fields_ = [("ifr_flags", ctypes.c_short),
64+
(
65+
"ifr_data_ethtool_perm_addr",
66+
ctypes.POINTER(ethtoolPermAddr))]
67+
68+
4869
class ifreq(ctypes.Structure):
49-
"""Class for setting flags on a socket."""
70+
"""Class for ioctl on socket."""
71+
_anonymous_ = ("ifr_data",)
5072
_fields_ = [("ifr_ifrn", ctypes.c_char * 16),
51-
("ifr_flags", ctypes.c_short)]
73+
("ifr_data", ifreq_data)]
5274

5375

5476
class RawPromiscuousSockets(object):
@@ -236,6 +258,23 @@ def get_ipv6_addr(interface_id):
236258

237259

238260
def get_mac_addr(interface_id):
261+
"""Retrieve permanent mac address, if unable to fallback to default one"""
262+
try:
263+
data = ethtoolPermAddr(cmd=ETHTOOL_GPERMADDR, size=MAX_ADDR_LEN)
264+
ifr = ifreq(ifr_ifrn=interface_id.encode())
265+
ifr.ifr_data_ethtool_perm_addr = ctypes.pointer(data)
266+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
267+
fcntl.ioctl(sock.fileno(), SIOCETHTOOL, ifr)
268+
# if not full of zeros
269+
if any(data.data[:data.size]):
270+
# kernel updates size to actual address size during ioctl call
271+
permaddr = [f'{b:02x}' for b in data.data[:data.size]]
272+
return ':'.join(permaddr)
273+
except OSError:
274+
pass
275+
LOG.warning("Failed to get permanent mac address for interface %s, "
276+
"falling back to default mac address",
277+
interface_id)
239278
return get_default_ip_addr(socket.AF_PACKET, interface_id)
240279

241280

ironic_python_agent/tests/unit/test_hardware.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import binascii
1616
from collections import namedtuple
17-
import glob
1817
import json
1918
import os
2019
import re
@@ -5900,6 +5899,7 @@ def fake_execute(cmd, *args, **kwargs):
59005899
FakeAddr = namedtuple('FakeAddr', ('family', 'address'))
59015900

59025901

5902+
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
59035903
@mock.patch.object(hardware.GenericHardwareManager, '_get_system_lshw_dict',
59045904
autospec=True, return_value={'id': 'host'})
59055905
@mock.patch.object(hardware, 'get_managers', autospec=True,
@@ -5924,7 +5924,8 @@ def test_list_network_interfaces(self,
59245924
mocked_listdir,
59255925
mocked_net_if_addrs,
59265926
mockedget_managers,
5927-
mocked_lshw):
5927+
mocked_lshw,
5928+
mocked_get_mac_addr):
59285929
mocked_lshw.return_value = json.loads(hws.LSHW_JSON_OUTPUT_V2[0])
59295930
mocked_listdir.return_value = ['lo', 'eth0', 'foobar']
59305931
mocked_exists.side_effect = [False, False, True, True]
@@ -5948,6 +5949,10 @@ def test_list_network_interfaces(self,
59485949
FakeAddr(socket.AF_INET6, 'fd00:1000::101')
59495950
]
59505951
}
5952+
mocked_get_mac_addr.side_effect = lambda iface: {
5953+
'lo': '00:00:00:00:00:00',
5954+
'eth0': '00:0c:29:8c:11:b1',
5955+
}.get(iface)
59515956
mocked_execute.return_value = ('em0\n', '')
59525957
mock_has_carrier.return_value = True
59535958
interfaces = self.hardware.list_network_interfaces()
@@ -5969,7 +5974,8 @@ def test_list_network_interfaces_with_biosdevname(self,
59695974
mocked_listdir,
59705975
mocked_net_if_addrs,
59715976
mockedget_managers,
5972-
mocked_lshw):
5977+
mocked_lshw,
5978+
mocked_get_mac_addr):
59735979
mocked_listdir.return_value = ['lo', 'eth0']
59745980
mocked_exists.side_effect = [False, False, True]
59755981
mocked_open.return_value.__enter__ = lambda s: s
@@ -5988,6 +5994,10 @@ def test_list_network_interfaces_with_biosdevname(self,
59885994
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
59895995
]
59905996
}
5997+
mocked_get_mac_addr.side_effect = lambda iface: {
5998+
'lo': '00:00:00:00:00:00',
5999+
'eth0': '00:0c:29:8c:11:b1',
6000+
}.get(iface)
59916001
mocked_execute.return_value = ('em0\n', '')
59926002
mock_has_carrier.return_value = True
59936003
interfaces = self.hardware.list_network_interfaces()
@@ -6011,7 +6021,8 @@ def test_list_network_interfaces_with_lldp(self,
60116021
mocked_listdir,
60126022
mocked_net_if_addrs,
60136023
mockedget_managers,
6014-
mocked_lshw):
6024+
mocked_lshw,
6025+
mocked_get_mac_addr):
60156026
CONF.set_override('collect_lldp', True)
60166027
mocked_listdir.return_value = ['lo', 'eth0']
60176028
mocked_exists.side_effect = [False, False, True]
@@ -6031,6 +6042,10 @@ def test_list_network_interfaces_with_lldp(self,
60316042
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
60326043
]
60336044
}
6045+
mocked_get_mac_addr.side_effect = lambda iface: {
6046+
'lo': '00:00:00:00:00:00',
6047+
'eth0': '00:0c:29:8c:11:b1',
6048+
}.get(iface)
60346049
mocked_lldp_info.return_value = {'eth0': [
60356050
(0, b''),
60366051
(1, b'\x04\x88Z\x92\xecTY'),
@@ -6065,7 +6080,8 @@ def test_list_network_interfaces_with_lldp_error(self,
60656080
mocked_listdir,
60666081
mocked_net_if_addrs,
60676082
mockedget_managers,
6068-
mocked_lshw):
6083+
mocked_lshw,
6084+
mocked_get_mac_addr):
60696085
CONF.set_override('collect_lldp', True)
60706086
mocked_listdir.return_value = ['lo', 'eth0']
60716087
mocked_exists.side_effect = [False, False, True]
@@ -6085,6 +6101,10 @@ def test_list_network_interfaces_with_lldp_error(self,
60856101
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
60866102
]
60876103
}
6104+
mocked_get_mac_addr.side_effect = lambda iface: {
6105+
'lo': '00:00:00:00:00:00',
6106+
'eth0': '00:0c:29:8c:11:b1',
6107+
}.get(iface)
60886108
mocked_lldp_info.side_effect = Exception('Boom!')
60896109
mocked_execute.return_value = ('em0\n', '')
60906110
mock_has_carrier.return_value = True
@@ -6106,7 +6126,8 @@ def test_list_network_interfaces_no_carrier(self,
61066126
mocked_listdir,
61076127
mocked_net_if_addrs,
61086128
mockedget_managers,
6109-
mocked_lshw):
6129+
mocked_lshw,
6130+
mocked_get_mac_addr):
61106131

61116132
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
61126133
mocked_listdir.return_value = ['lo', 'eth0']
@@ -6127,6 +6148,10 @@ def test_list_network_interfaces_no_carrier(self,
61276148
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
61286149
]
61296150
}
6151+
mocked_get_mac_addr.side_effect = lambda iface: {
6152+
'lo': '00:00:00:00:00:00',
6153+
'eth0': '00:0c:29:8c:11:b1',
6154+
}.get(iface)
61306155
mocked_execute.return_value = ('em0\n', '')
61316156
mock_has_carrier.return_value = False
61326157
interfaces = self.hardware.list_network_interfaces()
@@ -6147,7 +6172,8 @@ def test_list_network_interfaces_with_vendor_info(self,
61476172
mocked_listdir,
61486173
mocked_net_if_addrs,
61496174
mockedget_managers,
6150-
mocked_lshw):
6175+
mocked_lshw,
6176+
mocked_get_mac_addr):
61516177
mocked_listdir.return_value = ['lo', 'eth0']
61526178
mocked_exists.side_effect = [False, False, True]
61536179
mocked_open.return_value.__enter__ = lambda s: s
@@ -6167,6 +6193,10 @@ def test_list_network_interfaces_with_vendor_info(self,
61676193
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
61686194
]
61696195
}
6196+
mocked_get_mac_addr.side_effect = lambda iface: {
6197+
'lo': '00:00:00:00:00:00',
6198+
'eth0': '00:0c:29:8c:11:b1',
6199+
}.get(iface)
61706200
mocked_execute.return_value = ('em0\n', '')
61716201
mock_has_carrier.return_value = True
61726202
interfaces = self.hardware.list_network_interfaces()
@@ -6188,7 +6218,8 @@ def test_list_network_interfaces_with_bond(self,
61886218
mocked_listdir,
61896219
mocked_net_if_addrs,
61906220
mockedget_managers,
6191-
mocked_lshw):
6221+
mocked_lshw,
6222+
mocked_get_mac_addr):
61926223
mocked_listdir.return_value = ['lo', 'bond0']
61936224
mocked_exists.side_effect = [False, False, True]
61946225
mocked_open.return_value.__enter__ = lambda s: s
@@ -6207,6 +6238,10 @@ def test_list_network_interfaces_with_bond(self,
62076238
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
62086239
]
62096240
}
6241+
mocked_get_mac_addr.side_effect = lambda iface: {
6242+
'lo': '00:00:00:00:00:00',
6243+
'bond0': '00:0c:29:8c:11:b1',
6244+
}.get(iface)
62106245
mocked_execute.return_value = ('\n', '')
62116246
mock_has_carrier.return_value = True
62126247
interfaces = self.hardware.list_network_interfaces()
@@ -6231,7 +6266,8 @@ def test_list_network_interfaces_with_pci_address(self,
62316266
mocked_listdir,
62326267
mocked_net_if_addrs,
62336268
mockedget_managers,
6234-
mocked_lshw):
6269+
mocked_lshw,
6270+
mocked_get_mac_addr):
62356271
mocked_listdir.return_value = ['lo', 'eth0']
62366272
mocked_exists.side_effect = [False, False, True]
62376273
mocked_open.return_value.__enter__ = lambda s: s
@@ -6250,6 +6286,10 @@ def test_list_network_interfaces_with_pci_address(self,
62506286
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
62516287
]
62526288
}
6289+
mocked_get_mac_addr.side_effect = lambda iface: {
6290+
'lo': '00:00:00:00:00:00',
6291+
'eth0': '00:0c:29:8c:11:b1',
6292+
}.get(iface)
62536293
mocked_execute.return_value = ('em0\n', '')
62546294
mock_has_carrier.return_value = True
62556295
mock_get_pci.return_value = '0000:02:00.0'
@@ -6275,7 +6315,8 @@ def test_list_network_vlan_interfaces(self,
62756315
mocked_listdir,
62766316
mocked_net_if_addrs,
62776317
mockedget_managers,
6278-
mocked_lshw):
6318+
mocked_lshw,
6319+
mocked_get_mac_addr):
62796320
CONF.set_override('enable_vlan_interfaces', 'eth0.100')
62806321
mocked_listdir.return_value = ['lo', 'eth0']
62816322
mocked_exists.side_effect = [False, False, True]
@@ -6300,6 +6341,11 @@ def test_list_network_vlan_interfaces(self,
63006341
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
63016342
]
63026343
}
6344+
mocked_get_mac_addr.side_effect = lambda iface: {
6345+
'lo': '00:00:00:00:00:00',
6346+
'eth0': '00:0c:29:8c:11:b1',
6347+
'eth0.100': '00:0c:29:8c:11:b1',
6348+
}.get(iface)
63036349
mocked_execute.return_value = ('em0\n', '')
63046350
mock_has_carrier.return_value = True
63056351
interfaces = self.hardware.list_network_interfaces()
@@ -6323,7 +6369,8 @@ def test_list_network_vlan_interfaces_using_lldp(self,
63236369
mocked_listdir,
63246370
mocked_net_if_addrs,
63256371
mockedget_managers,
6326-
mocked_lshw):
6372+
mocked_lshw,
6373+
mocked_get_mac_addr):
63276374
CONF.set_override('collect_lldp', True)
63286375
CONF.set_override('enable_vlan_interfaces', 'eth0')
63296376
mocked_listdir.return_value = ['lo', 'eth0']
@@ -6355,6 +6402,12 @@ def test_list_network_vlan_interfaces_using_lldp(self,
63556402
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c2')
63566403
]
63576404
}
6405+
mocked_get_mac_addr.side_effect = lambda iface: {
6406+
'lo': '00:00:00:00:00:00',
6407+
'eth0': '00:0c:29:8c:11:b1',
6408+
'eth0.100': '00:0c:29:8c:11:c1',
6409+
'eth0.101': '00:0c:29:8c:11:c2',
6410+
}.get(iface)
63586411
mocked_lldp_info.return_value = {'eth0': [
63596412
(0, b''),
63606413
(127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'),
@@ -6388,7 +6441,8 @@ def test_list_network_vlan_invalid_int(self,
63886441
mocked_listdir,
63896442
mocked_net_if_addrs,
63906443
mockedget_managers,
6391-
mocked_lshw):
6444+
mocked_lshw,
6445+
mocked_get_mac_addr):
63926446
CONF.set_override('collect_lldp', True)
63936447
CONF.set_override('enable_vlan_interfaces', 'enp0s1')
63946448
mocked_listdir.return_value = ['lo', 'eth0']
@@ -6426,7 +6480,8 @@ def test_list_network_vlan_interfaces_using_lldp_all(self,
64266480
mocked_listdir,
64276481
mocked_net_if_addrs,
64286482
mockedget_managers,
6429-
mocked_lshw):
6483+
mocked_lshw,
6484+
mocked_get_mac_addr):
64306485
CONF.set_override('collect_lldp', True)
64316486
CONF.set_override('enable_vlan_interfaces', 'all')
64326487
mocked_listdir.return_value = ['lo', 'eth0', 'eth1']
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
Fixes IPA collecting the effective MAC address of NICs instead of the
5+
pesistent MAC address. In case it fails to fetch the persistent address
6+
falls back to effective MAC address.
7+
See https://bugs.launchpad.net/ironic-python-agent/+bug/2103450 for
8+
details.
9+

0 commit comments

Comments
 (0)