← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~rjschwei/cloud-init:iproute2tools into cloud-init:master

 

Robert Schweikert has proposed merging ~rjschwei/cloud-init:iproute2tools into cloud-init:master.

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

For more details, see:
https://code.launchpad.net/~rjschwei/cloud-init/+git/cloud-init/+merge/335406

Support iproute2 tools when older tools are not available
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~rjschwei/cloud-init:iproute2tools into cloud-init:master.
diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py
index c56319b..8a166dd 100644
--- a/cloudinit/config/cc_disable_ec2_metadata.py
+++ b/cloudinit/config/cc_disable_ec2_metadata.py
@@ -32,13 +32,23 @@ from cloudinit.settings import PER_ALWAYS
 
 frequency = PER_ALWAYS
 
-REJECT_CMD = ['route', 'add', '-host', '169.254.169.254', 'reject']
+REJECT_CMD_IF = ['route', 'add', '-host', '169.254.169.254', 'reject']
+REJECT_CMD_IP = ['ip', 'route', 'add', 'prohibit', '169.254.169.254']
 
 
 def handle(name, cfg, _cloud, log, _args):
     disabled = util.get_cfg_option_bool(cfg, "disable_ec2_metadata", False)
     if disabled:
-        util.subp(REJECT_CMD, capture=False)
+        reject_cmd = None
+        if util.which('ifconfig'):
+            reject_cmd = REJECT_CMD_IF
+        elif util.which('ip'):
+            reject_cmd = REJECT_CMD_IP
+        else:
+            log.error(('Neither "route" nor "ip" command found, unable to '
+                       'manipulate routing table'))
+            return
+        util.subp(reject_cmd, capture=False)
     else:
         log.debug(("Skipping module named %s,"
                    " disabling the ec2 route not enabled"), name)
