← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~chad.smith/cloud-init:net-tools-deprecation-plus-review-comments into cloud-init:master

 

Chad Smith has proposed merging ~chad.smith/cloud-init:net-tools-deprecation-plus-review-comments into cloud-init:master.

Commit message:
net: Depend on iproute2's ip instead of net-tools ifconfig or route

The net-tools package is deprecated and will eventually be dropped.
Where possible, use "ip route", "link" or "address" instead of "ifconfig"
or "route" calls. So that we run in an environment that no longer has
net-tools.

Requested reviews:
  cloud-init commiters (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/342428

see commit message
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~chad.smith/cloud-init:net-tools-deprecation-plus-review-comments into cloud-init:master.
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 993b26c..f67657c 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -8,7 +8,10 @@
 #
 # This file is part of cloud-init. See LICENSE file for license information.
 
+from copy import copy
 import re
+import socket
+import struct
 
 from cloudinit import log as logging
 from cloudinit import util
@@ -18,18 +21,73 @@ 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 = {
+    "addr": "",
+    "bcast": "",
+    "hwaddr": "",
+    "mask": "",
+    "scope": "",
+    "up": False
+}
+
+
+def netdev_cidr_to_mask(cidr):
+    mask = socket.inet_ntoa(
+        struct.pack(">I", (0xffffffff << (32 - int(cidr)) & 0xffffffff)))
+    return mask
+
+
+def netdev_info_iproute(ipaddr_data, iplink_data):
     devs = {}
-    for line in str(ifcfg_out).splitlines():
+    for line in str(ipaddr_data).splitlines():
+        details = line.lower().strip().split()
+        curdev = details[1]
+        fieldpost = ""
+        if curdev not in devs:
+            devs[curdev] = copy(DEFAULT_NETDEV_INFO)
+        if details[2] == "inet6":
+            fieldpost = "6"
+        for i in range(len(details)):
+            if details[i] == 'inet':
+                (addr, cidr) = details[i + 1].split("/")
+                devs[curdev]["mask"] = netdev_cidr_to_mask(cidr)
+                devs[curdev]["addr"] = addr
+            elif details[i] == 'inet6':
+                addr = details[i + 1]
+                devs[curdev]["addr6"] = addr
+            elif details[i] == "scope":
+                devs[curdev]["scope" + fieldpost] = details[i + 1]
+            elif details[i] == "brd":
+                devs[curdev]["bcast" + fieldpost] = details[i + 1]
+
+    for line in str(iplink_data).splitlines():
+        details = line.lower().strip().split()
+        # Strip trailing ':' and truncate any interface alias '@if34'
+        curdev = details[1].rstrip(':').split('@')[0]
+        for i in range(len(details)):
+            if details[i] == curdev + ":":
+                flags = details[i + 1].strip("<>").split(",")
+                if "lower_up" in flags and "up" in flags:
+                    devs[curdev]["up"] = True
+            if details[i] == "link/ether":
+                devs[curdev]["hwaddr"] = details[i + 1]
+    return devs
+
+
+def netdev_info_ifconfig(ifconfig_data):
+    # fields that need to be returned in devs for each dev
+    fields = ("hwaddr", "up", "addr", "bcast", "mask", "scope")
+    devs = {}
+    for line in str(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] = copy(DEFAULT_NETDEV_INFO)
         toks = line.lower().strip().split()
         if toks[0] == "up":
             devs[curdev]['up'] = True
@@ -39,41 +97,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":
+                if toks[i + 1].startswith("addr:"):
+                    devs[curdev]['addr'] = toks[i + 1].lstrip("addr:")
+                else:
+                    devs[curdev]['addr'] = toks[i + 1]
+            elif toks[i].startswith("bcast:"):
+                devs[curdev]['bcast'] = toks[i].lstrip("bcast:")
+            elif toks[i] == "broadcast":
+                devs[curdev]['bcast'] = toks[i + 1]
+            elif toks[i].startswith("mask:"):
+                devs[curdev]['mask'] = toks[i].lstrip("mask:")
+            elif toks[i] == "netmask":
+                devs[curdev]['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]['addr6'] = toks[i + 2]
+                else:
+                    devs[curdev]['addr6'] = toks[i + 1]
+            elif toks[i] == "prefixlen":
+                addr6 = devs[curdev]['addr6'] + "/" + toks[i + 1]
+                devs[curdev]['addr6'] = addr6
+            elif toks[i].startswith("scope:"):
+                devs[curdev]['scope6'] = toks[i].lstrip("scope:")
+            elif toks[i] == "scopeid":
+                res = re.match(".*<(\S+)>", toks[i + 1])
+                if res:
+                    devs[curdev]['scope6'] = res.group(1)
+    return devs
+
+
+def netdev_info(empty=""):
+    devs = {}
+    try:
+        # Try iproute first of all
+        (ipaddr_out, _err) = util.subp(["ip", "-o", "addr", "list"])
+        (iplink_out, _err) = util.subp(["ip", "-o", "link", "list"])
+        devs = netdev_info_iproute(ipaddr_out, iplink_out)
+    except util.ProcessExecutionError:
+        # 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)
 
     if empty != "":
         for (_devname, dev) in devs.items():
@@ -84,14 +151,83 @@ 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):
+    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 = '24'
+                flags.append("H")
+                entry['genmask'] = netdev_cidr_to_mask(cidr)
+            entry['destination'] = addr
+            entry['genmask'] = netdev_cidr_to_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", "-o", "-6", "route", "list"],
+                                           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 +237,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 +261,52 @@ 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", "-rn"],
+                                         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'] = entry['destination'][:-4]
             routes['ipv6'].append(entry)
     return routes
 
 
+def route_info():
+    routes = {}
+    try:
+        # Try iproute first of all
+        (iproute_out, _err) = util.subp(["ip", "-o", "route", "list"])
+        routes = netdev_route_info_iproute(iproute_out)
+    except util.ProcessExecutionError:
+        # Fall back to net-tools if iproute2 is not present
+        (route_out, _err) = util.subp(["netstat", "-rne"], rcs=[0, 1])
+        routes = netdev_route_info_netstat(route_out)
+    return routes
+
+
 def getgateway():
     try:
         routes = route_info()
@@ -193,27 +350,26 @@ def route_pformat():
     else:
         if routes.get('ipv4'):
             fields_v4 = ['Route', 'Destination', 'Gateway',
-                         'Genmask', 'Interface', 'Flags']
+                         'Genmask', 'Interface', 'Flags', 'Metric']
             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']])
