From 956e9c85b8a1dc0a973c67eb9f247d714d5c8318 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Mon, 24 Jan 2022 15:31:23 -0600 Subject: [PATCH 1/7] Add json parsing of ip addr show When obtaining information from "ip addr", default to using "ip --json addr" rather than using regex to parse "ip addr show" as json is machine readable as less prone to error. --- cloudinit/netinfo.py | 76 ++++++++- tests/data/netinfo/sample-ipaddrshow-json | 90 ++++++++++ .../data/netinfo/sample-ipaddrshow-json-down | 57 +++++++ tests/unittests/test_netinfo.py | 159 ++++++++++-------- 4 files changed, 304 insertions(+), 78 deletions(-) create mode 100644 tests/data/netinfo/sample-ipaddrshow-json create mode 100644 tests/data/netinfo/sample-ipaddrshow-json-down diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 74e6b35a327..b855bc3aa37 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -8,8 +8,10 @@ # # This file is part of cloud-init. See LICENSE file for license information. +import json import re from copy import copy, deepcopy +from ipaddress import IPv4Network from cloudinit import log as logging from cloudinit import subp, util @@ -18,13 +20,74 @@ LOG = logging.getLogger() - +# Example netdev format: +# {'eth0': {'hwaddr': '00:16:3e:16:db:54', +# 'ipv4': [{'bcast': '10.85.130.255', +# 'ip': '10.85.130.116', +# 'mask': '255.255.255.0', +# 'scope': 'global'}], +# 'ipv6': [{'ip': 'fd42:baa2:3dd:17a:216:3eff:fe16:db54/64', +# 'scope6': 'global'}, +# {'ip': 'fe80::216:3eff:fe16:db54/64', 'scope6': 'link'}], +# 'up': True}, +# 'lo': {'hwaddr': '', +# 'ipv4': [{'bcast': '', +# 'ip': '127.0.0.1', +# 'mask': '255.0.0.0', +# 'scope': 'host'}], +# 'ipv6': [{'ip': '::1/128', 'scope6': 'host'}], +# 'up': True}} DEFAULT_NETDEV_INFO = {"ipv4": [], "ipv6": [], "hwaddr": "", "up": False} +def _netdev_info_iproute_json(ipaddr_json): + """Get network device dicts from ip route and ip link info. + + ipaddr_out: Output string from 'ip --json addr' command. + + Returns a dict of device info keyed by network device name containing + device configuration values. + + Raises json.JSONDecodeError if json could not be decoded + """ + ipaddr_data = json.loads(ipaddr_json) + devs = {} + + for dev in ipaddr_data: + flags = dev["flags"] + dev_info = { + "hwaddr": dev["address"] if dev["link_type"] == "ether" else "", + "up": bool("UP" in flags and "LOWER_UP" in flags), + "ipv4": [], + "ipv6": [], + } + for addr in dev["addr_info"]: + if addr["family"] == "inet": + parsed_addr = { + "ip": addr["local"], + "mask": str( + IPv4Network(f'0.0.0.0/{addr["prefixlen"]}').netmask + ), + "bcast": ( + addr["broadcast"] if "broadcast" in addr else "" + ), + "scope": addr["scope"], + } + dev_info["ipv4"].append(parsed_addr) + elif addr["family"] == "inet6": + parsed_addr = { + "ip": f"{addr['local']}/{addr['prefixlen']}", + "scope6": addr["scope"], + } + dev_info["ipv6"].append(parsed_addr) + devs[dev["ifname"]] = dev_info + return devs + + def _netdev_info_iproute(ipaddr_out): """ - Get network device dicts from ip route and ip link info. + DEPRECATED: Only used on distros that don't support ip json output + Use _netdev_info_iproute_json() when possible. @param ipaddr_out: Output string from 'ip addr show' command. @@ -209,8 +272,13 @@ def netdev_info(empty=""): devs = _netdev_info_ifconfig_netbsd(ifcfg_out) elif subp.which("ip"): # Try iproute first of all - (ipaddr_out, _err) = subp.subp(["ip", "addr", "show"]) - devs = _netdev_info_iproute(ipaddr_out) + try: + (ipaddr_out, _err) = subp.subp(["ip", "--json", "addr"]) + devs = _netdev_info_iproute_json(ipaddr_out) + except subp.ProcessExecutionError: + # Can be removed when "ip --json" is available everywhere + (ipaddr_out, _err) = subp.subp(["ip", "addr", "show"]) + devs = _netdev_info_iproute(ipaddr_out) elif subp.which("ifconfig"): # Fall back to net-tools if iproute2 is not present (ifcfg_out, _err) = subp.subp(["ifconfig", "-a"], rcs=[0, 1]) diff --git a/tests/data/netinfo/sample-ipaddrshow-json b/tests/data/netinfo/sample-ipaddrshow-json new file mode 100644 index 00000000000..f64afba970f --- /dev/null +++ b/tests/data/netinfo/sample-ipaddrshow-json @@ -0,0 +1,90 @@ +[ + { + "ifindex": 1, + "ifname": "lo", + "flags": [ + "LOOPBACK", + "UP", + "LOWER_UP" + ], + "mtu": 65536, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "group": "default", + "txqlen": 1000, + "link_type": "loopback", + "address": "00:00:00:00:00:00", + "broadcast": "00:00:00:00:00:00", + "addr_info": [ + { + "family": "inet", + "local": "127.0.0.1", + "prefixlen": 8, + "scope": "host", + "label": "lo", + "valid_life_time": 4294967295, + "preferred_life_time": 4294967295 + }, + { + "family": "inet6", + "local": "::1", + "prefixlen": 128, + "scope": "host", + "valid_life_time": 4294967295, + "preferred_life_time": 4294967295 + } + ] + }, + { + "ifindex": 23, + "link_index": 24, + "ifname": "enp0s25", + "flags": [ + "BROADCAST", + "MULTICAST", + "UP", + "LOWER_UP" + ], + "mtu": 1500, + "qdisc": "noqueue", + "operstate": "UP", + "group": "default", + "txqlen": 1000, + "link_type": "ether", + "address": "50:7b:9d:2c:af:91", + "broadcast": "ff:ff:ff:ff:ff:ff", + "link_netnsid": 0, + "addr_info": [ + { + "family": "inet", + "local": "192.168.2.18", + "prefixlen": 24, + "broadcast": "192.168.2.255", + "scope": "global", + "dynamic": true, + "label": "enp0s25", + "valid_life_time": 2339, + "preferred_life_time": 2339 + }, + { + "family": "inet6", + "local": "fe80::7777:2222:1111:eeee", + "prefixlen": 64, + "scope": "global", + "dynamic": true, + "mngtmpaddr": true, + "noprefixroute": true, + "valid_life_time": 6823, + "preferred_life_time": 3223 + }, + { + "family": "inet6", + "local": "fe80::8107:2b92:867e:f8a6", + "prefixlen": 64, + "scope": "link", + "valid_life_time": 4294967295, + "preferred_life_time": 4294967295 + } + ] + } +] diff --git a/tests/data/netinfo/sample-ipaddrshow-json-down b/tests/data/netinfo/sample-ipaddrshow-json-down new file mode 100644 index 00000000000..7ad5dde021f --- /dev/null +++ b/tests/data/netinfo/sample-ipaddrshow-json-down @@ -0,0 +1,57 @@ +[ + { + "ifindex": 1, + "ifname": "lo", + "flags": [ + "LOOPBACK", + "UP", + "LOWER_UP" + ], + "mtu": 65536, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "group": "default", + "txqlen": 1000, + "link_type": "loopback", + "address": "00:00:00:00:00:00", + "broadcast": "00:00:00:00:00:00", + "addr_info": [ + { + "family": "inet", + "local": "127.0.0.1", + "prefixlen": 8, + "scope": "host", + "label": "lo", + "valid_life_time": 4294967295, + "preferred_life_time": 4294967295 + }, + { + "family": "inet6", + "local": "::1", + "prefixlen": 128, + "scope": "host", + "valid_life_time": 4294967295, + "preferred_life_time": 4294967295 + } + ] + }, + { + "ifindex": 23, + "link_index": 24, + "ifname": "eth0", + "flags": [ + "BROADCAST", + "MULTICAST" + ], + "mtu": 1500, + "qdisc": "noqueue", + "operstate": "DOWN", + "group": "default", + "txqlen": 1000, + "link_type": "ether", + "address": "00:16:3e:de:51:a6", + "broadcast": "ff:ff:ff:ff:ff:ff", + "link_netnsid": 0, + "addr_info": [] + } +] diff --git a/tests/unittests/test_netinfo.py b/tests/unittests/test_netinfo.py index 5ed15729742..c9614f2ef63 100644 --- a/tests/unittests/test_netinfo.py +++ b/tests/unittests/test_netinfo.py @@ -4,14 +4,18 @@ from copy import copy +import pytest + +from cloudinit import subp 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_JSON = readResource("netinfo/sample-ipaddrshow-json") 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) + assert NETDEV_FORMATTED_OUT == content @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") @@ -54,13 +54,19 @@ 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,is_json", + [(SAMPLE_IPADDRSHOW_OUT, False), (SAMPLE_IPADDRSHOW_JSON, True)], + ) @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") - def test_netdev_iproute_pformat(self, m_subp, m_which): - """netdev_pformat properly rendering ip route info.""" - m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, "") + def test_netdev_iproute_pformat(self, m_subp, m_which, resource, is_json): + """netdev_pformat properly rendering ip route info (non json).""" + m_subp.return_value = (resource, "") + if not is_json: + m_subp.side_effect = [subp.ProcessExecutionError, (resource, "")] m_which.side_effect = lambda x: x if x == "ip" else None content = netdev_pformat() new_output = copy(NETDEV_FORMATTED_OUT) @@ -70,19 +76,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,57 +101,62 @@ 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, + }, + } + @pytest.mark.parametrize( + "resource,is_json", + [ + ("netinfo/sample-ipaddrshow-output-down", False), + ("netinfo/sample-ipaddrshow-json-down", True), + ], + ) @mock.patch("cloudinit.netinfo.subp.which") @mock.patch("cloudinit.netinfo.subp.subp") - def test_netdev_info_iproute_down(self, m_subp, m_which): + def test_netdev_info_iproute_down( + self, m_subp, m_which, resource, is_json + ): """Test netdev_info with ip and down interfaces.""" - m_subp.return_value = ( - readResource("netinfo/sample-ipaddrshow-output-down"), - "", - ) + m_subp.return_value = (readResource(resource), "") + if not is_json: + m_subp.side_effect = [ + subp.ProcessExecutionError, + (readResource(resource), ""), + ] 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 +177,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 +197,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 +215,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 a3093667ca049ffffe74a49b7abaca081a08a0fc Mon Sep 17 00:00:00 2001 From: James Falcon Date: Tue, 25 Jan 2022 17:07:33 -0600 Subject: [PATCH 2/7] be defensive with keys --- cloudinit/netinfo.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index b855bc3aa37..085e31d8ce5 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -43,7 +43,7 @@ def _netdev_info_iproute_json(ipaddr_json): """Get network device dicts from ip route and ip link info. - ipaddr_out: Output string from 'ip --json addr' command. + ipaddr_json: Output string from 'ip --json addr' command. Returns a dict of device info keyed by network device name containing device configuration values. @@ -54,29 +54,34 @@ def _netdev_info_iproute_json(ipaddr_json): devs = {} for dev in ipaddr_data: - flags = dev["flags"] + flags = dev["flags"] if "flags" in dev else [] + address = dev["address"] if dev.get("link_type") == "ether" else "" dev_info = { - "hwaddr": dev["address"] if dev["link_type"] == "ether" else "", + "hwaddr": address, "up": bool("UP" in flags and "LOWER_UP" in flags), "ipv4": [], "ipv6": [], } - for addr in dev["addr_info"]: - if addr["family"] == "inet": + for addr in dev.get("addr_info", []): + if addr.get("family") == "inet": + mask = ( + str(IPv4Network(f'0.0.0.0/{addr["prefixlen"]}').netmask) + if "prefixlen" in addr + else "" + ) parsed_addr = { - "ip": addr["local"], - "mask": str( - IPv4Network(f'0.0.0.0/{addr["prefixlen"]}').netmask - ), - "bcast": ( - addr["broadcast"] if "broadcast" in addr else "" - ), - "scope": addr["scope"], + "ip": addr.get("local", ""), + "mask": mask, + "bcast": addr.get("broadcast", ""), + "scope": addr.get("scope", ""), } dev_info["ipv4"].append(parsed_addr) elif addr["family"] == "inet6": + ip = addr.get("local", "") + if ip: + ip = f"{ip}/{addr.get('prefixlen', 64)}" parsed_addr = { - "ip": f"{addr['local']}/{addr['prefixlen']}", + "ip": ip, "scope6": addr["scope"], } dev_info["ipv6"].append(parsed_addr) From a135cf4aa8f814f358785fb8f436f58bc79036ec Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 26 Jan 2022 12:48:59 -0600 Subject: [PATCH 3/7] ignore mask for ipv6 point-to-point --- cloudinit/netinfo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 085e31d8ce5..9b4df63eb10 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -78,7 +78,12 @@ def _netdev_info_iproute_json(ipaddr_json): dev_info["ipv4"].append(parsed_addr) elif addr["family"] == "inet6": ip = addr.get("local", "") - if ip: + # address here refers to a peer address, and according + # to "man 8 ip-address": + # If a peer address is specified, the local address cannot + # have a prefix length. The network prefix is associated + # with the peer rather than with the local address. + if ip and not addr.get("address"): ip = f"{ip}/{addr.get('prefixlen', 64)}" parsed_addr = { "ip": ip, From a135f2ef9c0d6e29a73405a77f36818fdc429cee Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 26 Jan 2022 14:02:37 -0600 Subject: [PATCH 4/7] update regexes we know are broken (hopefully for the last time) --- cloudinit/netinfo.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 9b4df63eb10..68215e807ad 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -120,7 +120,10 @@ def _netdev_info_iproute(ipaddr_out): } elif "inet6" in line: m = re.match( - r"\s+inet6\s(?P\S+)\sscope\s(?P\S+).*", line + r"\s+inet6\s(?P\S+)" + r"\s(peer\s\S+)?" + r"\sscope\s(?P\S+).*", + line, ) if not m: LOG.warning( @@ -130,8 +133,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: From cb28c05855b7180d30ae38e6764dcffb680cdc26 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 26 Jan 2022 14:09:15 -0600 Subject: [PATCH 5/7] Add metric to netinfo test samples --- tests/data/netinfo/sample-ipaddrshow-json | 1 + tests/data/netinfo/sample-ipaddrshow-output | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/netinfo/sample-ipaddrshow-json b/tests/data/netinfo/sample-ipaddrshow-json index f64afba970f..8f6a430c483 100644 --- a/tests/data/netinfo/sample-ipaddrshow-json +++ b/tests/data/netinfo/sample-ipaddrshow-json @@ -59,6 +59,7 @@ "family": "inet", "local": "192.168.2.18", "prefixlen": 24, + "metric": 100, "broadcast": "192.168.2.255", "scope": "global", "dynamic": true, diff --git a/tests/data/netinfo/sample-ipaddrshow-output b/tests/data/netinfo/sample-ipaddrshow-output index b2fa2672414..2aa3f90ca26 100644 --- a/tests/data/netinfo/sample-ipaddrshow-output +++ b/tests/data/netinfo/sample-ipaddrshow-output @@ -4,10 +4,9 @@ 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 brd 192.168.2.255 scope global dynamic enp0s25 + 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 - From 89fd66451d56ca5be0498a5f6862d0cc7c35dd2d Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 26 Jan 2022 14:24:28 -0600 Subject: [PATCH 6/7] regex is hard --- cloudinit/netinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 68215e807ad..20bd0bb0339 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -121,7 +121,7 @@ def _netdev_info_iproute(ipaddr_out): elif "inet6" in line: m = re.match( r"\s+inet6\s(?P\S+)" - r"\s(peer\s\S+)?" + r"(\s(peer\s\S+))?" r"\sscope\s(?P\S+).*", line, ) From ab6bb93e8b9b18e5732fc84ddee0b9dd7b938824 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 26 Jan 2022 19:10:54 -0600 Subject: [PATCH 7/7] additional test and fix found by test --- cloudinit/netinfo.py | 2 +- tests/unittests/test_netinfo.py | 120 +++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 20bd0bb0339..5eeeb9675b3 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -87,7 +87,7 @@ def _netdev_info_iproute_json(ipaddr_json): ip = f"{ip}/{addr.get('prefixlen', 64)}" parsed_addr = { "ip": ip, - "scope6": addr["scope"], + "scope6": addr.get("scope", ""), } dev_info["ipv6"].append(parsed_addr) devs[dev["ifname"]] = dev_info diff --git a/tests/unittests/test_netinfo.py b/tests/unittests/test_netinfo.py index c9614f2ef63..aecce921490 100644 --- a/tests/unittests/test_netinfo.py +++ b/tests/unittests/test_netinfo.py @@ -2,12 +2,18 @@ """Tests netinfo module functions and classes.""" +import json from copy import copy import pytest from cloudinit import subp -from cloudinit.netinfo import netdev_info, netdev_pformat, route_pformat +from cloudinit.netinfo import ( + _netdev_info_iproute_json, + netdev_info, + netdev_pformat, + route_pformat, +) from tests.unittests.helpers import mock, readResource # Example ifconfig and route output @@ -231,5 +237,117 @@ def test_route_warn_on_missing_commands(self, m_subp, m_which, caplog): ) m_subp.assert_not_called() + @pytest.mark.parametrize( + "input,expected", + [ + # Test hwaddr set when link_type is ether, + # Test up True when flags contains UP and LOWER_UP + ( + [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "00:00:00:00:00:00", + "flags": ["LOOPBACK", "UP", "LOWER_UP"], + } + ], + { + "eth0": { + "hwaddr": "00:00:00:00:00:00", + "ipv4": [], + "ipv6": [], + "up": True, + } + }, + ), + # Test hwaddr not set when link_type is not ether + # Test up False when flags does not contain both UP and LOWER_UP + ( + [ + { + "ifname": "eth0", + "link_type": "none", + "address": "00:00:00:00:00:00", + "flags": ["LOOPBACK", "UP"], + } + ], + { + "eth0": { + "hwaddr": "", + "ipv4": [], + "ipv6": [], + "up": False, + } + }, + ), + ( + [ + { + "ifname": "eth0", + "addr_info": [ + # Test for ipv4: + # ip set correctly + # mask set correctly + # bcast set correctly + # scope set correctly + { + "family": "inet", + "local": "10.0.0.1", + "broadcast": "10.0.0.255", + "prefixlen": 24, + "scope": "global", + }, + # Test for ipv6: + # ip set correctly + # mask set correctly when no 'address' present + # scope6 set correctly + { + "family": "inet6", + "local": "fd12:3456:7890:1234::5678:9012", + "prefixlen": 64, + "scope": "global", + }, + # Test for ipv6: + # mask not set when 'address' present + { + "family": "inet6", + "local": "fd12:3456:7890:1234::5678:9012", + "address": "fd12:3456:7890:1234::1", + "prefixlen": 64, + }, + ], + } + ], + { + "eth0": { + "hwaddr": "", + "ipv4": [ + { + "ip": "10.0.0.1", + "mask": "255.255.255.0", + "bcast": "10.0.0.255", + "scope": "global", + } + ], + "ipv6": [ + { + "ip": "fd12:3456:7890:1234::5678:9012/64", + "scope6": "global", + }, + { + "ip": "fd12:3456:7890:1234::5678:9012", + "scope6": "", + }, + ], + "up": False, + } + }, + ), + ], + ) + def test_netdev_info_iproute_json(self, input, expected): + out = _netdev_info_iproute_json(json.dumps(input)) + assert out == expected + # vi: ts=4 expandtab