diff --git a/cloudinit/config/tests/test_disable_ec2_metadata.py b/cloudinit/config/tests/test_disable_ec2_metadata.py
new file mode 100644
index 0000000..bade814
--- /dev/null
+++ b/cloudinit/config/tests/test_disable_ec2_metadata.py
@@ -0,0 +1,72 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Tests cc_disable_ec2_metadata handler"""
+
+import cloudinit.config.cc_disable_ec2_metadata as ec2_meta
+
+from cloudinit.tests.helpers import CiTestCase, mock
+
+import logging
+
+LOG = logging.getLogger(__name__)
+
+DISABLE_CFG = {'disable_ec2_metadata': 'true'}
+
+
+class TestEC2MetadataRoute(CiTestCase):
+
+    @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
+    @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
+    def test_disable_ifconfig(self, m_subp, m_which):
+        """Set the route if ifconfig command is available"""
+        m_subp.side_effect = command_check_ifconfig
+        m_which.side_effect = side_effect_use_ifconfig
+        ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+
+    @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
+    @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
+    def test_disable_ip(self, m_subp, m_which):
+        """Set the route if ip command is available"""
+        m_subp.side_effect = command_check_ip
+        m_which.side_effect = side_effect_use_ip
+        ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+
+    @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
+    @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
+    def test_disable_no_tool(self, m_subp, m_which):
+        """Set the route if ip command is available"""
+        m_subp.side_effect = command_dont_reach
+        m_which.side_effect = side_effect_has_no_tool
+        ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+
+
+def side_effect_use_ifconfig(tool):
+    if tool == 'ifconfig':
+        return True
+    else:
+        return False
+
+
+def side_effect_use_ip(tool):
+    if tool == 'ip':
+        return True
+    else:
+        return False
+
+
+def side_effect_has_no_tool(tool):
+    return False
+
+
+def command_check_ifconfig(cmd, capture):
+    assert(cmd == ['route', 'add', '-host', '169.254.169.254', 'reject'])
+
+
+def command_check_ip(cmd, capture):
+    assert(cmd == ['ip', 'route', 'add', 'prohibit', '169.254.169.254'])
+
+
+def command_dont_reach(cmd, capture):
+    assert('Test should not have reached this location' == 0)
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 993b26c..baad3f9 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -19,6 +19,117 @@ LOG = logging.getLogger()
 
 
 def netdev_info(empty=""):
+    if util.which('ifconfig'):
+        return _netdev_info_from_ifconfig(empty)
+    elif util.which('ip'):
+        return _netdev_info_from_ip(empty)
+    else:
+        LOG.error(('Neither "ifconfig" nor "ip" command found, unable to '
+                   'collect network device information'))
+        return {}
+
+
+def route_info():
+    if util.which('netstat'):
+        return _route_info_from_netstat()
+    elif util.which('ip'):
+        return _route_info_from_ip()
+    else:
+        LOG.error(('Neither "netstat"  nor "ip" command found, unable to '
+                   'collect routing information'))
+        return {}
+
+
+def getgateway():
+    try:
+        routes = route_info()
+    except Exception:
+        pass
+    else:
+        for r in routes.get('ipv4', []):
+            if r['flags'].find("G") >= 0:
+                return "%s[%s]" % (r['gateway'], r['iface'])
+    return None
+
+
+def netdev_pformat():
+    lines = []
+    try:
+        netdev = netdev_info(empty=".")
+    except Exception:
+        lines.append(util.center("Net device info failed", '!', 80))
+    else:
+        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"]])
+        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)
+
+
+def route_pformat():
+    lines = []
+    try:
+        routes = route_info()
+    except Exception as e:
+        lines.append(util.center('Route info failed', '!', 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', 'Proto', 'Recv-Q', 'Send-Q',
+                         'Local Address', 'Foreign Address', 'State']
+            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']])
+            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)
+
+
+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)
+
+
+def _netdev_info_from_ifconfig(empty=""):
+    """Use legacy ifconfig output"""
     fields = ("hwaddr", "addr", "bcast", "mask")
     (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
     devs = {}
@@ -84,7 +195,54 @@ def netdev_info(empty=""):
     return devs
 
 
-def route_info():
+def _netdev_info_from_ip(empty=""):
+    """Use ip to get network information"""
+    fields = ("hwaddr", "addr", "bcast", "mask")
+    (ipdata_out, _err) = util.subp(["ip", "a"], rcs=[0, 1])
+    devs = {}
+    this_device = None
+    for line in str(ipdata_out).splitlines():
+        if len(line) == 0:
+            continue
+        if line[0].isdigit():
+            prts = line.strip().split(':')
+            this_device = prts[1].strip()
+            devs[this_device] = {}
+            for field in fields:
+                devs[this_device][field] = ''
+            devs[this_device]['up'] = False
+            status_info = re.match('(<)(.*)(>)', prts[-1].strip()).group(2)
+            status_info = status_info.lower().split(',')
+            if 'up' in status_info:
+                devs[this_device]['up'] = True
+            if 'broadcast' in status_info and 'multicast' in status_info:
+                devs[this_device]['bcast'] = 'multicast'
+            continue
+        conf_data = line.strip()
+        conf_data_prts = conf_data.split()
+        if conf_data.startswith('inet '):
+            devs[this_device]['addr'] = conf_data_prts[1]
+            if 'brd' in conf_data_prts:
+                loc = conf_data_prts.index('brd')
+                devs[this_device]['bcast'] = conf_data_prts[loc + 1]
+        if conf_data.startswith('inet6'):
+            devs[this_device]['addr6'] = conf_data_prts[1]
+            if 'scope' in conf_data_prts:
+                loc = conf_data_prts.index('scope')
+                devs[this_device]['scope6'] = conf_data_prts[loc + 1]
+        if conf_data.startswith('link/ether'):
+            devs[this_device]['hwaddr'] = conf_data_prts[1]
+
+    if empty != "":
+        for (_devname, dev) in devs.items():
+            for field in dev:
+                if dev[field] == "":
+                    dev[field] = empty
+
+    return devs
+
+
+def _route_info_from_netstat():
     (route_out, _err) = util.subp(["netstat", "-rn"], rcs=[0, 1])
 
     routes = {}
@@ -150,91 +308,69 @@ def route_info():
     return routes
 
 
-def getgateway():
-    try:
-        routes = route_info()
-    except Exception:
-        pass
-    else:
-        for r in routes.get('ipv4', []):
-            if r['flags'].find("G") >= 0:
-                return "%s[%s]" % (r['gateway'], r['iface'])
-    return None
-
-
-def netdev_pformat():
-    lines = []
-    try:
-        netdev = netdev_info(empty=".")
-    except Exception:
-        lines.append(util.center("Net device info failed", '!', 80))
-    else:
-        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"]])
-        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)
+def _route_info_from_ip():
+    """Detremine route information from ip route command"""
+    routes = {}
+    routes['ipv4'] = []
+    routes['ipv6'] = []
 
+    # IPv4
+    (route_out, _err) = util.subp(['ip', '-4', 'route', 'list'], rcs=[0, 1])
 
-def route_pformat():
-    lines = []
-    try:
-        routes = route_info()
-    except Exception as e:
-        lines.append(util.center('Route info failed', '!', 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', 'Proto', 'Recv-Q', 'Send-Q',
-                         'Local Address', 'Foreign Address', 'State']
-            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']])
-            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)
+    entries = route_out.splitlines()
+    for line in entries:
+        route_info = line.strip().split()
+        dest = route_info[0]
+        if route_info[0] == 'default':
+            dest = '0.0.0.0'
+        flags = ''
+        gw = '0.0.0.0'
+        if 'via' in route_info:
+            loc = route_info.index('via')
+            # The NH (Next Hop) is basically equivalent to the gateway
+            gw = route_info[loc + 1]
+            flags = 'G'
+        loc = route_info.index('dev')
+        dev = route_info[loc + 1]
+        entry = {
+            'destination': dest,
+            'gateway': gw,
+            'genmask': '',
+            'flags': flags,
+            'metric': '0',
+            'ref': '0',
+            'use': '0',
+            'iface': dev
+        }
+        routes['ipv4'].append(entry)
 
+    # IPv6
+    (route_out, _err) = util.subp(['ip', '-6', 'route', 'list'], rcs=[0, 1])
 
-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)
+    entries = route_out.splitlines()
+    for line in entries:
+        route_info = line.strip().split()
+        ip = route_info[0]
+        if ip == 'default':
+            ip = '::'
+        proto = 'tcp6'
+        if 'proto' in route_info:
+            loc = route_info.index('proto')
+            proto = route_info[loc + 1]
+        gw = ''
+        if 'via' in route_info:
+            loc = route_info.index('via')
+            # The NH (Next Hop) is basically equivalent to the gateway
+            gw = route_info[loc + 1]
+        entry = {
+            'proto': proto,
+            'recv-q': '0',
+            'send-q': '0',
+            'local address': ip,
+            'foreign address': gw,
+            'state': '',
+        }
+        routes['ipv6'].append(entry)
+    return routes
 
 # vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
index 7dea2e4..3dc557c 100644
--- a/cloudinit/tests/test_netinfo.py
+++ b/cloudinit/tests/test_netinfo.py
@@ -2,7 +2,7 @@
 
 """Tests netinfo module functions and classes."""
 
-from cloudinit.netinfo import netdev_pformat, route_pformat
+from cloudinit.netinfo import getgateway, netdev_pformat, route_pformat
 from cloudinit.tests.helpers import CiTestCase, mock
 
 
@@ -27,6 +27,48 @@ lo        Link encap:Local Loopback
           collisions:0 txqueuelen:1
 """
 
