summaryrefslogtreecommitdiff
path: root/cloudinit/netinfo.py
diff options
context:
space:
mode:
authorChad Smith <chad.smith@canonical.com>2018-04-18 15:22:42 -0600
committerChad Smith <chad.smith@canonical.com>2018-04-18 15:22:42 -0600
commit6d48d265a0548a2dc23e587f2a335d4e38e8db90 (patch)
tree897e919ba57771d1faf299d61a2e87a4f46120ea /cloudinit/netinfo.py
parent4c573d0e0173d2b1e99a383c54a0a6c957aa1cbb (diff)
downloadvyos-cloud-init-6d48d265a0548a2dc23e587f2a335d4e38e8db90.tar.gz
vyos-cloud-init-6d48d265a0548a2dc23e587f2a335d4e38e8db90.zip
net: Depend on iproute2's ip instead of net-tools ifconfig or route
The net-tools package is deprecated and will eventually be dropped. Use "ip route", "link" or "address" instead of "ifconfig" or "route" calls. Cloud-init can now run in an environment that no longer has net-tools. This affects the network and route printing emitted to cloud-config-output.log as well as the cc_disable_ec2_metadata module. Additional changes:  - separate readResource and resourceLocation into standalone test    functions  - Fix ipv4 address rows to report scopes represented by ip addr show  - Formatted route/address ouput now handles multiple ipv4 and ipv6    addresses on a single interface Co-authored-by: James Hogarth <james.hogarth@gmail.com> Co-authored-by: Robert Schweikert <rjschwei@suse.com>
Diffstat (limited to 'cloudinit/netinfo.py')
-rw-r--r--cloudinit/netinfo.py345
1 files changed, 273 insertions, 72 deletions
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 993b26cf..f0906160 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -8,9 +8,11 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+from copy import copy, deepcopy
import re
from cloudinit import log as logging
+from cloudinit.net.network_state import net_prefix_to_ipv4_mask
from cloudinit import util
from cloudinit.simpletable import SimpleTable
@@ -18,18 +20,90 @@ from cloudinit.simpletable import SimpleTable
LOG = logging.getLogger()
-def netdev_info(empty=""):
- fields = ("hwaddr", "addr", "bcast", "mask")
- (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
+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 = {}
- for line in str(ifcfg_out).splitlines():
+ dev_name = None
+ for num, line in enumerate(ipaddr_out.splitlines()):
+ m = re.match(r'^\d+:\s(?P<dev>[^:]+):\s+<(?P<flags>\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<ip>\S+)\sscope\s(?P<scope6>\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<cidr4>\S+)(\sbrd\s(?P<bcast>\S+))?\sscope\s'
+ r'(?P<scope>\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<link_type>\S+)\s(?P<hwaddr>\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(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]
- devs[curdev] = {"up": False}
- for field in fields:
- devs[curdev][field] = ""
+ # 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
@@ -39,41 +113,50 @@ def netdev_info(empty=""):
if re.search(r"flags=\d+<up,", toks[1]):
devs[curdev]['up'] = True
- fieldpost = ""
- if toks[0] == "inet6":
- fieldpost = "6"
-
for i in range(len(toks)):
- # older net-tools (ubuntu) show 'inet addr:xx.yy',
- # newer (freebsd and fedora) show 'inet xx.yy'
- # just skip this 'inet' entry. (LP: #1285185)
- try:
- if ((toks[i] in ("inet", "inet6") and
- toks[i + 1].startswith("addr:"))):
- continue
- except IndexError:
- pass
-
- # Couple the different items we're interested in with the correct
- # field since FreeBSD/CentOS/Fedora differ in the output.
- ifconfigfields = {
- "addr:": "addr", "inet": "addr",
- "bcast:": "bcast", "broadcast": "bcast",
- "mask:": "mask", "netmask": "mask",
- "hwaddr": "hwaddr", "ether": "hwaddr",
- "scope": "scope",
- }
- for origfield, field in ifconfigfields.items():
- target = "%s%s" % (field, fieldpost)
- if devs[curdev].get(target, ""):
- continue
- if toks[i] == "%s" % origfield:
- try:
- devs[curdev][target] = toks[i + 1]
- except IndexError:
- pass
- elif toks[i].startswith("%s" % origfield):
- devs[curdev][target] = toks[i][len(field) + 1:]
+ if toks[i] == "inet": # Create new ipv4 addr entry
+ devs[curdev]['ipv4'].append(
+ {'ip': toks[i + 1].lstrip("addr:")})
+ elif toks[i].startswith("bcast:"):
+ devs[curdev]['ipv4'][-1]['bcast'] = toks[i].lstrip("bcast:")
+ elif toks[i] == "broadcast":
+ devs[curdev]['ipv4'][-1]['bcast'] = toks[i + 1]
+ elif toks[i].startswith("mask:"):
+ devs[curdev]['ipv4'][-1]['mask'] = toks[i].lstrip("mask:")
+ elif toks[i] == "netmask":
+ devs[curdev]['ipv4'][-1]['mask'] = toks[i + 1]
+ elif toks[i] == "hwaddr" or toks[i] == "ether":
+ 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(".*<(\S+)>", toks[i + 1])
+ if res:
+ devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)
+ return devs
+
+
+def netdev_info(empty=""):
+ devs = {}
+ if util.which('ip'):
+ # Try iproute first of all
+ (ipaddr_out, _err) = util.subp(["ip", "addr", "show"])
+ devs = _netdev_info_iproute(ipaddr_out)
+ elif util.which('ifconfig'):
+ # Fall back to net-tools if iproute2 is not present
+ (ifcfg_out, _err) = util.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 != "":
for (_devname, dev) in devs.items():
@@ -84,14 +167,94 @@ def netdev_info(empty=""):
return devs
-def route_info():
- (route_out, _err) = util.subp(["netstat", "-rn"], rcs=[0, 1])
+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) = util.subp(
+ ["ip", "--oneline", "-6", "route", "list", "table", "all"],
+ rcs=[0, 1])
+ except util.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_out.splitlines()[1:]
+ entries = route_data.splitlines()
for line in entries:
if not line:
continue
@@ -101,8 +264,8 @@ def route_info():
# default 10.65.0.1 UGS 0 34920 vtnet0
#
# Linux netstat shows 2 more:
- # Destination Gateway Genmask Flags MSS Window irtt Iface
- # 0.0.0.0 10.65.0.1 0.0.0.0 UG 0 0 0 eth0
+ # 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"):
@@ -125,31 +288,57 @@ def route_info():
routes['ipv4'].append(entry)
try:
- (route_out6, _err6) = util.subp(["netstat", "-A", "inet6", "-n"],
- rcs=[0, 1])
+ (route_data6, _err6) = util.subp(
+ ["netstat", "-A", "inet6", "--route", "--numeric"], rcs=[0, 1])
except util.ProcessExecutionError:
pass
else:
- entries6 = route_out6.splitlines()[1:]
+ entries6 = route_data6.splitlines()
for line in entries6:
if not line:
continue
toks = line.split()
- if (len(toks) < 6 or toks[0] == "Kernel" or
+ 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 = {
- 'proto': toks[0],
- 'recv-q': toks[1],
- 'send-q': toks[2],
- 'local address': toks[3],
- 'foreign address': toks[4],
- 'state': toks[5],
+ '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 util.which('ip'):
+ # Try iproute first of all
+ (iproute_out, _err) = util.subp(["ip", "-o", "route", "list"])
+ routes = _netdev_route_info_iproute(iproute_out)
+ elif util.which('netstat'):
+ # Fall back to net-tools if iproute2 is not present
+ (route_out, _err) = util.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 getgateway():
try:
routes = route_info()
@@ -166,21 +355,30 @@ def netdev_pformat():
lines = []
try:
netdev = netdev_info(empty=".")
- except Exception:
- lines.append(util.center("Net device info failed", '!', 80))
+ 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, d) in sorted(netdev.items()):
- tbl.add_row([dev, d["up"], d["addr"], d["mask"], ".", d["hwaddr"]])
- if d.get('addr6'):
- tbl.add_row([dev, d["up"],
- d["addr6"], ".", d.get("scope6"), d["hwaddr"]])
+ 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', '.'), data["hwaddr"]])
+ for addr in data.get('ipv6'):
+ tbl.add_row(
+ [dev, data["up"], addr["ip"], ".", addr["scope6"],
+ 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)
+ return "\n".join(lines) + "\n"
def route_pformat():
@@ -188,7 +386,10 @@ def route_pformat():
try:
routes = route_info()
except Exception as e:
- lines.append(util.center('Route info failed', '!', 80))
+ 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'):
@@ -205,20 +406,20 @@ def route_pformat():
header = util.center("Route IPv4 info", "+", max_len)
lines.extend([header, route_s])
if routes.get('ipv6'):
- fields_v6 = ['Route', 'Proto', 'Recv-Q', 'Send-Q',
- 'Local Address', 'Foreign Address', 'State']
+ fields_v6 = ['Route', 'Destination', 'Gateway', 'Interface',
+ 'Flags']
tbl_v6 = SimpleTable(fields_v6)
for (n, r) in enumerate(routes.get('ipv6')):
route_id = str(n)
- tbl_v6.add_row([route_id, r['proto'],
- r['recv-q'], r['send-q'],
- r['local address'], r['foreign address'],
- r['state']])
+ 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)
+ return "\n".join(lines) + "\n"
def debug_info(prefix='ci-info: '):