# Copyright (C) 2012 Canonical Ltd. # Copyright (C) 2012 Hewlett-Packard Development Company, L.P. # Copyright (C) 2012 Yahoo! Inc. # # Author: Scott Moser # Author: Juerg Haefliger # Author: Joshua Harlow # # This file is part of cloud-init. See LICENSE file for license information. 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.simpletable import SimpleTable LOG = logging.getLogger() DEFAULT_NETDEV_INFO = {"ipv4": [], "ipv6": [], "hwaddr": "", "up": False} def _netdev_info_iproute(ipaddr_out): """ Get network device dicts from ip route and ip link info. @param ipaddr_out: Output string from 'ip addr show' command. @returns: A dict of device info keyed by network device name containing device configuration values. @raise: TypeError if ipaddr_out isn't a string. """ devs = {} dev_name = None for num, line in enumerate(ipaddr_out.splitlines()): 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(",") devs[dev_name] = { "ipv4": [], "ipv6": [], "hwaddr": "", "up": bool("UP" in flags and "LOWER_UP" in flags), } elif "inet6" in line: m = re.match( 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 ) continue 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, ) if not m: LOG.warning( "Could not parse ip addr show: (line:%d) %s", num, line ) continue match = m.groupdict() 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: m = re.match( 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 ) continue if m.group("link_type") == "ether": devs[dev_name]["hwaddr"] = m.group("hwaddr") else: devs[dev_name]["hwaddr"] = "" else: continue return devs def _netdev_info_ifconfig_netbsd(ifconfig_data): # fields that need to be returned in devs for each dev devs = {} for line in ifconfig_data.splitlines(): if len(line) == 0: continue if line[0] not in ("\t", " "): curdev = line.split()[0] # current ifconfig pops a ':' on the end of the device 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 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)} ) elif toks[i] == "broadcast": devs[curdev]["ipv4"][-1]["bcast"] = toks[i + 1] elif toks[i] == "address:": devs[curdev]["hwaddr"] = toks[i + 1] elif toks[i] == "inet6": if toks[i + 1] == "addr:": devs[curdev]["ipv6"].append({"ip": toks[i + 2]}) else: 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 elif toks[i].startswith("scope:"): devs[curdev]["ipv6"][-1]["scope6"] = toks[i].lstrip("scope:") elif toks[i] == "scopeid": res = re.match(r".*<(\S+)>", toks[i + 1]) if res: devs[curdev]["ipv6"][-1]["scope6"] = res.group(1) else: devs[curdev]["ipv6"][-1]["scope6"] = toks[i + 1] return devs def _netdev_info_ifconfig(ifconfig_data): # fields that need to be returned in devs for each dev devs = {} for line in ifconfig_data.splitlines(): if len(line) == 0: continue if line[0] not in ("\t", " "): curdev = line.split()[0] # current ifconfig pops a ':' on the end of the device 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 # 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]) if res: devs[curdev]["ipv6"][-1]["scope6"] = res.group(1) else: devs[curdev]["ipv6"][-1]["scope6"] = toks[i + 1] return devs def netdev_info(empty=""): devs = {} 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"): # Try iproute first of all (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]) devs = _netdev_info_ifconfig(ifcfg_out) else: LOG.warning( "Could not print networks: missing 'ip' and 'ifconfig' commands" ) if empty == "": return devs recurse_types = (dict, tuple, list) def fill(data, new_val="", empty_vals=("", b"")): """Recursively replace 'empty_vals' in data (dict, tuple, list) with new_val""" if isinstance(data, dict): myiter = data.items() elif isinstance(data, (tuple, list)): myiter = enumerate(data) else: raise TypeError("Unexpected input to fill") for key, val in myiter: if val in empty_vals: data[key] = new_val elif isinstance(val, recurse_types): fill(val, new_val) fill(devs, new_val=empty) return devs def _netdev_route_info_iproute(iproute_data): """ Get network route dicts from ip route info. @param iproute_data: Output string from ip route command. @returns: A dict containing ipv4 and ipv6 route entries as lists. Each item in the list is a route dictionary representing destination, gateway, flags, genmask and interface information. """ routes = {} routes["ipv4"] = [] routes["ipv6"] = [] entries = iproute_data.splitlines() default_route_entry = { "destination": "", "flags": "", "gateway": "", "genmask": "", "iface": "", "metric": "", } for line in entries: entry = copy(default_route_entry) if not line: continue toks = line.split() flags = ["U"] if toks[0] == "default": entry["destination"] = "0.0.0.0" entry["genmask"] = "0.0.0.0" else: if "/" in toks[0]: (addr, cidr) = toks[0].split("/") else: addr = toks[0] 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" for i in range(len(toks)): if toks[i] == "via": 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) try: (iproute_data6, _err6) = subp.subp( ["ip", "--oneline", "-6", "route", "list", "table", "all"], rcs=[0, 1], ) except subp.ProcessExecutionError: pass else: entries6 = iproute_data6.splitlines() for line in entries6: entry = {} if not line: continue toks = line.split() if toks[0] == "default": entry["destination"] = "::/0" entry["flags"] = "UG" else: 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" if toks[i] == "dev": entry["iface"] = toks[i + 1] if toks[i] == "metric": entry["metric"] = toks[i + 1] if toks[i] == "expires": entry["flags"] = entry["flags"] + "e" routes["ipv6"].append(entry) return routes def _netdev_route_info_netstat(route_data): routes = {} routes["ipv4"] = [] routes["ipv6"] = [] entries = route_data.splitlines() for line in entries: if not line: continue toks = line.split() # FreeBSD shows 6 items in the routing table: # Destination Gateway Flags Refs Use Netif Expire # default 10.65.0.1 UGS 0 34920 vtnet0 # # 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" ): continue if len(toks) < 8: toks.append("-") toks.append("-") 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], } routes["ipv4"].append(entry) try: (route_data6, _err6) = subp.subp( ["netstat", "-A", "inet6", "--route", "--numeric"], rcs=[0, 1] ) except subp.ProcessExecutionError: pass else: entries6 = route_data6.splitlines() for line in entries6: 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" ): continue entry = { "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": 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) return routes def route_info(): routes = {} 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"): # Fall back to net-tools if iproute2 is not present (route_out, _err) = subp.subp( ["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" ) return routes def netdev_pformat(): lines = [] empty = "." try: netdev = netdev_info(empty=empty) except Exception as e: lines.append( util.center( "Net device info failed ({error})".format(error=str(e)), "!", 80, ) ) else: if not netdev: 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"): tbl.add_row( ( 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"]) ) netdev_s = tbl.get_string() max_len = len(max(netdev_s.splitlines(), key=len)) header = util.center("Net device info", "+", max_len) lines.extend([header, netdev_s]) return "\n".join(lines) + "\n" def route_pformat(): lines = [] try: routes = route_info() except Exception as e: lines.append( util.center( "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", ] tbl_v4 = SimpleTable(fields_v4) 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"], ] ) 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", ] tbl_v6 = SimpleTable(fields_v6) for (n, r) in enumerate(routes.get("ipv6")): route_id = str(n) if r["iface"] == "lo": continue 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) lines.extend([header, route_s]) return "\n".join(lines) + "\n" def debug_info(prefix="ci-info: "): lines = [] netdev_lines = netdev_pformat().splitlines() if prefix: for line in netdev_lines: lines.append("%s%s" % (prefix, line)) else: lines.extend(netdev_lines) route_lines = route_pformat().splitlines() if prefix: for line in route_lines: lines.append("%s%s" % (prefix, line)) else: lines.extend(route_lines) return "\n".join(lines) # vi: ts=4 expandtab