From aeb93b63efdb8a37f2e26a03cab0848566528571 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 11 Jan 2022 10:55:55 -0600 Subject: [PATCH 1/2] Update parsing of ip addr show Newer versions of "ip addr show" include a metric on the inet line. Parse this while also keeping backwards compatibility. --- cloudinit/netinfo.py | 6 +- .../netinfo/sample-ipaddrshow-output-metric | 12 ++ tests/unittests/test_netinfo.py | 135 +++++++++--------- 3 files changed, 82 insertions(+), 71 deletions(-) create mode 100644 tests/data/netinfo/sample-ipaddrshow-output-metric diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 74e6b35a327..0d7538bb761 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -57,8 +57,10 @@ def _netdev_info_iproute(ipaddr_out): devs[dev_name]["ipv6"].append(m.groupdict()) elif "inet" in line: m = re.match( - r"\s+inet\s(?P\S+)(\sbrd\s(?P\S+))?\sscope\s" - r"(?P\S+).*", + r"\s+inet\s(?P\S+)" + r"(\smetric\s(?P\d+))?" + r"(\sbrd\s(?P\S+))?" + r"\sscope\s(?P\S+).*", line, ) if not m: diff --git a/tests/data/netinfo/sample-ipaddrshow-output-metric b/tests/data/netinfo/sample-ipaddrshow-output-metric new file mode 100644 index 00000000000..2aa3f90ca26 --- /dev/null +++ b/tests/data/netinfo/sample-ipaddrshow-output-metric @@ -0,0 +1,12 @@ +1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo\ valid_lft forever preferred_lft forever + inet6 ::1/128 scope host \ valid_lft forever preferred_lft forever +2: enp0s25: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 + link/ether 50:7b:9d:2c:af:91 brd ff:ff:ff:ff:ff:ff + inet 192.168.2.18/24 metric 100 brd 192.168.2.255 scope global dynamic enp0s25 + valid_lft 84174sec preferred_lft 84174sec + inet6 fe80::7777:2222:1111:eeee/64 scope global + valid_lft forever preferred_lft forever + inet6 fe80::8107:2b92:867e:f8a6/64 scope link + valid_lft forever preferred_lft forever diff --git a/tests/unittests/test_netinfo.py b/tests/unittests/test_netinfo.py index 5ed15729742..5f80d401344 100644 --- a/tests/unittests/test_netinfo.py +++ b/tests/unittests/test_netinfo.py @@ -1,17 +1,21 @@ # This file is part of cloud-init. See LICENSE file for license information. """Tests netinfo module functions and classes.""" - from copy import copy +import pytest + from cloudinit.netinfo import netdev_info, netdev_pformat, route_pformat -from tests.unittests.helpers import CiTestCase, mock, readResource +from tests.unittests.helpers import mock, readResource # Example ifconfig and route output SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output") SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output") SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output") SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output") +SAMPLE_IPADDRSHOW_OUT_METRIC = readResource( + "netinfo/sample-ipaddrshow-output-metric" +) SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4") SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6") SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4") @@ -21,11 +25,7 @@ FREEBSD_NETDEV_OUT = readResource("netinfo/freebsd-netdev-formatted-output") -class TestNetInfo(CiTestCase): - - maxDiff = None - with_logs = True - +class TestNetInfo: @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") def test_netdev_old_nettools_pformat(self, m_subp, m_which): @@ -33,7 +33,7 @@ def test_netdev_old_nettools_pformat(self, m_subp, m_which): m_subp.return_value = (SAMPLE_OLD_IFCONFIG_OUT, "") m_which.side_effect = lambda x: x if x == "ifconfig" else None content = netdev_pformat() - self.assertEqual(NETDEV_FORMATTED_OUT, content) + assert NETDEV_FORMATTED_OUT == content @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") @@ -42,7 +42,7 @@ def test_netdev_new_nettools_pformat(self, m_subp, m_which): m_subp.return_value = (SAMPLE_NEW_IFCONFIG_OUT, "") m_which.side_effect = lambda x: x if x == "ifconfig" else None content = netdev_pformat() - self.assertEqual(NETDEV_FORMATTED_OUT, content) + NETDEV_FORMATTED_OUT == content @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") @@ -54,13 +54,16 @@ def test_netdev_freebsd_nettools_pformat(self, m_subp, m_which): print() print(content) print() - self.assertEqual(FREEBSD_NETDEV_OUT, content) + assert FREEBSD_NETDEV_OUT == content + @pytest.mark.parametrize( + "resource", [SAMPLE_IPADDRSHOW_OUT, SAMPLE_IPADDRSHOW_OUT_METRIC] + ) @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") - def test_netdev_iproute_pformat(self, m_subp, m_which): + def test_netdev_iproute_pformat(self, m_subp, m_which, resource): """netdev_pformat properly rendering ip route info.""" - m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, "") + m_subp.return_value = (resource, "") m_which.side_effect = lambda x: x if x == "ip" else None content = netdev_pformat() new_output = copy(NETDEV_FORMATTED_OUT) @@ -70,19 +73,19 @@ def test_netdev_iproute_pformat(self, m_subp, m_which): new_output = new_output.replace( "255.0.0.0 | . |", "255.0.0.0 | host |" ) - self.assertEqual(new_output, content) + assert new_output == content @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") - def test_netdev_warn_on_missing_commands(self, m_subp, m_which): + def test_netdev_warn_on_missing_commands(self, m_subp, m_which, caplog): """netdev_pformat warns when missing both ip and 'netstat'.""" m_which.return_value = None # Niether ip nor netstat found content = netdev_pformat() - self.assertEqual("\n", content) - self.assertEqual( - "WARNING: Could not print networks: missing 'ip' and 'ifconfig'" - " commands\n", - self.logs.getvalue(), + assert "\n" == content + log = caplog.records[0] + assert log.levelname == "WARNING" + assert log.msg == ( + "Could not print networks: missing 'ip' and 'ifconfig' commands" ) m_subp.assert_not_called() @@ -95,23 +98,20 @@ def test_netdev_info_nettools_down(self, m_subp, m_which): "", ) m_which.side_effect = lambda x: x if x == "ifconfig" else None - self.assertEqual( - { - "eth0": { - "ipv4": [], - "ipv6": [], - "hwaddr": "00:16:3e:de:51:a6", - "up": False, - }, - "lo": { - "ipv4": [{"ip": "127.0.0.1", "mask": "255.0.0.0"}], - "ipv6": [{"ip": "::1/128", "scope6": "host"}], - "hwaddr": ".", - "up": True, - }, + assert netdev_info(".") == { + "eth0": { + "ipv4": [], + "ipv6": [], + "hwaddr": "00:16:3e:de:51:a6", + "up": False, }, - netdev_info("."), - ) + "lo": { + "ipv4": [{"ip": "127.0.0.1", "mask": "255.0.0.0"}], + "ipv6": [{"ip": "::1/128", "scope6": "host"}], + "hwaddr": ".", + "up": True, + }, + } @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") @@ -122,30 +122,27 @@ def test_netdev_info_iproute_down(self, m_subp, m_which): "", ) m_which.side_effect = lambda x: x if x == "ip" else None - self.assertEqual( - { - "lo": { - "ipv4": [ - { - "ip": "127.0.0.1", - "bcast": ".", - "mask": "255.0.0.0", - "scope": "host", - } - ], - "ipv6": [{"ip": "::1/128", "scope6": "host"}], - "hwaddr": ".", - "up": True, - }, - "eth0": { - "ipv4": [], - "ipv6": [], - "hwaddr": "00:16:3e:de:51:a6", - "up": False, - }, + assert netdev_info(".") == { + "lo": { + "ipv4": [ + { + "ip": "127.0.0.1", + "bcast": ".", + "mask": "255.0.0.0", + "scope": "host", + } + ], + "ipv6": [{"ip": "::1/128", "scope6": "host"}], + "hwaddr": ".", + "up": True, }, - netdev_info("."), - ) + "eth0": { + "ipv4": [], + "ipv6": [], + "hwaddr": "00:16:3e:de:51:a6", + "up": False, + }, + } @mock.patch("cloudinit.netinfo.netdev_info") def test_netdev_pformat_with_down(self, m_netdev_info): @@ -166,9 +163,9 @@ def test_netdev_pformat_with_down(self, m_netdev_info): "up": False, }, } - self.assertEqual( - readResource("netinfo/netdev-formatted-output-down"), - netdev_pformat(), + assert ( + readResource("netinfo/netdev-formatted-output-down") + == netdev_pformat() ) @mock.patch("cloudinit.netinfo.subp.which") @@ -186,7 +183,7 @@ def subp_netstat_route_selector(*args, **kwargs): m_subp.side_effect = subp_netstat_route_selector m_which.side_effect = lambda x: x if x == "netstat" else None content = route_pformat() - self.assertEqual(ROUTE_FORMATTED_OUT, content) + assert ROUTE_FORMATTED_OUT == content @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") @@ -204,19 +201,19 @@ def subp_iproute_selector(*args, **kwargs): m_subp.side_effect = subp_iproute_selector m_which.side_effect = lambda x: x if x == "ip" else None content = route_pformat() - self.assertEqual(ROUTE_FORMATTED_OUT, content) + assert ROUTE_FORMATTED_OUT == content @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") - def test_route_warn_on_missing_commands(self, m_subp, m_which): + def test_route_warn_on_missing_commands(self, m_subp, m_which, caplog): """route_pformat warns when missing both ip and 'netstat'.""" m_which.return_value = None # Niether ip nor netstat found content = route_pformat() - self.assertEqual("\n", content) - self.assertEqual( - "WARNING: Could not print routes: missing 'ip' and 'netstat'" - " commands\n", - self.logs.getvalue(), + assert "\n" == content + log = caplog.records[0] + assert log.levelname == "WARNING" + assert log.msg == ( + "Could not print routes: missing 'ip' and 'netstat' commands" ) m_subp.assert_not_called() From 4963ea69e2333dd414105bfe350d48707399cd79 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 11 Jan 2022 11:17:23 -0600 Subject: [PATCH 2/2] thanks pylint --- tests/unittests/test_netinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/test_netinfo.py b/tests/unittests/test_netinfo.py index 5f80d401344..e62852053e2 100644 --- a/tests/unittests/test_netinfo.py +++ b/tests/unittests/test_netinfo.py @@ -42,7 +42,7 @@ def test_netdev_new_nettools_pformat(self, m_subp, m_which): m_subp.return_value = (SAMPLE_NEW_IFCONFIG_OUT, "") m_which.side_effect = lambda x: x if x == "ifconfig" else None content = netdev_pformat() - NETDEV_FORMATTED_OUT == content + assert NETDEV_FORMATTED_OUT == content @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp")