From f28008479b23d3a78fe30ba77835342d438629ba Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sun, 6 Oct 2024 22:08:13 +0900 Subject: [PATCH 1/3] Drop dependency on netifaces The netifaces library was abandoned and archived. Replace it by psutil which is already part of the requirements. Closes-Bug: #2071596 Change-Id: Ibca206ec2af1374199d0c0cfad897dded1298733 (cherry picked from commit 42ea1dbd1a106147c8ed332782e942ee745d6c74) --- ironic_python_agent/netutils.py | 28 +- .../tests/unit/test_hardware.py | 273 +++++++++++++----- requirements.txt | 1 - 3 files changed, 212 insertions(+), 90 deletions(-) diff --git a/ironic_python_agent/netutils.py b/ironic_python_agent/netutils.py index 2e289808..2f39bbaf 100644 --- a/ironic_python_agent/netutils.py +++ b/ironic_python_agent/netutils.py @@ -20,10 +20,10 @@ import struct import sys -import netifaces from oslo_config import cfg from oslo_log import log as logging from oslo_utils import netutils +import psutil from ironic_python_agent import utils @@ -214,31 +214,29 @@ def _get_lldp_info(interfaces): return lldp_info -def get_default_ip_addr(type, interface_id): - """Retrieve default IPv4 or IPv6 address.""" +def get_default_ip_addr(family, interface_id): + """Retrieve default IPv4, IPv6 or mac address.""" try: - addrs = netifaces.ifaddresses(interface_id) - return addrs[type][0]['addr'] - except (ValueError, IndexError, KeyError): + addrs = psutil.net_if_addrs()[interface_id] + for addr in addrs: + if addr.family == family: + return addr.address + except KeyError: # No default IP address found - return None + pass + return None def get_ipv4_addr(interface_id): - return get_default_ip_addr(netifaces.AF_INET, interface_id) + return get_default_ip_addr(socket.AF_INET, interface_id) def get_ipv6_addr(interface_id): - return get_default_ip_addr(netifaces.AF_INET6, interface_id) + return get_default_ip_addr(socket.AF_INET6, interface_id) def get_mac_addr(interface_id): - try: - addrs = netifaces.ifaddresses(interface_id) - return addrs[netifaces.AF_LINK][0]['addr'] - except (ValueError, IndexError, KeyError): - # No mac address found - return None + return get_default_ip_addr(socket.AF_PACKET, interface_id) # Other options... diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 75869971..2a418be3 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -13,16 +13,18 @@ # limitations under the License. import binascii +from collections import namedtuple +import glob import json import os import re import shutil +import socket import stat import time from unittest import mock from ironic_lib import utils as il_utils -import netifaces from oslo_concurrency import processutils from oslo_config import cfg from oslo_utils import units @@ -5895,30 +5897,32 @@ def fake_execute(cmd, *args, **kwargs): self.assertGreaterEqual(len(io_dict), len(expected)) +FakeAddr = namedtuple('FakeAddr', ('family', 'address')) + + @mock.patch.object(hardware.GenericHardwareManager, '_get_system_lshw_dict', autospec=True, return_value={'id': 'host'}) @mock.patch.object(hardware, 'get_managers', autospec=True, return_value=[hardware.GenericHardwareManager()]) -@mock.patch('netifaces.ifaddresses', autospec=True) +@mock.patch('psutil.net_if_addrs', autospec=True) @mock.patch('os.listdir', autospec=True) @mock.patch('os.path.exists', autospec=True) @mock.patch('builtins.open', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) -@mock.patch.object(netutils, 'get_mac_addr', autospec=True) @mock.patch.object(netutils, 'interface_has_carrier', autospec=True) class TestListNetworkInterfaces(base.IronicAgentTest): + def setUp(self): super().setUp() self.hardware = hardware.GenericHardwareManager() def test_list_network_interfaces(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): mocked_lshw.return_value = json.loads(hws.LSHW_JSON_OUTPUT_V2[0]) @@ -5928,16 +5932,24 @@ def test_list_network_interfaces(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'foobar': [ + FakeAddr(socket.AF_INET, '192.168.2.2'), + FakeAddr(socket.AF_INET6, 'fd00:1000::101') + ] } mocked_execute.return_value = ('em0\n', '') mock_has_carrier.return_value = True - mock_get_mac.side_effect = [ - '00:0c:29:8c:11:b1', - None, - ] interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -5951,12 +5963,11 @@ def test_list_network_interfaces(self, def test_list_network_interfaces_with_biosdevname(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): mocked_listdir.return_value = ['lo', 'eth0'] @@ -5965,12 +5976,19 @@ def test_list_network_interfaces_with_biosdevname(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_execute.return_value = ('em0\n', '') - mock_get_mac.return_value = '00:0c:29:8c:11:b1' mock_has_carrier.return_value = True interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) @@ -5987,12 +6005,11 @@ def test_list_network_interfaces_with_biosdevname(self, def test_list_network_interfaces_with_lldp(self, mocked_lldp_info, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): CONF.set_override('collect_lldp', True) @@ -6002,9 +6019,17 @@ def test_list_network_interfaces_with_lldp(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_lldp_info.return_value = {'eth0': [ (0, b''), @@ -6013,7 +6038,6 @@ def test_list_network_interfaces_with_lldp(self, (3, b'\x00x')] } mock_has_carrier.return_value = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' mocked_execute.return_value = ('em0\n', '') interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) @@ -6032,10 +6056,16 @@ def test_list_network_interfaces_with_lldp(self, self.assertEqual('em0', interfaces[0].biosdevname) @mock.patch.object(netutils, 'get_lldp_info', autospec=True) - def test_list_network_interfaces_with_lldp_error( - self, mocked_lldp_info, mock_has_carrier, mock_get_mac, - mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, mockedget_managers, mocked_lshw): + def test_list_network_interfaces_with_lldp_error(self, + mocked_lldp_info, + mock_has_carrier, + mocked_execute, + mocked_open, + mocked_exists, + mocked_listdir, + mocked_net_if_addrs, + mockedget_managers, + mocked_lshw): CONF.set_override('collect_lldp', True) mocked_listdir.return_value = ['lo', 'eth0'] mocked_exists.side_effect = [False, False, True] @@ -6043,14 +6073,21 @@ def test_list_network_interfaces_with_lldp_error( mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_lldp_info.side_effect = Exception('Boom!') mocked_execute.return_value = ('em0\n', '') mock_has_carrier.return_value = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6063,12 +6100,11 @@ def test_list_network_interfaces_with_lldp_error( def test_list_network_interfaces_no_carrier(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): @@ -6079,13 +6115,20 @@ def test_list_network_interfaces_no_carrier(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = [OSError('boom')] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_execute.return_value = ('em0\n', '') mock_has_carrier.return_value = False - mock_get_mac.return_value = '00:0c:29:8c:11:b1' interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6098,12 +6141,11 @@ def test_list_network_interfaces_no_carrier(self, def test_list_network_interfaces_with_vendor_info(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): mocked_listdir.return_value = ['lo', 'eth0'] @@ -6113,13 +6155,20 @@ def test_list_network_interfaces_with_vendor_info(self, read_mock = mocked_open.return_value.read mac = '00:0c:29:8c:11:b1' read_mock.side_effect = ['0x15b3\n', '0x1014\n'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_execute.return_value = ('em0\n', '') mock_has_carrier.return_value = True - mock_get_mac.return_value = mac interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6133,12 +6182,11 @@ def test_list_network_interfaces_with_vendor_info(self, def test_list_network_interfaces_with_bond(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): mocked_listdir.return_value = ['lo', 'bond0'] @@ -6147,16 +6195,20 @@ def test_list_network_interfaces_with_bond(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'bond0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_execute.return_value = ('\n', '') mock_has_carrier.return_value = True - mock_get_mac.side_effect = [ - '00:0c:29:8c:11:b1', - None, - ] interfaces = self.hardware.list_network_interfaces() self.assertEqual(1, len(interfaces)) self.assertEqual('bond0', interfaces[0].name) @@ -6169,12 +6221,11 @@ def test_list_network_interfaces_with_bond(self, def test_list_network_vlan_interfaces(self, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): CONF.set_override('enable_vlan_interfaces', 'eth0.100') @@ -6184,13 +6235,25 @@ def test_list_network_vlan_interfaces(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'eth0.100': [ + FakeAddr(socket.AF_INET, '192.168.2.2'), + FakeAddr(socket.AF_INET6, 'fd00::1000::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_execute.return_value = ('em0\n', '') - mock_get_mac.mock_has_carrier = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' + mock_has_carrier.return_value = True interfaces = self.hardware.list_network_interfaces() self.assertEqual(2, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6206,12 +6269,11 @@ def test_list_network_vlan_interfaces(self, def test_list_network_vlan_interfaces_using_lldp(self, mocked_lldp_info, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): CONF.set_override('collect_lldp', True) @@ -6223,13 +6285,34 @@ def test_list_network_vlan_interfaces_using_lldp(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'eth0.100': [ + FakeAddr(socket.AF_INET, '192.168.100.2'), + FakeAddr(socket.AF_INET6, 'fd00:0100::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c1') + ], + 'eth0.101': [ + FakeAddr(socket.AF_INET, '192.168.101.2'), + FakeAddr(socket.AF_INET6, 'fd00:0101::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c2') + ] + } mocked_lldp_info.return_value = {'eth0': [ (0, b''), (127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'), (127, b'\x00\x80\xc2\x03\x00e\x08vlan-101')] } mock_has_carrier.return_value = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' interfaces = self.hardware.list_network_interfaces() self.assertEqual(3, len(interfaces)) self.assertEqual('eth0', interfaces[0].name) @@ -6241,22 +6324,21 @@ def test_list_network_vlan_interfaces_using_lldp(self, ] self.assertEqual(expected_lldp_info, interfaces[0].lldp) self.assertEqual('eth0.100', interfaces[1].name) - self.assertEqual('00:0c:29:8c:11:b1', interfaces[1].mac_address) + self.assertEqual('00:0c:29:8c:11:c1', interfaces[1].mac_address) self.assertIsNone(interfaces[1].lldp) self.assertEqual('eth0.101', interfaces[2].name) - self.assertEqual('00:0c:29:8c:11:b1', interfaces[2].mac_address) + self.assertEqual('00:0c:29:8c:11:c2', interfaces[2].mac_address) self.assertIsNone(interfaces[2].lldp) @mock.patch.object(netutils, 'LOG', autospec=True) def test_list_network_vlan_invalid_int(self, mocked_log, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): CONF.set_override('collect_lldp', True) @@ -6267,13 +6349,20 @@ def test_list_network_vlan_invalid_int(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] - mocked_ifaddresses.return_value = { - netifaces.AF_INET: [{'addr': '192.168.1.2'}], - netifaces.AF_INET6: [{'addr': 'fd00::101'}] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] } mocked_execute.return_value = ('em0\n', '') - mock_get_mac.mock_has_carrier = True - mock_get_mac.return_value = '00:0c:29:8c:11:b1' + mock_has_carrier.return_value = True self.hardware.list_network_interfaces() mocked_log.warning.assert_called_once_with( @@ -6283,12 +6372,11 @@ def test_list_network_vlan_invalid_int(self, def test_list_network_vlan_interfaces_using_lldp_all(self, mocked_lldp_info, mock_has_carrier, - mock_get_mac, mocked_execute, mocked_open, mocked_exists, mocked_listdir, - mocked_ifaddresses, + mocked_net_if_addrs, mockedget_managers, mocked_lshw): CONF.set_override('collect_lldp', True) @@ -6300,6 +6388,43 @@ def test_list_network_vlan_interfaces_using_lldp_all(self, mocked_open.return_value.__exit__ = mock.Mock() read_mock = mocked_open.return_value.read read_mock.side_effect = ['1'] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ], + 'eth1': [ + FakeAddr(socket.AF_INET, '192.168.2.2'), + FakeAddr(socket.AF_INET6, 'fd00:1000::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b2') + ], + 'eth0.100': [ + FakeAddr(socket.AF_INET, '192.168.100.2'), + FakeAddr(socket.AF_INET6, 'fd00:0100::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c1') + ], + 'eth0.101': [ + FakeAddr(socket.AF_INET, '192.168.101.2'), + FakeAddr(socket.AF_INET6, 'fd00:0101::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:c2') + ], + 'eth1.102': [ + FakeAddr(socket.AF_INET, '192.168.102.2'), + FakeAddr(socket.AF_INET6, 'fd00:1102::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:d1') + ], + 'eth1.103': [ + FakeAddr(socket.AF_INET, '192.168.103.2'), + FakeAddr(socket.AF_INET6, 'fd00:1103::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:d2') + ] + } mocked_lldp_info.return_value = {'eth0': [ (0, b''), (127, b'\x00\x80\xc2\x03\x00d\x08vlan-100'), diff --git a/requirements.txt b/requirements.txt index e7481c3e..b943d1f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ pbr>=2.0.0 # Apache-2.0 eventlet>=0.18.2 # MIT -netifaces>=0.10.4 # MIT oslo.config>=5.2.0 # Apache-2.0 oslo.concurrency>=3.26.0 # Apache-2.0 oslo.log>=4.6.1 # Apache-2.0 From f7eb261c3691bdaa3b94088e40210fd4b9a5b17d Mon Sep 17 00:00:00 2001 From: Kaifeng Wang Date: Sat, 1 Feb 2025 14:35:16 +0800 Subject: [PATCH 2/3] Collect bus and driver for interfaces It's useful to have pci bus address/driver collected, the operator can use the information to configure portgroup in a consistent way. Change-Id: I432bca881ad881bae6d5e67c9b6fb52fe55b4e1e (cherry picked from commit 96bf1ef0123ccd0b31e763267bd22b11db4e6c95) --- ironic_python_agent/hardware.py | 13 +++-- ironic_python_agent/netutils.py | 18 +++++++ .../tests/unit/test_hardware.py | 48 +++++++++++++++++++ .../tests/unit/test_netutils.py | 24 ++++++++++ ...-intf-bus-and-driver-63ed0277b372c1d1.yaml | 4 ++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/report-intf-bus-and-driver-63ed0277b372c1d1.yaml diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 04bb26c6..63391804 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -809,11 +809,13 @@ class NetworkInterface(encoding.SerializableComparable): serializable_fields = ('name', 'mac_address', 'ipv4_address', 'ipv6_address', 'has_carrier', 'lldp', 'vendor', 'product', 'client_id', - 'biosdevname', 'speed_mbps') + 'biosdevname', 'speed_mbps', 'pci_address', + 'driver') def __init__(self, name, mac_addr, ipv4_address=None, ipv6_address=None, has_carrier=True, lldp=None, vendor=None, product=None, - client_id=None, biosdevname=None, speed_mbps=None): + client_id=None, biosdevname=None, speed_mbps=None, + pci_address=None, driver=None): self.name = name self.mac_address = mac_addr self.ipv4_address = ipv4_address @@ -824,6 +826,8 @@ def __init__(self, name, mac_addr, ipv4_address=None, ipv6_address=None, self.product = product self.biosdevname = biosdevname self.speed_mbps = speed_mbps + self.pci_address = pci_address + self.driver = driver # client_id is used for InfiniBand only. we calculate the DHCP # client identifier Option to allow DHCP to work over InfiniBand. # see https://tools.ietf.org/html/rfc4390 @@ -1431,7 +1435,10 @@ def get_interface_info(self, interface_name): vendor=_get_device_info(interface_name, 'net', 'vendor'), product=_get_device_info(interface_name, 'net', 'device'), biosdevname=self.get_bios_given_nic_name(interface_name), - speed_mbps=self._get_network_speed(interface_name)) + speed_mbps=self._get_network_speed(interface_name), + pci_address=netutils.get_interface_pci_address(interface_name), + driver=netutils.get_interface_driver(interface_name) + ) def get_ipv4_addr(self, interface_id): return netutils.get_ipv4_addr(interface_id) diff --git a/ironic_python_agent/netutils.py b/ironic_python_agent/netutils.py index 2f39bbaf..8f401855 100644 --- a/ironic_python_agent/netutils.py +++ b/ironic_python_agent/netutils.py @@ -280,6 +280,24 @@ def interface_has_carrier(interface_name): return False +def get_interface_pci_address(interface_name): + path = '/sys/class/net/{}/device'.format(interface_name) + try: + return os.path.basename(os.readlink(path)) + except FileNotFoundError: + LOG.debug('No bus address found for interface %s', + interface_name) + + +def get_interface_driver(interface_name): + path = '/sys/class/net/{}/device/driver'.format(interface_name) + try: + return os.path.basename(os.readlink(path)) + except FileNotFoundError: + LOG.debug('No driver found for interface %s', + interface_name) + + def wrap_ipv6(ip): if netutils.is_valid_ipv6(ip): return "[%s]" % ip diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 2a418be3..f23540f1 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -6219,6 +6219,54 @@ def test_list_network_interfaces_with_bond(self, self.assertTrue(interfaces[0].has_carrier) self.assertEqual('', interfaces[0].biosdevname) + @mock.patch.object(netutils, 'get_interface_driver', autospec=True) + @mock.patch.object(netutils, 'get_interface_pci_address', autospec=True) + def test_list_network_interfaces_with_pci_address(self, + mock_get_pci, + mock_get_driver, + mock_has_carrier, + mocked_execute, + mocked_open, + mocked_exists, + mocked_listdir, + mocked_net_if_addrs, + mockedget_managers, + mocked_lshw): + mocked_listdir.return_value = ['lo', 'eth0'] + mocked_exists.side_effect = [False, False, True] + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = ['1'] + mocked_net_if_addrs.return_value = { + 'lo': [ + FakeAddr(socket.AF_INET, '127.0.0.1'), + FakeAddr(socket.AF_INET6, '::1'), + FakeAddr(socket.AF_PACKET, '00:00:00:00:00:00') + ], + 'eth0': [ + FakeAddr(socket.AF_INET, '192.168.1.2'), + FakeAddr(socket.AF_INET6, 'fd00::101'), + FakeAddr(socket.AF_PACKET, '00:0c:29:8c:11:b1') + ] + } + mocked_execute.return_value = ('em0\n', '') + mock_has_carrier.return_value = True + mock_get_pci.return_value = '0000:02:00.0' + mock_get_driver.return_value = 'e1000e' + interfaces = self.hardware.list_network_interfaces() + self.assertEqual(1, len(interfaces)) + self.assertEqual('eth0', interfaces[0].name) + self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address) + self.assertEqual('192.168.1.2', interfaces[0].ipv4_address) + self.assertEqual('fd00::101', interfaces[0].ipv6_address) + self.assertIsNone(interfaces[0].lldp) + self.assertTrue(interfaces[0].has_carrier) + self.assertEqual('em0', interfaces[0].biosdevname) + self.assertIsNone(interfaces[0].speed_mbps) + self.assertEqual('0000:02:00.0', interfaces[0].pci_address) + self.assertEqual('e1000e', interfaces[0].driver) + def test_list_network_vlan_interfaces(self, mock_has_carrier, mocked_execute, diff --git a/ironic_python_agent/tests/unit/test_netutils.py b/ironic_python_agent/tests/unit/test_netutils.py index 21bf3a4a..255cbecb 100644 --- a/ironic_python_agent/tests/unit/test_netutils.py +++ b/ironic_python_agent/tests/unit/test_netutils.py @@ -394,3 +394,27 @@ def test_wrap_ipv6(self): def test_wrap_ipv6_with_ipv4(self): res = netutils.wrap_ipv6('1.2.3.4') self.assertEqual('1.2.3.4', res) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_pci_address(self, mock_read): + mock_read.return_value = '../../../0000:02:00.0' + addr = netutils.get_interface_pci_address('ens160') + self.assertEqual('0000:02:00.0', addr) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_pci_address_notfound(self, mock_read): + mock_read.side_effect = FileNotFoundError + addr = netutils.get_interface_pci_address('ens160') + self.assertIsNone(addr) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_driver(self, mock_read): + mock_read.return_value = '../../../../bus/pci/drivers/e1000e' + addr = netutils.get_interface_driver('ens160') + self.assertEqual('e1000e', addr) + + @mock.patch('os.readlink', autospec=True) + def test_get_interface_driver_notfound(self, mock_read): + mock_read.side_effect = FileNotFoundError + driver = netutils.get_interface_driver('ens160') + self.assertIsNone(driver) diff --git a/releasenotes/notes/report-intf-bus-and-driver-63ed0277b372c1d1.yaml b/releasenotes/notes/report-intf-bus-and-driver-63ed0277b372c1d1.yaml new file mode 100644 index 00000000..ead108a5 --- /dev/null +++ b/releasenotes/notes/report-intf-bus-and-driver-63ed0277b372c1d1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support to collect pci address and driver information for interfaces. From 660c77009cfd42dee370f235720db465be5ef57f Mon Sep 17 00:00:00 2001 From: Nicolas Belouin Date: Mon, 7 Apr 2025 15:36:14 +0200 Subject: [PATCH 3/3] 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 | 81 ++++++++++++++++--- .../fix-mac-permaddr-0bc7d688eee4b814.yaml | 9 +++ 3 files changed, 118 insertions(+), 15 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 8f401855..9eca6933 100644 --- a/ironic_python_agent/netutils.py +++ b/ironic_python_agent/netutils.py @@ -34,6 +34,13 @@ 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 @@ 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 f23540f1..997cef58 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -14,7 +14,6 @@ import binascii from collections import namedtuple -import glob import json import os import re @@ -5900,6 +5899,7 @@ def fake_execute(cmd, *args, **kwargs): 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, @@ -5924,7 +5924,8 @@ def test_list_network_interfaces(self, 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] @@ -5948,6 +5949,10 @@ def test_list_network_interfaces(self, 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() @@ -5969,7 +5974,8 @@ def test_list_network_interfaces_with_biosdevname(self, 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 @@ -5988,6 +5994,10 @@ def test_list_network_interfaces_with_biosdevname(self, 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() @@ -6011,7 +6021,8 @@ def test_list_network_interfaces_with_lldp(self, 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] @@ -6031,6 +6042,10 @@ def test_list_network_interfaces_with_lldp(self, 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'), @@ -6065,7 +6080,8 @@ def test_list_network_interfaces_with_lldp_error(self, 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] @@ -6085,6 +6101,10 @@ def test_list_network_interfaces_with_lldp_error(self, 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 @@ -6106,7 +6126,8 @@ def test_list_network_interfaces_no_carrier(self, 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'] @@ -6127,6 +6148,10 @@ def test_list_network_interfaces_no_carrier(self, 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() @@ -6147,7 +6172,8 @@ def test_list_network_interfaces_with_vendor_info(self, 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 @@ -6167,6 +6193,10 @@ def test_list_network_interfaces_with_vendor_info(self, 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() @@ -6188,7 +6218,8 @@ def test_list_network_interfaces_with_bond(self, 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 @@ -6207,6 +6238,10 @@ def test_list_network_interfaces_with_bond(self, 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() @@ -6231,7 +6266,8 @@ def test_list_network_interfaces_with_pci_address(self, 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 @@ -6250,6 +6286,10 @@ def test_list_network_interfaces_with_pci_address(self, 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' @@ -6275,7 +6315,8 @@ def test_list_network_vlan_interfaces(self, 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] @@ -6300,6 +6341,11 @@ def test_list_network_vlan_interfaces(self, 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() @@ -6323,7 +6369,8 @@ def test_list_network_vlan_interfaces_using_lldp(self, 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'] @@ -6355,6 +6402,12 @@ def test_list_network_vlan_interfaces_using_lldp(self, 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'), @@ -6388,7 +6441,8 @@ def test_list_network_vlan_invalid_int(self, 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'] @@ -6426,7 +6480,8 @@ def test_list_network_vlan_interfaces_using_lldp_all(self, 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 00000000..7edb156f --- /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. +