Remove netifaces usage

The Ironic project recently learned that the netifaces library was
no longer being maintained. As the underlying dependency was minimal,
the dependency has been removed from the ironic-python-agent.

Change-Id: I1a7c01d16eb8d2a2e372bd0d2474a601d3d2c1f4
This commit is contained in:
Julia Kreger 2023-04-10 08:47:06 -07:00
parent 0304c73c0e
commit 30b360d543
5 changed files with 165 additions and 80 deletions

View File

@ -19,7 +19,6 @@ import socket
import struct
import sys
import netifaces
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import netutils
@ -213,31 +212,51 @@ def _get_lldp_info(interfaces):
return lldp_info
def get_default_ip_addr(type, interface_id):
"""Retrieve default IPv4 or IPv6 address."""
try:
addrs = netifaces.ifaddresses(interface_id)
return addrs[type][0]['addr']
except (ValueError, IndexError, KeyError):
# No default IP address found
return None
def _extract_address(lines):
"""Extract an usable IP address from the supplied lines.
:param lines: The split lines of an "ip addr show $interface" command.
:returns: The first found global/non-temporary IP address.
"""
for line in lines:
# Get a global, non-temporary (i.e. v6 privacy) address
if 'global' in line and 'temporary' not in line:
line = line.lstrip()
# line is the list of addresses
address = line.split(' ')[1]
# strip tailing / off of the result, as v4 and v6 addresses
return address.split('/')[0]
def get_ipv4_addr(interface_id):
return get_default_ip_addr(netifaces.AF_INET, interface_id)
try:
out, _ = utils.execute('ip', '-4', 'addr', 'show', interface_id)
except OSError:
LOG.error('The IP command is required. Could not get IPv4 address.')
return None
return _extract_address(out.splitlines())
def get_ipv6_addr(interface_id):
return get_default_ip_addr(netifaces.AF_INET6, interface_id)
try:
out, _ = utils.execute('ip', '-6', 'addr', 'show', interface_id)
except OSError:
LOG.error('The IP command is required. Could not get IPv4 address.')
return None
return _extract_address(out.splitlines())
def get_mac_addr(interface_id):
path = '/sys/class/net/{}/address'.format(interface_id)
try:
addrs = netifaces.ifaddresses(interface_id)
return addrs[netifaces.AF_LINK][0]['addr']
except (ValueError, IndexError, KeyError):
# No mac address found
with open(path, 'rt') as fp:
return fp.read().strip()
except OSError as e:
LOG.debug('Encountered error while attempting to access the address '
'for %s. Error: %s', interface_id, e)
return None
LOG.debug('No MAC Address found for interface %s.', interface_id)
return None
# Other options...

View File

@ -15,6 +15,8 @@
import os
from unittest import mock
from ironic_lib import utils as il_utils
from ironic_python_agent import errors
from ironic_python_agent import hardware
from ironic_python_agent.hardware_managers import mlnx
@ -25,6 +27,13 @@ IB_ADDRESS = 'a0:00:00:27:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52'
CLIENT_ID = 'ff:00:00:00:00:00:02:00:00:02:c9:00:7c:fe:90:03:00:29:26:52'
IPV4_ADDRESS = """
3: ib0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
inet 192.168.1.2/24 brd 192.168.1.255 scope global dynamic noprefixroute eth0
valid_lft 30541sec preferred_lft 30541sec
""" # noqa
class MlnxHardwareManager(base.IronicAgentTest):
def setUp(self):
super(MlnxHardwareManager, self).setUp()
@ -121,12 +130,20 @@ class MlnxHardwareManager(base.IronicAgentTest):
hardware.HardwareSupport.NONE,
self.hardware.evaluate_hardware_support())
@mock.patch.object(il_utils, 'execute', autospec=True)
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
@mock.patch.object(hardware, '_get_device_info', autospec=True)
def test_get_interface_info(self, mocked_get_device_info, mock_get_mac):
def test_get_interface_info(self, mocked_get_device_info, mock_get_mac,
mocked_execute):
mocked_get_device_info.side_effect = ['0x15b3', '0x0014']
mock_get_mac.return_value = IB_ADDRESS
mocked_execute.side_effect = [
(IPV4_ADDRESS, '')
]
network_interface = self.hardware.get_interface_info('ib0')
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'ib0')
])
self.assertEqual('ib0', network_interface.name)
self.assertEqual('7c:fe:90:29:26:52', network_interface.mac_address)
self.assertEqual('0x15b3', network_interface.vendor)

