← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~james-hogarth/cloud-init:net-tools-deprecation into cloud-init:master

 

Scott Moser has proposed merging ~james-hogarth/cloud-init:net-tools-deprecation into cloud-init:master.

Requested reviews:
  cloud-init commiters (cloud-init-dev)
Related bugs:
  Bug #925145 in cloud-init: "Use ip instead of ifconfig and route"
  https://bugs.launchpad.net/cloud-init/+bug/925145

For more details, see:
https://code.launchpad.net/~james-hogarth/cloud-init/+git/cloud-init/+merge/333657
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~james-hogarth/cloud-init:net-tools-deprecation into cloud-init:master.
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 993b26c..0b76301 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -9,6 +9,8 @@
 # This file is part of cloud-init. See LICENSE file for license information.
 
 import re
+import socket
+import struct
 
 from cloudinit import log as logging
 from cloudinit import util
@@ -18,11 +20,62 @@ from cloudinit.simpletable import SimpleTable
 LOG = logging.getLogger()
 
 
-def netdev_info(empty=""):
+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):
+    # fields that need to be returned in devs for each dev
+    fields = ("hwaddr", "up", "addr", "bcast", "mask", "scope")
+    devs = {}
+    for line in str(ipaddr_data).splitlines():
+        details = line.lower().strip().split()
+        curdev = details[1]
+        fieldpost = ""
+        if curdev not in devs:
+            devs[curdev] = {}
+        if details[2] == "inet6":
+            fieldpost = "6"
+        for i in range(len(details)):
+            if details[i] == "inet" or details[i] == "inet6":
+                addr = ""
+                if fieldpost == "":
+                    (addr, cidr) = details[i + 1].split("/")
+                    devs[curdev]["mask"] = netdev_cidr_to_mask(cidr)
+                else:
+                    addr = details[i + 1]
+                devs[curdev]["addr" + fieldpost] = addr
+            if details[i] == "scope":
+                devs[curdev]["scope" + fieldpost] = details[i + 1]
+            if details[i] == "brd":
+                devs[curdev]["bcast" + fieldpost] = details[i + 1]
+
+    for line in str(iplink_data).splitlines():
+        details = line.lower().strip().split()
+        curdev = details[1][:-1]
+        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]
+
+    # Run through all devs and ensure all fields are defined
+    for dev in devs:
+        for field in fields:
+            if field not in devs[dev]:
+                devs[dev][field] = ""
+
+    return devs
+
+
+def netdev_info_ifconfig(ifconfig_data):
     fields = ("hwaddr", "addr", "bcast", "mask")
-    (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
     devs = {}
-    for line in str(ifcfg_out).splitlines():
+    for line in str(ifconfig_data).splitlines():
         if len(line) == 0:
             continue
         if line[0] not in ("\t", " "):
@@ -74,6 +127,20 @@ def netdev_info(empty=""):
                         pass
                 elif toks[i].startswith("%s" % origfield):
                     devs[curdev][target] = toks[i][len(field) + 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,73 @@ 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()
+    for line in entries:
+        entry = {}
+        if not line:
+            continue
+        toks = line.split()
+        if toks[0] == "default":
+            entry['destination'] = "0.0.0.0"
+            entry['genmask'] = "0.0.0.0"
+            entry['flags'] = "UG"
+        else:
+            (addr, cidr) = toks[0].split("/")
+            entry['destination'] = addr
+            entry['genmask'] = netdev_cidr_to_mask(cidr)
+            entry['gateway'] = "0.0.0.0"
+            entry['flags'] = "U"
+        for i in range(len(toks)):
+            if toks[i] == "via":
+                entry['gateway'] = toks[i + 1]
+            if toks[i] == "dev":
+                entry["iface"] = toks[i + 1]
+            if toks[i] == "metric":
+                entry['metric'] = toks[i + 1]
+        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 +227,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 +251,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 +340,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..744c338 100644
--- a/cloudinit/tests/test_netinfo.py
+++ b/cloudinit/tests/test_netinfo.py
@@ -2,6 +2,7 @@
 
 """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
 
@@ -27,80 +28,141 @@ 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'])
+# 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):
+    def netdev_nettools_selector(*args, **kwargs):
+        # pylint:disable=no-method-argument
+        if 'ip' in args[0]:
+            raise util.ProcessExecutionError
+        if 'ifconfig' in args[0]:
+            return (SAMPLE_IFCONFIG_OUT, '')
+        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_nettools_selector)
+    def test_netdev_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."""
-        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_nettools_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