From 2ec1ce3661099eb73134d36eb8a4437938b304e9 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sun, 6 Oct 2024 20:15:47 +0900 Subject: [PATCH] Drop dependency on netifaces The netifaces library was abandoned and archived. Replace it by own parse logic from proc files + psutil. Closes-Bug: #2071596 Change-Id: I334e10b869694eaa8c6afd842ce8d4dc606a4f5b --- oslo_utils/netutils.py | 50 ++++++++++++------ oslo_utils/tests/test_netutils.py | 86 +++++++++++++++++++++++-------- requirements.txt | 2 +- 3 files changed, 99 insertions(+), 39 deletions(-) diff --git a/oslo_utils/netutils.py b/oslo_utils/netutils.py index c01bb7b4..a7bc60a3 100644 --- a/oslo_utils/netutils.py +++ b/oslo_utils/netutils.py @@ -27,7 +27,7 @@ from urllib import parse import netaddr from netaddr.core import INET_ATON from netaddr.core import INET_PTON -import netifaces +import psutil from oslo_utils._i18n import _ @@ -417,16 +417,23 @@ def _get_my_ipv4_address(): """Figure out the best ipv4 """ LOCALHOST = '127.0.0.1' - gtw = netifaces.gateways() - try: - interface = gtw['default'][netifaces.AF_INET][1] - except (KeyError, IndexError): - LOG.info('Could not determine default network interface, ' - 'using 127.0.0.1 for IPv4 address') - return LOCALHOST + interface = None + + with open('/proc/net/route') as routes: + for route in routes: + route_attrs = route.strip().split('\t') + if route_attrs[1] == '00000000': + interface = route_attrs[0] + break + else: + LOG.info('Could not determine default network interface, ' + 'using %s for IPv4 address', LOCALHOST) + return LOCALHOST try: - return netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr'] + addrs = psutil.net_if_addrs()[interface] + v4addrs = [addr for addr in addrs if addr.family == socket.AF_INET] + return v4addrs[0].address except (KeyError, IndexError): LOG.info('Could not determine IPv4 address for interface %s, ' 'using 127.0.0.1', @@ -461,16 +468,25 @@ def _get_my_ipv6_address(): """Figure out the best IPv6 address """ LOCALHOST = '::1' - gtw = netifaces.gateways() - try: - interface = gtw['default'][netifaces.AF_INET6][1] - except (KeyError, IndexError): - LOG.info('Could not determine default network interface, ' - 'using %s for IPv6 address', LOCALHOST) - return LOCALHOST + interface = None + ZERO_ADDRESS = '00000000000000000000000000000000' + + with open('/proc/net/ipv6_route') as routes: + for route in routes: + route_attrs = route.strip().split(' ') + if ((route_attrs[0], route_attrs[1]) == (ZERO_ADDRESS, '00') and + (route_attrs[2], route_attrs[3]) == (ZERO_ADDRESS, '00')): + interface = route_attrs[-1] + break + else: + LOG.info('Could not determine default network interface, ' + 'using %s for IPv6 address', LOCALHOST) + return LOCALHOST try: - return netifaces.ifaddresses(interface)[netifaces.AF_INET6][0]['addr'] + addrs = psutil.net_if_addrs()[interface] + v6addrs = [addr for addr in addrs if addr.family == socket.AF_INET6] + return v6addrs[0].address except (KeyError, IndexError): LOG.info('Could not determine IPv6 address for interface ' '%(interface)s, using %(address)s', diff --git a/oslo_utils/tests/test_netutils.py b/oslo_utils/tests/test_netutils.py index da3d425d..b366eef3 100644 --- a/oslo_utils/tests/test_netutils.py +++ b/oslo_utils/tests/test_netutils.py @@ -13,13 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +from collections import namedtuple import contextlib import io import socket from unittest import mock import netaddr -import netifaces from oslotest import base as test_base from oslo_utils import netutils @@ -358,31 +358,75 @@ class NetworkUtilsTest(test_base.BaseTestCase): addr = netutils.get_my_ipv6() self.assertEqual(addr, '2001:db8::2') - @mock.patch('netifaces.gateways') - @mock.patch('netifaces.ifaddresses') - def test_get_my_ip_address_with_default_route( - self, ifaddr, gateways): - ifaddr.return_value = {netifaces.AF_INET: [{'addr': '172.18.204.1'}], - netifaces.AF_INET6: [{'addr': '2001:db8::2'}]} - self.assertEqual('172.18.204.1', netutils._get_my_ipv4_address()) + @mock.patch('builtins.open') + @mock.patch('psutil.net_if_addrs') + def test_get_my_ipv4_address_with_default_route( + self, mock_ifaddrs, mock_open): + mock_open.return_value = io.StringIO( + """Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT +eth0 00000000 01cc12ac 0003 0 0 600 00000000 0 0 0 +eth0 00cc12ac 00000000 0001 0 0 600 00FFFFFF 0 0 0 +eth1 00cd12ac 00000000 0001 0 0 600 00FFFFFF 0 0 0""") # noqa : E501 + + addr = namedtuple('addr', ['family', 'address']) + mock_ifaddrs.return_value = { + 'eth0': [ + addr(family=socket.AF_INET, address='172.18.204.2'), + addr(family=socket.AF_INET6, address='2001:db8::2') + ], + 'eth1': [ + addr(family=socket.AF_INET, address='172.18.205.2'), + addr(family=socket.AF_INET6, address='2001:db8::1000::2') + ]} + self.assertEqual('172.18.204.2', netutils._get_my_ipv4_address()) + mock_open.assert_called_once_with('/proc/net/route') + + @mock.patch('builtins.open') + @mock.patch('psutil.net_if_addrs') + def test_get_my_ipv6_address_with_default_route( + self, mock_ifaddrs, mock_open): + mock_open.return_value = io.StringIO( + """00000000000000000000000000000000 00 00000000000000000000000000000000 00 20010db8000000000000000000000001 00000000 00000000 00000000 08000000 eth0 +20010db8000000000000000000000000 31 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000000 00000000 08000000 eth0 +20010db8100000000000000000000000 31 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000000 00000000 08000000 eth1""") # noqa: E501 + + addr = namedtuple('addr', ['family', 'address']) + mock_ifaddrs.return_value = { + 'eth0': [ + addr(family=socket.AF_INET, address='172.18.204.2'), + addr(family=socket.AF_INET6, address='2001:db8::2') + ], + 'eth1': [ + addr(family=socket.AF_INET, address='172.18.205.2'), + addr(family=socket.AF_INET6, address='2001:db8::1000::2') + ]} self.assertEqual('2001:db8::2', netutils._get_my_ipv6_address()) + mock_open.assert_called_once_with('/proc/net/ipv6_route') - @mock.patch('netifaces.gateways') - @mock.patch('netifaces.ifaddresses') - def test_get_my_ip_address_without_default_route( - self, ifaddr, gateways): - ifaddr.return_value = {} - self.assertEqual('127.0.0.1', netutils._get_my_ipv4_address()) - self.assertEqual('::1', netutils._get_my_ipv6_address()) + @mock.patch('builtins.open') + @mock.patch('psutil.net_if_addrs') + def test_get_my_ipv4_address_without_default_route( + self, mock_ifaddrs, mock_open): + mock_open.return_value = io.StringIO( + """Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT +eth0 00cc12ac 00000000 0001 0 0 600 00FFFFFF 0 0 0 +eth1 00cd12ac 00000000 0001 0 0 600 00FFFFFF 0 0 0""") # noqa : E501 - @mock.patch('netifaces.gateways') - @mock.patch('netifaces.ifaddresses') - def test_get_my_ipv4_address_without_default_interface( - self, ifaddr, gateways): - gateways.return_value = {} self.assertEqual('127.0.0.1', netutils._get_my_ipv4_address()) + mock_open.assert_called_once_with('/proc/net/route') + mock_ifaddrs.assert_not_called() + + @mock.patch('builtins.open') + @mock.patch('psutil.net_if_addrs') + def test_get_my_ipv6_address_without_default_route( + self, mock_ifaddrs, mock_open): + mock_open.return_value = io.StringIO( + """20010db8000000000000000000000000 31 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000000 00000000 08000000 eth0 +20010db8100000000000000000000000 31 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000000 00000000 08000000 eth1""") # noqa: E501 + self.assertEqual('::1', netutils._get_my_ipv6_address()) - self.assertFalse(ifaddr.called) + mock_open.assert_called_once_with('/proc/net/ipv6_route') + mock_ifaddrs.assert_not_called() class IPv6byEUI64TestCase(test_base.BaseTestCase): diff --git a/requirements.txt b/requirements.txt index 2ca05465..5c0c83fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,9 @@ iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 netaddr>=0.10.0 # BSD -netifaces>=0.10.4 # MIT debtcollector>=1.2.0 # Apache-2.0 pyparsing>=2.1.0 # MIT packaging>=20.4 # BSD tzdata>=2022.4 # MIT PyYAML>=3.13 # MIT +psutil>=3.2.2 # BST