View File

@ -22,7 +22,6 @@ from unittest import mock
from ironic_lib import disk_utils
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
@ -80,6 +79,26 @@ BLK_DEVICE_TEMPLATE_PARTUUID_DEVICE = [
partuuid="1234-5678", serial="sda1123"),
]
IPV4_ADDRESS = """
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
inet 192.168.1.2/24 brd 192.168.1.255 scope global dynamic noprefixroute eth0
valid_lft 30541sec preferred_lft 30541sec
""" # noqa
IPV6_ADDRESS = """
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
inet6 fd00::101/128 scope global dynamic noprefixroute
valid_lft 379301sec preferred_lft 379301sec
inet6 fd9b:f401:ddb1::f01/128 scope global noprefixroute
valid_lft forever preferred_lft forever
inet6 fd9b:f401:ddb1:0:ffe5:3caa:b00e:b54d/64 scope global temporary dynamic
valid_lft 592077sec preferred_lft 73462sec
inet6 fd9b:f401:ddb1:0:fad1:11ff:fead:6fcb/64 scope global mngtmpaddr noprefixroute
valid_lft forever preferred_lft forever
inet6 fe80::fad1:aaff:fead:11cb/64 scope link noprefixroute
valid_lft forever preferred_lft forever
""" # noqa
class FakeHardwareManager(hardware.GenericHardwareManager):
def __init__(self, hardware_support):
@ -270,7 +289,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual(expected_lldp_data, result)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -284,7 +302,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
mocked_listdir.return_value = ['lo', 'eth0', 'foobar']
@ -293,17 +310,22 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_execute.return_value = ('em0\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('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()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'eth0'),
mock.call('ip', '-6', 'addr', 'show', 'eth0'),
mock.call('biosdevname', '-i', 'eth0')
])
self.assertEqual(1, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
@ -314,7 +336,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual('em0', interfaces[0].biosdevname)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -328,7 +349,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
mocked_listdir.return_value = ['lo', 'eth0']
@ -337,14 +357,19 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_execute.return_value = ('em0\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('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()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'eth0'),
mock.call('ip', '-6', 'addr', 'show', 'eth0'),
mock.call('biosdevname', '-i', 'eth0')
])
self.assertEqual(1, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
@ -407,7 +432,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -421,7 +445,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mocked_lldp_info,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
@ -432,10 +455,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_lldp_info.return_value = {'eth0': [
(0, b''),
(1, b'\x04\x88Z\x92\xecTY'),
@ -444,8 +463,17 @@ class TestGenericHardwareManager(base.IronicAgentTest):
}
mock_has_carrier.return_value = True
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
mocked_execute.return_value = ('em0\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('em0\n', '')
]
interfaces = self.hardware.list_network_interfaces()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'eth0'),
mock.call('ip', '-6', 'addr', 'show', 'eth0'),
mock.call('biosdevname', '-i', 'eth0')
])
self.assertEqual(1, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
@ -465,14 +493,13 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(netutils, 'get_mac_addr', autospec=True)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
@mock.patch('netifaces.ifaddresses', 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)
def test_list_network_interfaces_with_lldp_error(
self, mocked_execute, mocked_open, mocked_exists, mocked_listdir,
mocked_ifaddresses, mocked_lldp_info, mockedget_managers,
mocked_lldp_info, mockedget_managers,
mock_get_mac, mock_has_carrier):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
CONF.set_override('collect_lldp', True)
@ -482,15 +509,20 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_lldp_info.side_effect = Exception('Boom!')
mocked_execute.return_value = ('em0\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('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()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'eth0'),
mock.call('ip', '-6', 'addr', 'show', 'eth0'),
mock.call('biosdevname', '-i', 'eth0')
])
self.assertEqual(1, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
@ -525,14 +557,19 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_execute.return_value = ('em0\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('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()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'eth0'),
mock.call('ip', '-6', 'addr', 'show', 'eth0'),
mock.call('biosdevname', '-i', 'eth0')
])
self.assertEqual(1, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
@ -543,7 +580,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual('em0', interfaces[0].biosdevname)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -557,7 +593,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
mocked_listdir.return_value = ['lo', 'eth0']
@ -567,14 +602,19 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_execute.return_value = ('em0\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('em0\n', '')
]
mock_has_carrier.return_value = True
mock_get_mac.return_value = mac
interfaces = self.hardware.list_network_interfaces()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'eth0'),
mock.call('ip', '-6', 'addr', 'show', 'eth0'),
mock.call('biosdevname', '-i', 'eth0')
])
self.assertEqual(1, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual(mac, interfaces[0].mac_address)
@ -586,7 +626,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual('em0', interfaces[0].biosdevname)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -600,7 +639,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
mocked_listdir.return_value = ['lo', 'bond0']
@ -609,17 +647,22 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_execute.return_value = ('\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('\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()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'bond0'),
mock.call('ip', '-6', 'addr', 'show', 'bond0'),
mock.call('biosdevname', '-i', 'bond0')
])
self.assertEqual(1, len(interfaces))
self.assertEqual('bond0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
@ -630,7 +673,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual('', interfaces[0].biosdevname)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -644,7 +686,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
CONF.set_override('enable_vlan_interfaces', 'eth0.100')
@ -654,14 +695,24 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_execute.return_value = ('em0\n', '')
mocked_execute.side_effect = [
(IPV4_ADDRESS, ''),
(IPV6_ADDRESS, ''),
('em0\n', ''),
('', ''),
('', ''),
('', ''),
('', ''),
]
mock_get_mac.mock_has_carrier = True
mock_get_mac.return_value = '00:0c:29:8c:11:b1'
interfaces = self.hardware.list_network_interfaces()
mocked_execute.assert_has_calls([
mock.call('ip', '-4', 'addr', 'show', 'eth0'),
mock.call('ip', '-6', 'addr', 'show', 'eth0'),
mock.call('biosdevname', '-i', 'eth0')
])
self.assertEqual(7, mocked_execute.call_count)
self.assertEqual(2, len(interfaces))
self.assertEqual('eth0', interfaces[0].name)
self.assertEqual('00:0c:29:8c:11:b1', interfaces[0].mac_address)
@ -674,7 +725,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -688,7 +738,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mocked_lldp_info,
mockedget_managers):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
@ -727,7 +776,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(netutils, 'LOG', autospec=True)
@mock.patch('ironic_python_agent.hardware.get_managers', autospec=True)
@mock.patch('netifaces.ifaddresses', autospec=True)
@mock.patch('os.listdir', autospec=True)
@mock.patch('os.path.exists', autospec=True)
@mock.patch('builtins.open', autospec=True)
@ -741,7 +789,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_open,
mocked_exists,
mocked_listdir,
mocked_ifaddresses,
mockedget_managers,
mocked_log):
mockedget_managers.return_value = [hardware.GenericHardwareManager()]
@ -753,10 +800,6 @@ class TestGenericHardwareManager(base.IronicAgentTest):
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_execute.return_value = ('em0\n', '')
mock_get_mac.mock_has_carrier = True
mock_get_mac.return_value = '00:0c:29:8c:11:b1'

View File

@ -0,0 +1,7 @@
---
other:
- |
The Python ``netifaces`` library has been removed as a dependency by the
Ironic team upon learning that the maintainer was no longer maintaining
the library. The agent's use of this library was minimal, thus enabling
us to easily remove the dependency.

View File

@ -3,7 +3,6 @@
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
eventlet!=0.18.3,!=0.20.1,>=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