+SAMPLE_IP_A_OUT = (
+    '1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN '
+    'group default qlen 1000\n'
+    'link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00\n'
+    'inet 127.0.0.1/8 scope host lo\n'
+    '   valid_lft forever preferred_lft forever\n'
+    'inet6 ::1/128 scope host\n'
+    '   valid_lft forever preferred_lft forever\n'
+    '2: wlp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state '
+    'UP group default qlen 1000\n'
+    'link/ether 84:3a:4b:09:6f:ec brd ff:ff:ff:ff:ff:ff\n'
+    'inet 192.168.1.101/24 brd 192.168.1.255 scope global wlp3s0\n'
+    '   valid_lft forever preferred_lft forever\n'
+    'inet 192.168.1.3/24 brd 192.168.1.255 scope global secondary wlp3s0\n'
+    '   valid_lft forever preferred_lft forever\n'
+    'inet6 fe80::863a:4bff:fe09:6fec/64 scope link\n'
+    '   valid_lft forever preferred_lft forever'
+)
+
+SAMPLE_ROUTE_INFO = {
+    'ipv4': [
+        {
+            'genmask': '0.0.0.0',
+            'use': '0',
+            'iface': 'eth1',
+            'flags': 'UG',
+            'metric': '0',
+            'destination': '0.0.0.0',
+            'ref': '0',
+            'gateway': '192.168.1.1'},
+        {
+            'genmask': '255.0.0.0',
+            'use': '0',
+            'iface': 'eth2',
+            'flags': 'UG',
+            'metric': '0',
+            'destination': '10.0.0.0',
+            'ref': '0',
+            'gateway': '10.163.8.1'}
+    ]
+}
+
 SAMPLE_ROUTE_OUT = '\n'.join([
     '0.0.0.0         192.168.2.1     0.0.0.0         UG        0 0          0'
     ' enp0s25',
@@ -35,6 +77,20 @@ SAMPLE_ROUTE_OUT = '\n'.join([
     '192.168.2.0     0.0.0.0         255.255.255.0   U         0 0          0'
     ' enp0s25'])
 
+SAMPLE_ROUTE_OUT_IP_V4 = '\n'.join([
+    'default via 192.168.1.1 dev br0',
+    '10.0.0.0/8 via 10.163.8.1 dev tun0',
+    '10.163.8.1 dev tun0  proto kernel  scope link  src 10.163.8.118 ',
+    '137.65.0.0/16 via 10.163.8.1 dev tun0'])
+
+SAMPLE_ROUTE_OUT_IP_V6 = '\n'.join([
+    '2621:111:80c0:8080:12:160:68:53 dev eth0 proto kernel metric 256 expires '
+    '9178sec pref medium',
+    '2621:111:80c0:8080::/64 dev eth0 proto ra metric 100 pref medium',
+    'fe80::1 dev eth0 proto static metric 100 pref medium',
+    'fe80::/64 dev eth0 proto kernel metric 256 pref medium',
+    'default via fe80::1 dev eth0 proto static metric 100 pref medium',
+    '2620:113:80c0:8000::/50 dev tun0  metric 1024  pref medium'])
 
 NETDEV_FORMATTED_OUT = '\n'.join([
     '+++++++++++++++++++++++++++++++++++++++Net device info+++++++++++++++++++'
@@ -56,6 +112,26 @@ NETDEV_FORMATTED_OUT = '\n'.join([
     '+---------+------+------------------------------+---------------+-------+'
     '-------------------+'])
 
+NETDEV_FORMATTED_OUT_IP = '\n'.join([
+    '++++++++++++++++++++++++++++++++++Net device info++++++++++++++++++++++'
+    '++++++++++++',
+    '+--------+------+------------------------------+------+-------+----------'
+    '---------+',
+    '| Device |  Up  |           Address            | Mask | Scope |     Hw-Ad'
+    'dress    |',
+    '+--------+------+------------------------------+------+-------+----------'
+    '---------+',
+    '|   lo   | True |         127.0.0.1/8          |  .   |   .   |         .'
+    '         |',
+    '|   lo   | True |           ::1/128            |  .   |  host |         .'
+    '         |',
+    '| wlp3s0 | True |        192.168.1.3/24        |  .   |   .   | 84:3a:4b:'
+    '09:6f:ec |',
+    '| wlp3s0 | True | fe80::863a:4bff:fe09:6fec/64 |  .   |  link | 84:3a:4b:'
+    '09:6f:ec |',
+    '+--------+------+------------------------------+------+-------+----------'
+    '---------+'])
+
 ROUTE_FORMATTED_OUT = '\n'.join([
     '+++++++++++++++++++++++++++++Route IPv4 info++++++++++++++++++++++++++'
     '+++',
@@ -86,21 +162,113 @@ ROUTE_FORMATTED_OUT = '\n'.join([
     '+-------+-------------+-------------+---------------+---------------+'
     '-----------------+-------+'])
 
+ROUTE_FORMATTED_OUT_IP = '\n'.join([
+    '+++++++++++++++++++++++++++Route IPv4 info+++++++++++++++++++++++++++',
+    '+-------+---------------+-------------+---------+-----------+-------+',
+    '| Route |  Destination  |   Gateway   | Genmask | Interface | Flags |',
+    '+-------+---------------+-------------+---------+-----------+-------+',
+    '|   0   |    0.0.0.0    | 192.168.1.1 |         |    br0    |   G   |',
+    '|   1   |   10.0.0.0/8  |  10.163.8.1 |         |    tun0   |   G   |',
+    '|   2   |   10.163.8.1  |   0.0.0.0   |         |    tun0   |       |',
+    '|   3   | 137.65.0.0/16 |  10.163.8.1 |         |    tun0   |   G   |',
+    '+-------+---------------+-------------+---------+-----------+-------+',
+    '++++++++++++++++++++++++++++++++++++++++Route IPv6 info++++++++++++++'
+    '+++++++++++++++++++++++++++',
+    '+-------+--------+--------+--------+---------------------------------'
+    '+-----------------+-------+',
+    '| Route | Proto  | Recv-Q | Send-Q |          Local Address          '
+    '| Foreign Address | State |',
+    '+-------+--------+--------+--------+---------------------------------'
+    '+-----------------+-------+',
+    '|   0   | kernel |   0    |   0    | 2621:111:80c0:8080:12:160:68:53 '
+    '|                 |       |',
+    '|   1   |   ra   |   0    |   0    |     2621:111:80c0:8080::/64     '
+    '|                 |       |',
+    '|   2   | static |   0    |   0    |             fe80::1             '
+    '|                 |       |',
+    '|   3   | kernel |   0    |   0    |            fe80::/64            '
+    '|                 |       |',
+    '|   4   | static |   0    |   0    |                ::               '
+    '|     fe80::1     |       |',
+    '|   5   |  tcp6  |   0    |   0    |     2620:113:80c0:8000::/50     '
+    '|                 |       |',
+    '+-------+--------+--------+--------+---------------------------------'
+    '+-----------------+-------+'])
+
 
 class TestNetInfo(CiTestCase):
 
     maxDiff = None
 
+    @mock.patch('cloudinit.netinfo.route_info')
+    def test_getdateway_route(self, m_route_info):
+        """getgateway finds the first gateway"""
+        m_route_info.return_value = SAMPLE_ROUTE_INFO
+        gateway = getgateway()
+        self.assertEqual('192.168.1.1[eth1]', gateway)
+
+    @mock.patch('cloudinit.netinfo.util.which')
     @mock.patch('cloudinit.netinfo.util.subp')
-    def test_netdev_pformat(self, m_subp):
+    def test_netdev_pformat_ifconfig(self, m_subp, m_which):
         """netdev_pformat properly rendering network device information."""
         m_subp.return_value = (SAMPLE_IFCONFIG_OUT, '')
+        m_which.side_effect = side_effect_use_ifconfig
         content = netdev_pformat()
         self.assertEqual(NETDEV_FORMATTED_OUT, content)
 
+    @mock.patch('cloudinit.netinfo.util.which')
     @mock.patch('cloudinit.netinfo.util.subp')
-    def test_route_pformat(self, m_subp):
+    def test_netdev_pformat_ip(self, m_subp, m_which):
+        """netdev_pformat properly rendering network device information."""
+        m_subp.return_value = (SAMPLE_IP_A_OUT, '')
+        m_which.side_effect = side_effect_use_ip
+        content = netdev_pformat()
+        self.assertEqual(NETDEV_FORMATTED_OUT_IP, content)
+
+    @mock.patch('cloudinit.netinfo.util.which')
+    @mock.patch('cloudinit.netinfo.util.subp')
+    def test_route_pformat_netstat(self, m_subp, m_which):
         """netdev_pformat properly rendering network device information."""
         m_subp.return_value = (SAMPLE_ROUTE_OUT, '')
+        m_which.side_effect = side_effect_use_netstat
         content = route_pformat()
         self.assertEqual(ROUTE_FORMATTED_OUT, content)
+
+    @mock.patch('cloudinit.netinfo.util.which')
+    @mock.patch('cloudinit.netinfo.util.subp')
+    def test_route_pformat_ip(self, m_subp, m_which):
+        """netdev_pformat properly rendering network device information."""
+        m_subp.side_effect = side_effect_return_route_info
+        m_which.side_effect = side_effect_use_ip
+        content = route_pformat()
+        self.assertEqual(ROUTE_FORMATTED_OUT_IP, content)
+
+
+def side_effect_use_ifconfig(tool):
+    if tool == 'ifconfig':
+        return True
+    else:
+        return False
+
+
+def side_effect_use_ip(tool):
+    if tool == 'ip':
+        return True
+    else:
+        return False
+
+
+def side_effect_use_netstat(tool):
+    if tool == 'netstat':
+        return True
+    else:
+        return False
+
+
+def side_effect_return_route_info(cmd, rcs=None):
+    if '-4' in list(cmd):
+        return (SAMPLE_ROUTE_OUT_IP_V4, 0)
+    else:
+        return (SAMPLE_ROUTE_OUT_IP_V6, 0)
+
+# vi: ts=4 expandtab

Follow ups