+                                r['iface'], r['flags'], r['metric']])
             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', 'Proto', 'Recv-Q', 'Send-Q',
-                         'Local Address', 'Foreign Address', 'State']
+            fields_v6 = ['Route', 'Destination', 'Gateway', 'Interface',
+                         'Flags', 'Metric']
             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']])
+                tbl_v6.add_row([route_id, r['destination'],
+                                r['gateway'], r['iface'],
+                                r['flags'], r['metric']])
             route_s = tbl_v6.get_string()
             max_len = len(max(route_s.splitlines(), key=len))
             header = util.center("Route IPv6 info", "+", max_len)
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
index 7dea2e4..fe48acf 100644
--- a/cloudinit/tests/test_netinfo.py
+++ b/cloudinit/tests/test_netinfo.py
@@ -2,12 +2,13 @@
 
 """Tests netinfo module functions and classes."""
 
+from cloudinit import util
 from cloudinit.netinfo import netdev_pformat, route_pformat
 from cloudinit.tests.helpers import CiTestCase, mock
 
 
 # Example ifconfig and route output
-SAMPLE_IFCONFIG_OUT = """\
+SAMPLE_OLD_IFCONFIG_OUT = """\
 enp0s25   Link encap:Ethernet  HWaddr 50:7b:9d:2c:af:91
           inet addr:192.168.2.18  Bcast:192.168.2.255  Mask:255.255.255.0
           inet6 addr: fe80::8107:2b92:867e:f8a6/64 Scope:Link
