Skip to content

Commit fb1fd9a

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 fb1fd9a

File tree

3 files changed

+118
-14
lines changed

3 files changed

+118
-14
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 & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5900,6 +5900,7 @@ def fake_execute(cmd, *args, **kwargs):
59005900
FakeAddr = namedtuple('FakeAddr', ('family', 'address'))
59015901

59025902

5903+
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
59035904
@mock.patch.object(hardware.GenericHardwareManager, '_get_system_lshw_dict',
59045905
autospec=True, return_value={'id': 'host'})
59055906
@mock.patch.object(hardware, 'get_managers', autospec=True,
@@ -5924,7 +5925,8 @@ def test_list_network_interfaces(self,
59245925
mocked_listdir,
59255926
mocked_net_if_addrs,
59265927
mockedget_managers,
5927-
mocked_lshw):
5928+
mocked_lshw,
5929+
mocked_get_mac_addr):
59285930
mocked_lshw.return_value = json.loads(hws.LSHW_JSON_OUTPUT_V2[0])
59295931
mocked_listdir.return_value = ['lo', 'eth0', 'foobar']
59305932
mocked_exists.side_effect = [False, False, True, True]
@@ -5948,6 +5950,10 @@ def test_list_network_interfaces(self,
59485950
FakeAddr(socket.AF_INET6, 'fd00:1000::101')
59495951
]
59505952
}
5953+
mocked_get_mac_addr.side_effect = lambda iface: {
5954+
'lo': '00:00:00:00:00:00',
5955+
'eth0': '00:0c:29:8c:11:b1',
5956+
}.get(iface)
59515957
mocked_execute.return_value = ('em0\n', '')
59525958
mock_has_carrier.return_value = True
59535959
interfaces = self.hardware.list_network_interfaces()
@@ -5969,7 +5975,8 @@ def test_list_network_interfaces_with_biosdevname(self,
59695975
mocked_listdir,
59705976
mocked_net_if_addrs,
59715977
mockedget_managers,
5972-
mocked_lshw):
5978+
mocked_lshw,
5979+
mocked_get_mac_addr):
59735980
mocked_listdir.return_value = ['lo', 'eth0']
59745981
mocked_exists.side_effect = [False, False, True]
59755982
mocked_open.return_value.__enter__ = lambda s: s
@@ -5988,6 +5995,10 @@ def test_list_network_interfaces_with_biosdevname(self,
59885995
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
59895996
]
59905997
}
5998+
mocked_get_mac_addr.side_effect = lambda iface: {
5999+
'lo': '00:00:00:00:00:00',
6000+
'eth0': '00:0c:29:8c:11:b1',
6001+
}.get(iface)
59916002
mocked_execute.return_value = ('em0\n', '')
59926003
mock_has_carrier.return_value = True
59936004
interfaces = self.hardware.list_network_interfaces()
@@ -6011,7 +6022,8 @@ def test_list_network_interfaces_with_lldp(self,
60116022
mocked_listdir,
60126023
mocked_net_if_addrs,
60136024
mockedget_managers,
6014-
mocked_lshw):
6025+
mocked_lshw,
6026+
mocked_get_mac_addr):
60156027
CONF.set_override('collect_lldp', True)
60166028
mocked_listdir.return_value = ['lo', 'eth0']
60176029
mocked_exists.side_effect = [False, False, True]
@@ -6031,6 +6043,10 @@ def test_list_network_interfaces_with_lldp(self,
60316043
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
60326044
]
60336045
}
6046+
mocked_get_mac_addr.side_effect = lambda iface: {
6047+
'lo': '00:00:00:00:00:00',
6048+
'eth0': '00:0c:29:8c:11:b1',
6049+
}.get(iface)
60346050
mocked_lldp_info.return_value = {'eth0': [
60356051
(0, b''),
60366052
(1, b'\x04\x88Z\x92\xecTY'),
@@ -6065,7 +6081,8 @@ def test_list_network_interfaces_with_lldp_error(self,
60656081
mocked_listdir,
60666082
mocked_net_if_addrs,
60676083
mockedget_managers,
6068-
mocked_lshw):
6084+
mocked_lshw,
6085+
mocked_get_mac_addr):
60696086
CONF.set_override('collect_lldp', True)
60706087
mocked_listdir.return_value = ['lo', 'eth0']
60716088
mocked_exists.side_effect = [False, False, True]
@@ -6085,6 +6102,10 @@ def test_list_network_interfaces_with_lldp_error(self,
60856102
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
60866103
]
60876104
}
6105+
mocked_get_mac_addr.side_effect = lambda iface: {
6106+
'lo': '00:00:00:00:00:00',
6107+
'eth0': '00:0c:29:8c:11:b1',
6108+
}.get(iface)
60886109
mocked_lldp_info.side_effect = Exception('Boom!')
60896110
mocked_execute.return_value = ('em0\n', '')
60906111
mock_has_carrier.return_value = True
@@ -6106,7 +6127,8 @@ def test_list_network_interfaces_no_carrier(self,
61066127
mocked_listdir,
61076128
mocked_net_if_addrs,
61086129
mockedget_managers,
6109-
mocked_lshw):
6130+
mocked_lshw,
6131+
mocked_get_mac_addr):
61106132

