From bae9b11da9ed7dd0b16fe5adeaf4774b7cc628cf Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 15 Dec 2021 20:16:38 -0600 Subject: Adopt Black and isort (SC-700) (#1157) Applied Black and isort, fixed any linting issues, updated tox.ini and CI. --- cloudinit/netinfo.py | 403 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 238 insertions(+), 165 deletions(-) (limited to 'cloudinit/netinfo.py') diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 628e2908..74e6b35a 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -8,25 +8,18 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from copy import copy, deepcopy import re +from copy import copy, deepcopy from cloudinit import log as logging +from cloudinit import subp, util from cloudinit.net.network_state import net_prefix_to_ipv4_mask -from cloudinit import subp -from cloudinit import util - from cloudinit.simpletable import SimpleTable LOG = logging.getLogger() -DEFAULT_NETDEV_INFO = { - "ipv4": [], - "ipv6": [], - "hwaddr": "", - "up": False -} +DEFAULT_NETDEV_INFO = {"ipv4": [], "ipv6": [], "hwaddr": "", "up": False} def _netdev_info_iproute(ipaddr_out): @@ -42,51 +35,63 @@ def _netdev_info_iproute(ipaddr_out): devs = {} dev_name = None for num, line in enumerate(ipaddr_out.splitlines()): - m = re.match(r'^\d+:\s(?P[^:]+):\s+<(?P\S+)>\s+.*', line) + m = re.match(r"^\d+:\s(?P[^:]+):\s+<(?P\S+)>\s+.*", line) if m: - dev_name = m.group('dev').lower().split('@')[0] - flags = m.group('flags').split(',') + dev_name = m.group("dev").lower().split("@")[0] + flags = m.group("flags").split(",") devs[dev_name] = { - 'ipv4': [], 'ipv6': [], 'hwaddr': '', - 'up': bool('UP' in flags and 'LOWER_UP' in flags), + "ipv4": [], + "ipv6": [], + "hwaddr": "", + "up": bool("UP" in flags and "LOWER_UP" in flags), } - elif 'inet6' in line: + elif "inet6" in line: m = re.match( - r'\s+inet6\s(?P\S+)\sscope\s(?P\S+).*', line) + r"\s+inet6\s(?P\S+)\sscope\s(?P\S+).*", line + ) if not m: LOG.warning( - 'Could not parse ip addr show: (line:%d) %s', num, line) + "Could not parse ip addr show: (line:%d) %s", num, line + ) continue - devs[dev_name]['ipv6'].append(m.groupdict()) - elif 'inet' in line: + 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+).*', line) + r"\s+inet\s(?P\S+)(\sbrd\s(?P\S+))?\sscope\s" + r"(?P\S+).*", + line, + ) if not m: LOG.warning( - 'Could not parse ip addr show: (line:%d) %s', num, line) + "Could not parse ip addr show: (line:%d) %s", num, line + ) continue match = m.groupdict() - cidr4 = match.pop('cidr4') - addr, _, prefix = cidr4.partition('/') + cidr4 = match.pop("cidr4") + addr, _, prefix = cidr4.partition("/") if not prefix: - prefix = '32' - devs[dev_name]['ipv4'].append({ - 'ip': addr, - 'bcast': match['bcast'] if match['bcast'] else '', - 'mask': net_prefix_to_ipv4_mask(prefix), - 'scope': match['scope']}) - elif 'link' in line: + prefix = "32" + devs[dev_name]["ipv4"].append( + { + "ip": addr, + "bcast": match["bcast"] if match["bcast"] else "", + "mask": net_prefix_to_ipv4_mask(prefix), + "scope": match["scope"], + } + ) + elif "link" in line: m = re.match( - r'\s+link/(?P\S+)\s(?P\S+).*', line) + r"\s+link/(?P\S+)\s(?P\S+).*", line + ) if not m: LOG.warning( - 'Could not parse ip addr show: (line:%d) %s', num, line) + "Could not parse ip addr show: (line:%d) %s", num, line + ) continue - if m.group('link_type') == 'ether': - devs[dev_name]['hwaddr'] = m.group('hwaddr') + if m.group("link_type") == "ether": + devs[dev_name]["hwaddr"] = m.group("hwaddr") else: - devs[dev_name]['hwaddr'] = '' + devs[dev_name]["hwaddr"] = "" else: continue return devs @@ -101,40 +106,41 @@ def _netdev_info_ifconfig_netbsd(ifconfig_data): if line[0] not in ("\t", " "): curdev = line.split()[0] # current ifconfig pops a ':' on the end of the device - if curdev.endswith(':'): + if curdev.endswith(":"): curdev = curdev[:-1] if curdev not in devs: devs[curdev] = deepcopy(DEFAULT_NETDEV_INFO) toks = line.lower().strip().split() if len(toks) > 1: if re.search(r"flags=[x\d]+", toks[1]): - devs[curdev]['up'] = True + devs[curdev]["up"] = True for i in range(len(toks)): if toks[i] == "inet": # Create new ipv4 addr entry - network, net_bits = toks[i + 1].split('/') - devs[curdev]['ipv4'].append( - {'ip': network, 'mask': net_prefix_to_ipv4_mask(net_bits)}) + network, net_bits = toks[i + 1].split("/") + devs[curdev]["ipv4"].append( + {"ip": network, "mask": net_prefix_to_ipv4_mask(net_bits)} + ) elif toks[i] == "broadcast": - devs[curdev]['ipv4'][-1]['bcast'] = toks[i + 1] + devs[curdev]["ipv4"][-1]["bcast"] = toks[i + 1] elif toks[i] == "address:": - devs[curdev]['hwaddr'] = toks[i + 1] + devs[curdev]["hwaddr"] = toks[i + 1] elif toks[i] == "inet6": if toks[i + 1] == "addr:": - devs[curdev]['ipv6'].append({'ip': toks[i + 2]}) + devs[curdev]["ipv6"].append({"ip": toks[i + 2]}) else: - devs[curdev]['ipv6'].append({'ip': toks[i + 1]}) + devs[curdev]["ipv6"].append({"ip": toks[i + 1]}) elif toks[i] == "prefixlen": # Add prefix to current ipv6 value - addr6 = devs[curdev]['ipv6'][-1]['ip'] + "/" + toks[i + 1] - devs[curdev]['ipv6'][-1]['ip'] = addr6 + addr6 = devs[curdev]["ipv6"][-1]["ip"] + "/" + toks[i + 1] + devs[curdev]["ipv6"][-1]["ip"] = addr6 elif toks[i].startswith("scope:"): - devs[curdev]['ipv6'][-1]['scope6'] = toks[i].lstrip("scope:") + devs[curdev]["ipv6"][-1]["scope6"] = toks[i].lstrip("scope:") elif toks[i] == "scopeid": - res = re.match(r'.*<(\S+)>', toks[i + 1]) + res = re.match(r".*<(\S+)>", toks[i + 1]) if res: - devs[curdev]['ipv6'][-1]['scope6'] = res.group(1) + devs[curdev]["ipv6"][-1]["scope6"] = res.group(1) else: - devs[curdev]['ipv6'][-1]['scope6'] = toks[i + 1] + devs[curdev]["ipv6"][-1]["scope6"] = toks[i + 1] return devs @@ -148,49 +154,50 @@ def _netdev_info_ifconfig(ifconfig_data): if line[0] not in ("\t", " "): curdev = line.split()[0] # current ifconfig pops a ':' on the end of the device - if curdev.endswith(':'): + if curdev.endswith(":"): curdev = curdev[:-1] if curdev not in devs: devs[curdev] = deepcopy(DEFAULT_NETDEV_INFO) toks = line.lower().strip().split() if toks[0] == "up": - devs[curdev]['up'] = True + devs[curdev]["up"] = True # If the output of ifconfig doesn't contain the required info in the # obvious place, use a regex filter to be sure. elif len(toks) > 1: if re.search(r"flags=\d+', toks[i + 1]) + res = re.match(r".*<(\S+)>", toks[i + 1]) if res: - devs[curdev]['ipv6'][-1]['scope6'] = res.group(1) + devs[curdev]["ipv6"][-1]["scope6"] = res.group(1) else: - devs[curdev]['ipv6'][-1]['scope6'] = toks[i + 1] + devs[curdev]["ipv6"][-1]["scope6"] = toks[i + 1] return devs @@ -200,17 +207,18 @@ def netdev_info(empty=""): if util.is_NetBSD(): (ifcfg_out, _err) = subp.subp(["ifconfig", "-a"], rcs=[0, 1]) devs = _netdev_info_ifconfig_netbsd(ifcfg_out) - elif subp.which('ip'): + elif subp.which("ip"): # Try iproute first of all (ipaddr_out, _err) = subp.subp(["ip", "addr", "show"]) devs = _netdev_info_iproute(ipaddr_out) - elif subp.which('ifconfig'): + elif subp.which("ifconfig"): # Fall back to net-tools if iproute2 is not present (ifcfg_out, _err) = subp.subp(["ifconfig", "-a"], rcs=[0, 1]) devs = _netdev_info_ifconfig(ifcfg_out) else: LOG.warning( - "Could not print networks: missing 'ip' and 'ifconfig' commands") + "Could not print networks: missing 'ip' and 'ifconfig' commands" + ) if empty == "": return devs @@ -219,7 +227,7 @@ def netdev_info(empty=""): def fill(data, new_val="", empty_vals=("", b"")): """Recursively replace 'empty_vals' in data (dict, tuple, list) - with new_val""" + with new_val""" if isinstance(data, dict): myiter = data.items() elif isinstance(data, (tuple, list)): @@ -249,46 +257,52 @@ def _netdev_route_info_iproute(iproute_data): """ routes = {} - routes['ipv4'] = [] - routes['ipv6'] = [] + routes["ipv4"] = [] + routes["ipv6"] = [] entries = iproute_data.splitlines() default_route_entry = { - 'destination': '', 'flags': '', 'gateway': '', 'genmask': '', - 'iface': '', 'metric': ''} + "destination": "", + "flags": "", + "gateway": "", + "genmask": "", + "iface": "", + "metric": "", + } for line in entries: entry = copy(default_route_entry) if not line: continue toks = line.split() - flags = ['U'] + flags = ["U"] if toks[0] == "default": - entry['destination'] = "0.0.0.0" - entry['genmask'] = "0.0.0.0" + entry["destination"] = "0.0.0.0" + entry["genmask"] = "0.0.0.0" else: - if '/' in toks[0]: + if "/" in toks[0]: (addr, cidr) = toks[0].split("/") else: addr = toks[0] - cidr = '32' + cidr = "32" flags.append("H") - entry['genmask'] = net_prefix_to_ipv4_mask(cidr) - entry['destination'] = addr - entry['genmask'] = net_prefix_to_ipv4_mask(cidr) - entry['gateway'] = "0.0.0.0" + entry["genmask"] = net_prefix_to_ipv4_mask(cidr) + entry["destination"] = addr + entry["genmask"] = net_prefix_to_ipv4_mask(cidr) + entry["gateway"] = "0.0.0.0" for i in range(len(toks)): if toks[i] == "via": - entry['gateway'] = toks[i + 1] + entry["gateway"] = toks[i + 1] flags.insert(1, "G") if toks[i] == "dev": entry["iface"] = toks[i + 1] if toks[i] == "metric": - entry['metric'] = toks[i + 1] - entry['flags'] = ''.join(flags) - routes['ipv4'].append(entry) + entry["metric"] = toks[i + 1] + entry["flags"] = "".join(flags) + routes["ipv4"].append(entry) try: (iproute_data6, _err6) = subp.subp( ["ip", "--oneline", "-6", "route", "list", "table", "all"], - rcs=[0, 1]) + rcs=[0, 1], + ) except subp.ProcessExecutionError: pass else: @@ -299,30 +313,30 @@ def _netdev_route_info_iproute(iproute_data): continue toks = line.split() if toks[0] == "default": - entry['destination'] = "::/0" - entry['flags'] = "UG" + entry["destination"] = "::/0" + entry["flags"] = "UG" else: - entry['destination'] = toks[0] - entry['gateway'] = "::" - entry['flags'] = "U" + entry["destination"] = toks[0] + entry["gateway"] = "::" + entry["flags"] = "U" for i in range(len(toks)): if toks[i] == "via": - entry['gateway'] = toks[i + 1] - entry['flags'] = "UG" + entry["gateway"] = toks[i + 1] + entry["flags"] = "UG" if toks[i] == "dev": entry["iface"] = toks[i + 1] if toks[i] == "metric": - entry['metric'] = toks[i + 1] + entry["metric"] = toks[i + 1] if toks[i] == "expires": - entry['flags'] = entry['flags'] + 'e' - routes['ipv6'].append(entry) + entry["flags"] = entry["flags"] + "e" + routes["ipv6"].append(entry) return routes def _netdev_route_info_netstat(route_data): routes = {} - routes['ipv4'] = [] - routes['ipv6'] = [] + routes["ipv4"] = [] + routes["ipv6"] = [] entries = route_data.splitlines() for line in entries: @@ -336,9 +350,14 @@ def _netdev_route_info_netstat(route_data): # Linux netstat shows 2 more: # Destination Gateway Genmask Flags Metric Ref Use Iface # 0.0.0.0 10.65.0.1 0.0.0.0 UG 0 0 0 eth0 - if (len(toks) < 6 or toks[0] == "Kernel" or - toks[0] == "Destination" or toks[0] == "Internet" or - toks[0] == "Internet6" or toks[0] == "Routing"): + if ( + len(toks) < 6 + or toks[0] == "Kernel" + or toks[0] == "Destination" + or toks[0] == "Internet" + or toks[0] == "Internet6" + or toks[0] == "Routing" + ): continue if len(toks) < 8: toks.append("-") @@ -346,20 +365,21 @@ def _netdev_route_info_netstat(route_data): toks[7] = toks[5] toks[5] = "-" entry = { - 'destination': toks[0], - 'gateway': toks[1], - 'genmask': toks[2], - 'flags': toks[3], - 'metric': toks[4], - 'ref': toks[5], - 'use': toks[6], - 'iface': toks[7], + "destination": toks[0], + "gateway": toks[1], + "genmask": toks[2], + "flags": toks[3], + "metric": toks[4], + "ref": toks[5], + "use": toks[6], + "iface": toks[7], } - routes['ipv4'].append(entry) + routes["ipv4"].append(entry) try: (route_data6, _err6) = subp.subp( - ["netstat", "-A", "inet6", "--route", "--numeric"], rcs=[0, 1]) + ["netstat", "-A", "inet6", "--route", "--numeric"], rcs=[0, 1] + ) except subp.ProcessExecutionError: pass else: @@ -368,44 +388,52 @@ def _netdev_route_info_netstat(route_data): if not line: continue toks = line.split() - if (len(toks) < 7 or toks[0] == "Kernel" or - toks[0] == "Destination" or toks[0] == "Internet" or - toks[0] == "Proto" or toks[0] == "Active"): + if ( + len(toks) < 7 + or toks[0] == "Kernel" + or toks[0] == "Destination" + or toks[0] == "Internet" + or toks[0] == "Proto" + or toks[0] == "Active" + ): continue entry = { - 'destination': toks[0], - 'gateway': toks[1], - 'flags': toks[2], - 'metric': toks[3], - 'ref': toks[4], - 'use': toks[5], - 'iface': toks[6], + "destination": toks[0], + "gateway": toks[1], + "flags": toks[2], + "metric": toks[3], + "ref": toks[4], + "use": toks[5], + "iface": toks[6], } # skip lo interface on ipv6 - if entry['iface'] == "lo": + if entry["iface"] == "lo": continue # strip /128 from address if it's included - if entry['destination'].endswith('/128'): - entry['destination'] = re.sub( - r'\/128$', '', entry['destination']) - routes['ipv6'].append(entry) + if entry["destination"].endswith("/128"): + entry["destination"] = re.sub( + r"\/128$", "", entry["destination"] + ) + routes["ipv6"].append(entry) return routes def route_info(): routes = {} - if subp.which('ip'): + if subp.which("ip"): # Try iproute first of all (iproute_out, _err) = subp.subp(["ip", "-o", "route", "list"]) routes = _netdev_route_info_iproute(iproute_out) - elif subp.which('netstat'): + elif subp.which("netstat"): # Fall back to net-tools if iproute2 is not present (route_out, _err) = subp.subp( - ["netstat", "--route", "--numeric", "--extend"], rcs=[0, 1]) + ["netstat", "--route", "--numeric", "--extend"], rcs=[0, 1] + ) routes = _netdev_route_info_netstat(route_out) else: LOG.warning( - "Could not print routes: missing 'ip' and 'netstat' commands") + "Could not print routes: missing 'ip' and 'netstat' commands" + ) return routes @@ -418,24 +446,42 @@ def netdev_pformat(): lines.append( util.center( "Net device info failed ({error})".format(error=str(e)), - '!', 80)) + "!", + 80, + ) + ) else: if not netdev: - return '\n' - fields = ['Device', 'Up', 'Address', 'Mask', 'Scope', 'Hw-Address'] + return "\n" + fields = ["Device", "Up", "Address", "Mask", "Scope", "Hw-Address"] tbl = SimpleTable(fields) for (dev, data) in sorted(netdev.items()): - for addr in data.get('ipv4'): + for addr in data.get("ipv4"): tbl.add_row( - (dev, data["up"], addr["ip"], addr["mask"], - addr.get('scope', empty), data["hwaddr"])) - for addr in data.get('ipv6'): + ( + dev, + data["up"], + addr["ip"], + addr["mask"], + addr.get("scope", empty), + data["hwaddr"], + ) + ) + for addr in data.get("ipv6"): tbl.add_row( - (dev, data["up"], addr["ip"], empty, - addr.get("scope6", empty), data["hwaddr"])) - if len(data.get('ipv6')) + len(data.get('ipv4')) == 0: - tbl.add_row((dev, data["up"], empty, empty, empty, - data["hwaddr"])) + ( + dev, + data["up"], + addr["ip"], + empty, + addr.get("scope6", empty), + data["hwaddr"], + ) + ) + if len(data.get("ipv6")) + len(data.get("ipv4")) == 0: + tbl.add_row( + (dev, data["up"], empty, empty, empty, data["hwaddr"]) + ) netdev_s = tbl.get_string() max_len = len(max(netdev_s.splitlines(), key=len)) header = util.center("Net device info", "+", max_len) @@ -450,33 +496,59 @@ def route_pformat(): except Exception as e: lines.append( util.center( - 'Route info failed ({error})'.format(error=str(e)), - '!', 80)) + "Route info failed ({error})".format(error=str(e)), "!", 80 + ) + ) util.logexc(LOG, "Route info failed: %s" % e) else: - if routes.get('ipv4'): - fields_v4 = ['Route', 'Destination', 'Gateway', - 'Genmask', 'Interface', 'Flags'] + if routes.get("ipv4"): + fields_v4 = [ + "Route", + "Destination", + "Gateway", + "Genmask", + "Interface", + "Flags", + ] tbl_v4 = SimpleTable(fields_v4) - for (n, r) in enumerate(routes.get('ipv4')): + for (n, r) in enumerate(routes.get("ipv4")): route_id = str(n) - tbl_v4.add_row([route_id, r['destination'], - r['gateway'], r['genmask'], - r['iface'], r['flags']]) + tbl_v4.add_row( + [ + route_id, + r["destination"], + r["gateway"], + r["genmask"], + r["iface"], + r["flags"], + ] + ) route_s = tbl_v4.get_string() max_len = len(max(route_s.splitlines(), key=len)) header = util.center("Route IPv4 info", "+", max_len) lines.extend([header, route_s]) - if routes.get('ipv6'): - fields_v6 = ['Route', 'Destination', 'Gateway', 'Interface', - 'Flags'] + if routes.get("ipv6"): + fields_v6 = [ + "Route", + "Destination", + "Gateway", + "Interface", + "Flags", + ] tbl_v6 = SimpleTable(fields_v6) - for (n, r) in enumerate(routes.get('ipv6')): + for (n, r) in enumerate(routes.get("ipv6")): route_id = str(n) - if r['iface'] == 'lo': + if r["iface"] == "lo": continue - tbl_v6.add_row([route_id, r['destination'], - r['gateway'], r['iface'], r['flags']]) + tbl_v6.add_row( + [ + route_id, + r["destination"], + r["gateway"], + r["iface"], + r["flags"], + ] + ) route_s = tbl_v6.get_string() max_len = len(max(route_s.splitlines(), key=len)) header = util.center("Route IPv6 info", "+", max_len) @@ -484,7 +556,7 @@ def route_pformat(): return "\n".join(lines) + "\n" -def debug_info(prefix='ci-info: '): +def debug_info(prefix="ci-info: "): lines = [] netdev_lines = netdev_pformat().splitlines() if prefix: @@ -500,4 +572,5 @@ def debug_info(prefix='ci-info: '): lines.extend(route_lines) return "\n".join(lines) + # vi: ts=4 expandtab -- cgit v1.2.3 From 223b23e2c428aff6c1e61f49d8e2edde77801a12 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 26 Jan 2022 19:40:06 -0600 Subject: Add json parsing of ip addr show (SC-723) (#1210) 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. Deprecate but leave fallback to use "ip addr" for older iproute2 tooling which does not support --json param. Fix regex parsing of "ip addr" to support peer addresses and metrics. --- cloudinit/netinfo.py | 97 ++++++++- tests/data/netinfo/sample-ipaddrshow-json | 91 ++++++++ tests/data/netinfo/sample-ipaddrshow-json-down | 57 +++++ tests/data/netinfo/sample-ipaddrshow-output | 3 +- tests/unittests/test_netinfo.py | 279 ++++++++++++++++++------- 5 files changed, 443 insertions(+), 84 deletions(-) create mode 100644 tests/data/netinfo/sample-ipaddrshow-json create mode 100644 tests/data/netinfo/sample-ipaddrshow-json-down (limited to 'cloudinit/netinfo.py') diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 74e6b35a..5eeeb967 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,84 @@ from cloudinit.simpletable import SimpleTable 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_json: 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"] if "flags" in dev else [] + address = dev["address"] if dev.get("link_type") == "ether" else "" + dev_info = { + "hwaddr": address, + "up": bool("UP" in flags and "LOWER_UP" in flags), + "ipv4": [], + "ipv6": [], + } + 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.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", "") + # 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, + "scope6": addr.get("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. @@ -47,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( @@ -57,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: @@ -209,8 +287,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 00000000..8f6a430c --- /dev/null +++ b/tests/data/netinfo/sample-ipaddrshow-json @@ -0,0 +1,91 @@ +[ + { + "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, + "metric": 100, + "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 00000000..7ad5dde0 --- /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/data/netinfo/sample-ipaddrshow-output b/tests/data/netinfo/sample-ipaddrshow-output index b2fa2672..2aa3f90c 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 - diff --git a/tests/unittests/test_netinfo.py b/tests/unittests/test_netinfo.py index 5ed15729..aecce921 100644 --- a/tests/unittests/test_netinfo.py +++ b/tests/unittests/test_netinfo.py @@ -2,16 +2,26 @@ """Tests netinfo module functions and classes.""" +import json from copy import copy -from cloudinit.netinfo import netdev_info, netdev_pformat, route_pformat -from tests.unittests.helpers import CiTestCase, mock, readResource +import pytest + +from cloudinit import subp +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 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 +31,7 @@ ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output") 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 +39,7 @@ class TestNetInfo(CiTestCase): 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 +48,7 @@ class TestNetInfo(CiTestCase): 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 +60,19 @@ class TestNetInfo(CiTestCase): 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 +82,19 @@ class TestNetInfo(CiTestCase): 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 +107,62 @@ class TestNetInfo(CiTestCase): "", ) 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 +183,9 @@ class TestNetInfo(CiTestCase): "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 +203,7 @@ class TestNetInfo(CiTestCase): 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,21 +221,133 @@ class TestNetInfo(CiTestCase): 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() + @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 -- cgit v1.2.3