@@ -27,80 +28,182 @@ lo        Link encap:Local Loopback
           collisions:0 txqueuelen:1
 """
 
-SAMPLE_ROUTE_OUT = '\n'.join([
-    '0.0.0.0         192.168.2.1     0.0.0.0         UG        0 0          0'
-    ' enp0s25',
-    '0.0.0.0         192.168.2.1     0.0.0.0         UG        0 0          0'
-    ' wlp3s0',
-    '192.168.2.0     0.0.0.0         255.255.255.0   U         0 0          0'
-    ' enp0s25'])
+SAMPLE_NEW_IFCONFIG_OUT = """\
+enp0s25: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
+        inet 192.168.2.18  netmask 255.255.255.0  broadcast 192.168.2.255
+        inet6 fe80::8107:2b92:867e:f8a6  prefixlen 64  scopeid 0x20<link>
+        ether 50:7b:9d:2c:af:91  txqueuelen 1000  (Ethernet)
+        RX packets 3017  bytes 10601563 (10.1 MiB)
+        RX errors 0  dropped 39  overruns 0  frame 0
+        TX packets 2627  bytes 196976 (192.3 KiB)
+        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
 
+lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
+        inet 127.0.0.1  netmask 255.0.0.0
+        inet6 ::1  prefixlen 128  scopeid 0x10<host>
+        loop  txqueuelen 1  (Local Loopback)
+        RX packets 0  bytes 0 (0.0 B)
+        RX errors 0  dropped 0  overruns 0  frame 0
+        TX packets 0  bytes 0 (0.0 B)
+        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
+"""
+
+# Intentionally disabling line length check on mock data for clarity
+SAMPLE_IPADDR_OUT = '\n'.join([
+    '1: lo    inet 127.0.0.1/8 scope host lo\       valid_lft forever preferred_lft forever',  # noqa: E501
+    '1: lo    inet6 ::1/128 scope host \       valid_lft forever preferred_lft forever',  # noqa: E501
+    '2: enp0s25    inet 192.168.2.18/24 brd 192.168.2.255 scope global dynamic enp0s25\       valid_lft 84174sec preferred_lft 84174sec',  # noqa: E501
+    '2: enp0s25    inet6 fe80::8107:2b92:867e:f8a6/64 scope link \       valid_lft forever preferred_lft forever'])  # noqa: E501
+
+SAMPLE_IPLINK_OUT = '\n'.join([
+    '1: lo: <LOOPBACK,UP,LOWER_UP> 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',  # noqa: E501
+    '2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> 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'])  # noqa: E501
+
+SAMPLE_ROUTE_OUT_V4 = '\n'.join([
+    'Kernel IP routing table',                                                              # noqa: E501
+    'Destination     Gateway         Genmask         Flags Metric Ref    Use Iface',        # noqa: E501
+    '0.0.0.0         192.168.2.1     0.0.0.0         UG        100 0          0 enp0s25',   # noqa: E501
+    '0.0.0.0         192.168.2.1     0.0.0.0         UG        150 0          0 wlp3s0',    # noqa: E501
+    '192.168.2.0     0.0.0.0         255.255.255.0   U         100 0          0 enp0s25'])  # noqa: E501
+
+SAMPLE_ROUTE_OUT_V6 = '\n'.join([
+    'Kernel IPv6 routing table',                                                            # noqa: E501
+    'Destination                     Next Hop                   Flag Met Re  Use If',       # noqa: E501
+    '2a00:abcd:82ae:cd33::657/128    ::                         Ue   256 1     0 enp0s25',  # noqa: E501
+    '2a00:abcd:82ae:cd33::/64        ::                         U    100 1     0 enp0s25',  # noqa: E501
+    '2a00:abcd:82ae:cd33::/56        fe80::32ee:54de:cd43:b4e1  UG   100 1     0 enp0s25',  # noqa: E501
+    'fd81:123f:654::657/128          ::                         U    256 1     0 enp0s25',  # noqa: E501
+    'fd81:123f:654::/64              ::                         U    100 1     0 enp0s25',  # noqa: E501
+    'fd81:123f:654::/48              fe80::32ee:54de:cd43:b4e1  UG   100 1     0 enp0s25',  # noqa: E501
+    'fe80::abcd:ef12:bc34:da21/128   ::                         U    100 1     2 enp0s25',  # noqa: E501
+    'fe80::/64                       ::                         U    256 1 16880 enp0s25',  # noqa: E501
+    '::/0                            fe80::32ee:54de:cd43:b4e1  UG   100 1     0 enp0s25',  # noqa: E501
+    '::/0                            ::                         !n   -1  1424956 lo',       # noqa: E501
+    '::1/128                         ::                         Un   0   4 26289 lo'])      # noqa: E501
+
+SAMPLE_IPROUTE_OUT_V4 = '\n'.join([
+    'default via 192.168.2.1 dev enp0s25 proto static metric 100',  # noqa: E501
+    'default via 192.168.2.1 dev wlp3s0 proto static metric 150',   # noqa: E501
+    '192.168.2.0/24 dev enp0s25 proto kernel scope link src 192.168.2.18 metric 100'])  # noqa: E501
+
+SAMPLE_IPROUTE_OUT_V6 = '\n'.join([
+    '2a00:abcd:82ae:cd33::657 dev enp0s25 proto kernel metric 256 expires 2334sec pref medium',  # noqa: E501
+    '2a00:abcd:82ae:cd33::/64 dev enp0s25 proto ra metric 100 pref medium',  # noqa: E501
+    '2a00:abcd:82ae:cd33::/56 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium',  # noqa: E501
+    'fd81:123f:654::657 dev enp0s25 proto kernel metric 256 pref medium',  # noqa: E501
+    'fd81:123f:654::/64 dev enp0s25 proto ra metric 100 pref medium',  # noqa: E501
+    'fd81:123f:654::/48 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium',  # noqa: E501
+    'fe80::abcd:ef12:bc34:da21 dev enp0s25 proto static metric 100 pref medium',  # noqa: E501
+    'fe80::/64 dev enp0s25 proto kernel metric 256 pref medium',  # noqa: E501
+    'default via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto static metric 100 pref medium'])  # noqa: E501
 
 NETDEV_FORMATTED_OUT = '\n'.join([
-    '+++++++++++++++++++++++++++++++++++++++Net device info+++++++++++++++++++'
-    '++++++++++++++++++++',
-    '+---------+------+------------------------------+---------------+-------+'
-    '-------------------+',
-    '|  Device |  Up  |           Address            |      Mask     | Scope |'
-    '     Hw-Address    |',
-    '+---------+------+------------------------------+---------------+-------+'
-    '-------------------+',
-    '| enp0s25 | True |         192.168.2.18         | 255.255.255.0 |   .   |'
-    ' 50:7b:9d:2c:af:91 |',
-    '| enp0s25 | True | fe80::8107:2b92:867e:f8a6/64 |       .       |  link |'
-    ' 50:7b:9d:2c:af:91 |',
-    '|    lo   | True |          127.0.0.1           |   255.0.0.0   |   .   |'
-    '         .         |',
-    '|    lo   | True |           ::1/128            |       .       |  host |'
-    '         .         |',
-    '+---------+------+------------------------------+---------------+-------+'
-    '-------------------+'])
+    '+++++++++++++++++++++++++++++++++++++++Net device info+++++++++++++++++++++++++++++++++++++++',   # noqa: E501
+    '+---------+------+------------------------------+---------------+-------+-------------------+',   # noqa: E501
+    '|  Device |  Up  |           Address            |      Mask     | Scope |     Hw-Address    |',   # noqa: E501
+    '+---------+------+------------------------------+---------------+-------+-------------------+',   # noqa: E501
+    '| enp0s25 | True |         192.168.2.18         | 255.255.255.0 |   .   | 50:7b:9d:2c:af:91 |',   # noqa: E501
+    '| enp0s25 | True | fe80::8107:2b92:867e:f8a6/64 |       .       |  link | 50:7b:9d:2c:af:91 |',   # noqa: E501
+    '|    lo   | True |          127.0.0.1           |   255.0.0.0   |   .   |         .         |',   # noqa: E501
+    '|    lo   | True |           ::1/128            |       .       |  host |         .         |',   # noqa: E501
+    '+---------+------+------------------------------+---------------+-------+-------------------+'])  # noqa: E501
 
 ROUTE_FORMATTED_OUT = '\n'.join([
-    '+++++++++++++++++++++++++++++Route IPv4 info++++++++++++++++++++++++++'
-    '+++',
-    '+-------+-------------+-------------+---------------+-----------+-----'
-    '--+',
-    '| Route | Destination |   Gateway   |    Genmask    | Interface | Flags'
-    ' |',
-    '+-------+-------------+-------------+---------------+-----------+'
-    '-------+',
-    '|   0   |   0.0.0.0   | 192.168.2.1 |    0.0.0.0    |   wlp3s0  |'
-    '   UG  |',
-    '|   1   | 192.168.2.0 |   0.0.0.0   | 255.255.255.0 |  enp0s25  |'
-    '   U   |',
-    '+-------+-------------+-------------+---------------+-----------+'
-    '-------+',
-    '++++++++++++++++++++++++++++++++++++++++Route IPv6 info++++++++++'
-    '++++++++++++++++++++++++++++++',
-    '+-------+-------------+-------------+---------------+---------------+'
-    '-----------------+-------+',
-    '| Route |    Proto    |    Recv-Q   |     Send-Q    | Local Address |'
-    ' Foreign Address | State |',
-    '+-------+-------------+-------------+---------------+---------------+'
-    '-----------------+-------+',
-    '|   0   |   0.0.0.0   | 192.168.2.1 |    0.0.0.0    |       UG      |'
-    '        0        |   0   |',
-    '|   1   | 192.168.2.0 |   0.0.0.0   | 255.255.255.0 |       U       |'
-    '        0        |   0   |',
-    '+-------+-------------+-------------+---------------+---------------+'
-    '-----------------+-------+'])
+    '+++++++++++++++++++++++++++++++++Route IPv4 info++++++++++++++++++++++++++++++++++',  # noqa: E501
+    '+-------+-------------+-------------+---------------+-----------+-------+--------+',  # noqa: E501
+    '| Route | Destination |   Gateway   |    Genmask    | Interface | Flags | Metric |',  # noqa: E501
+    '+-------+-------------+-------------+---------------+-----------+-------+--------+',  # noqa: E501
+    '|   0   |   0.0.0.0   | 192.168.2.1 |    0.0.0.0    |  enp0s25  |   UG  |  100   |',  # noqa: E501
+    '|   1   |   0.0.0.0   | 192.168.2.1 |    0.0.0.0    |   wlp3s0  |   UG  |  150   |',  # noqa: E501
+    '|   2   | 192.168.2.0 |   0.0.0.0   | 255.255.255.0 |  enp0s25  |   U   |  100   |',  # noqa: E501
+    '+-------+-------------+-------------+---------------+-----------+-------+--------+',  # noqa: E501
+    '+++++++++++++++++++++++++++++++++++++++Route IPv6 info++++++++++++++++++++++++++++++++++++++++',   # noqa: E501
+    '+-------+---------------------------+---------------------------+-----------+-------+--------+',   # noqa: E501
+    '| Route |        Destination        |          Gateway          | Interface | Flags | Metric |',   # noqa: E501
+    '+-------+---------------------------+---------------------------+-----------+-------+--------+',   # noqa: E501
+    '|   0   |  2a00:abcd:82ae:cd33::657 |             ::            |  enp0s25  |   Ue  |  256   |',   # noqa: E501
+    '|   1   |  2a00:abcd:82ae:cd33::/64 |             ::            |  enp0s25  |   U   |  100   |',   # noqa: E501
+    '|   2   |  2a00:abcd:82ae:cd33::/56 | fe80::32ee:54de:cd43:b4e1 |  enp0s25  |   UG  |  100   |',   # noqa: E501
+    '|   3   |     fd81:123f:654::657    |             ::            |  enp0s25  |   U   |  256   |',   # noqa: E501
+    '|   4   |     fd81:123f:654::/64    |             ::            |  enp0s25  |   U   |  100   |',   # noqa: E501
+    '|   5   |     fd81:123f:654::/48    | fe80::32ee:54de:cd43:b4e1 |  enp0s25  |   UG  |  100   |',   # noqa: E501
+    '|   6   | fe80::abcd:ef12:bc34:da21 |             ::            |  enp0s25  |   U   |  100   |',   # noqa: E501
+    '|   7   |         fe80::/64         |             ::            |  enp0s25  |   U   |  256   |',   # noqa: E501
+    '|   8   |            ::/0           | fe80::32ee:54de:cd43:b4e1 |  enp0s25  |   UG  |  100   |',   # noqa: E501
+    '+-------+---------------------------+---------------------------+-----------+-------+--------+'])  # noqa: E501
 
 
 class TestNetInfo(CiTestCase):
 
     maxDiff = None
 
-    @mock.patch('cloudinit.netinfo.util.subp')
-    def test_netdev_pformat(self, m_subp):
+    # older ifconfig syntax from around 2012
+    def netdev_old_nettools_selector(*args, **kwargs):
+        # pylint:disable=no-method-argument
+        if 'ip' in args[0]:
+            raise util.ProcessExecutionError
+        if 'ifconfig' in args[0]:
+            return (SAMPLE_OLD_IFCONFIG_OUT, '')
+
+    # current ifconfig syntax from present snapshot builds
+    def netdev_new_nettools_selector(*args, **kwargs):
+        # pylint:disable=no-method-argument
+        if 'ip' in args[0]:
+            raise util.ProcessExecutionError
+        if 'ifconfig' in args[0]:
+            return (SAMPLE_NEW_IFCONFIG_OUT, '')
+
+    def netdev_nettools_route_selector(*args, **kwargs):
+        # pylint:disable=no-method-argument
+        if 'ip' in args[0]:
+            raise util.ProcessExecutionError
+        if 'netstat' in args[0] and 'inet6' not in args[0]:
+            return (SAMPLE_ROUTE_OUT_V4, '')
+        if 'netstat' in args[0] and 'inet6' in args[0]:
+            return (SAMPLE_ROUTE_OUT_V6, '')
+
+    def netdev_iproute_selector(*args, **kwargs):
+        # pylint:disable=no-method-argument
+        if 'ip' in args[0] and 'addr' in args[0]:
+            return (SAMPLE_IPADDR_OUT, '')
+        if 'ip' in args[0] and 'link' in args[0]:
+            return (SAMPLE_IPLINK_OUT, '')
+        if 'ip' in args[0] and 'route' in args[0] and '-6' not in args[0]:
+            return (SAMPLE_IPROUTE_OUT_V4, '')
+        if 'ip' in args[0] and 'route' in args[0] and '-6' in args[0]:
+            return (SAMPLE_IPROUTE_OUT_V6, '')
+
+    @mock.patch('cloudinit.netinfo.util.subp',
+                side_effect=netdev_old_nettools_selector)
+    def test_netdev_old_nettools_pformat(self, m_subp):
         """netdev_pformat properly rendering network device information."""
-        m_subp.return_value = (SAMPLE_IFCONFIG_OUT, '')
         content = netdev_pformat()
         self.assertEqual(NETDEV_FORMATTED_OUT, content)
 
-    @mock.patch('cloudinit.netinfo.util.subp')
-    def test_route_pformat(self, m_subp):
+    @mock.patch('cloudinit.netinfo.util.subp',
+                side_effect=netdev_new_nettools_selector)
+    def test_netdev_new_nettools_pformat(self, m_subp):
+        """netdev_pformat properly rendering network device information."""
+        content = netdev_pformat()
+        self.assertEqual(NETDEV_FORMATTED_OUT, content)
+
+    @mock.patch('cloudinit.netinfo.util.subp',
+                side_effect=netdev_iproute_selector)
+    def test_netdev_iproute_pformat(self, m_subp):
+        """netdev_pformat properly rendering network device information."""
+        content = netdev_pformat()
+        self.assertEqual(NETDEV_FORMATTED_OUT, content)
+
+    @mock.patch('cloudinit.netinfo.util.subp',
+                side_effect=netdev_nettools_route_selector)
+    def test_route_nettools_pformat(self, m_subp):
+        """netdev_pformat properly rendering network device information."""
+        content = route_pformat()
+        self.assertEqual(ROUTE_FORMATTED_OUT, content)
+
+    @mock.patch('cloudinit.netinfo.util.subp',
+                side_effect=netdev_iproute_selector)
+    def test_route_iproute_pformat(self, m_subp):
         """netdev_pformat properly rendering network device information."""
-        m_subp.return_value = (SAMPLE_ROUTE_OUT, '')
         content = route_pformat()
         self.assertEqual(ROUTE_FORMATTED_OUT, content)

Follow ups