61116133
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
61126134
mocked_listdir.return_value = ['lo', 'eth0']
@@ -6127,6 +6149,10 @@ def test_list_network_interfaces_no_carrier(self,
61276149
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
61286150
]
61296151
}
6152+
mocked_get_mac_addr.side_effect = lambda iface: {
6153+
'lo': '00:00:00:00:00:00',
6154+
'eth0': '00:0c:29:8c:11:b1',
6155+
}.get(iface)
61306156
mocked_execute.return_value = ('em0\n', '')
61316157
mock_has_carrier.return_value = False
61326158
interfaces = self.hardware.list_network_interfaces()
@@ -6147,7 +6173,8 @@ def test_list_network_interfaces_with_vendor_info(self,
61476173
mocked_listdir,
61486174
mocked_net_if_addrs,
61496175
mockedget_managers,
6150-
mocked_lshw):
6176+
mocked_lshw,
6177+
mocked_get_mac_addr):
61516178
mocked_listdir.return_value = ['lo', 'eth0']
61526179
mocked_exists.side_effect = [False, False, True]
61536180
mocked_open.return_value.__enter__ = lambda s: s
@@ -6167,6 +6194,10 @@ def test_list_network_interfaces_with_vendor_info(self,
61676194
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
61686195
]
61696196
}
6197+
mocked_get_mac_addr.side_effect = lambda iface: {
6198+
'lo': '00:00:00:00:00:00',
6199+
'eth0': '00:0c:29:8c:11:b1',
6200+
}.get(iface)
61706201
mocked_execute.return_value = ('em0\n', '')
61716202
mock_has_carrier.return_value = True
61726203
interfaces = self.hardware.list_network_interfaces()
@@ -6188,7 +6219,8 @@ def test_list_network_interfaces_with_bond(self,
61886219
mocked_listdir,
61896220
mocked_net_if_addrs,
61906221
mockedget_managers,
6191-
mocked_lshw):
6222+
mocked_lshw,
6223+
mocked_get_mac_addr):
61926224
mocked_listdir.return_value = ['lo', 'bond0']
61936225
mocked_exists.side_effect = [False, False, True]
61946226
mocked_open.return_value.__enter__ = lambda s: s
@@ -6207,6 +6239,10 @@ def test_list_network_interfaces_with_bond(self,
62076239
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
62086240
]
62096241
}
6242+
mocked_get_mac_addr.side_effect = lambda iface: {
6243+
'lo': '00:00:00:00:00:00',
6244+
'bond0': '00:0c:29:8c:11:b1',
6245+
}.get(iface)
62106246
mocked_execute.return_value = ('\n', '')
62116247
mock_has_carrier.return_value = True
62126248
interfaces = self.hardware.list_network_interfaces()
@@ -6231,7 +6267,8 @@ def test_list_network_interfaces_with_pci_address(self,
62316267
mocked_listdir,
62326268
mocked_net_if_addrs,
62336269
mockedget_managers,
6234-
mocked_lshw):
6270+
mocked_lshw,
6271+
mocked_get_mac_addr):
62356272
mocked_listdir.return_value = ['lo', 'eth0']
62366273
mocked_exists.side_effect = [False, False, True]
62376274
mocked_open.return_value.__enter__ = lambda s: s
@@ -6250,6 +6287,10 @@ def test_list_network_interfaces_with_pci_address(self,
62506287
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
62516288
]
62526289
}
6290+
mocked_get_mac_addr.side_effect = lambda iface: {
6291+
'lo': '00:00:00:00:00:00',
6292+
'eth0': '00:0c:29:8c:11:b1',
6293+
}.get(iface)
62536294
mocked_execute.return_value = ('em0\n', '')
62546295
mock_has_carrier.return_value = True
62556296
mock_get_pci.return_value = '0000:02:00.0'
@@ -6275,7 +6316,8 @@ def test_list_network_vlan_interfaces(self,
62756316
mocked_listdir,
62766317
mocked_net_if_addrs,
62776318
mockedget_managers,
6278-
mocked_lshw):
6319+
mocked_lshw,
6320+
mocked_get_mac_addr):
62796321
CONF.set_override('enable_vlan_interfaces', 'eth0.100')
62806322
mocked_listdir.return_value = ['lo', 'eth0']
62816323
mocked_exists.side_effect = [False, False, True]
@@ -6300,6 +6342,11 @@ def test_list_network_vlan_interfaces(self,
63006342
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1')
63016343
]
63026344
}
6345+
mocked_get_mac_addr.side_effect = lambda iface: {
6346+
'lo': '00:00:00:00:00:00',
6347+
'eth0': '00:0c:29:8c:11:b1',
6348+
'eth0.100': '00:0c:29:8c:11:b1',
6349+
}.get(iface)
63036350
mocked_execute.return_value = ('em0\n', '')
63046351
mock_has_carrier.return_value = True
63056352
interfaces = self.hardware.list_network_interfaces()
@@ -6323,7 +6370,8 @@ def test_list_network_vlan_interfaces_using_lldp(self,
63236370
mocked_listdir,
63246371
mocked_net_if_addrs,
63256372
mockedget_managers,
6326-
mocked_lshw):
6373+
mocked_lshw,
6374+
mocked_get_mac_addr):
63276375
CONF.set_override('collect_lldp', True)
63286376
CONF.set_override('enable_vlan_interfaces', 'eth0')
63296377
mocked_listdir.return_value = ['lo', 'eth0']
@@ -6355,6 +6403,12 @@ def test_list_network_vlan_interfaces_using_lldp(self,
63556403
FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c2')
63566404
]
63576405
}
6406+
mocked_get_mac_addr.side_effect = lambda iface: {
6407+
'lo': '00:00:00:00:00:00',
6408+
'eth0': '00:0c:29:8c:11:b1',
6409+
'eth0.100': '00:0c:29:8c:11:c1',
6410+
'eth0.101': '00:0c:29:8c:11:c2',
6411+
}.get(iface)
63586412
mocked_lldp_info.return_value = {'eth0': [
63596413
(0, b''),
63606414
(127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'),
@@ -6388,7 +6442,8 @@ def test_list_network_vlan_invalid_int(self,
63886442
mocked_listdir,
63896443
mocked_net_if_addrs,
63906444
mockedget_managers,
6391-
mocked_lshw):
6445+
mocked_lshw,
6446+
mocked_get_mac_addr):
63926447
CONF.set_override('collect_lldp', True)
63936448
CONF.set_override('enable_vlan_interfaces', 'enp0s1')
63946449
mocked_listdir.return_value = ['lo', 'eth0']
@@ -6426,7 +6481,8 @@ def test_list_network_vlan_interfaces_using_lldp_all(self,
64266481
mocked_listdir,
64276482
mocked_net_if_addrs,
64286483
mockedget_managers,
6429-
mocked_lshw):
6484+
mocked_lshw,
6485+
mocked_get_mac_addr):
64306486
CONF.set_override('collect_lldp', True)
64316487
CONF.set_override('enable_vlan_interfaces', 'all')
64326488
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)