← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel

 

Chad Smith has proposed merging ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel.

Commit message:
Sync bugfixes from master into Bionic for release

Requested reviews:
  cloud-init commiters (cloud-init-dev)
Related bugs:
  Bug #1667735 in cloud-init: "cloud-init doesn't retry metadata lookups and hangs forever if metadata is down"
  https://bugs.launchpad.net/cloud-init/+bug/1667735
  Bug #1763511 in cloud-init (Ubuntu): "DataSourceSmartOS should default to ext4"
  https://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/1763511

For more details, see:
https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/343562
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel.
diff --git a/MANIFEST.in b/MANIFEST.in
index 1a4d771..57a85ea 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,6 @@
 include *.py MANIFEST.in LICENSE* ChangeLog
 global-include *.txt *.rst *.ini *.in *.conf *.cfg *.sh
+graft bash_completion
 graft config
 graft doc
 graft packages
diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init
new file mode 100644
index 0000000..581432c
--- /dev/null
+++ b/bash_completion/cloud-init
@@ -0,0 +1,77 @@
+# Copyright (C) 2018 Canonical Ltd.
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+# bash completion for cloud-init cli
+_cloudinit_complete()
+{
+
+    local cur_word prev_word
+    cur_word="${COMP_WORDS[COMP_CWORD]}"
+    prev_word="${COMP_WORDS[COMP_CWORD-1]}"
+
+    subcmds="analyze clean collect-logs devel dhclient-hook features init modules single status"
+    base_params="--help --file --version --debug --force"
+    case ${COMP_CWORD} in
+        1)
+            COMPREPLY=($(compgen -W "$base_params $subcmds" -- $cur_word))
+            ;;
+        2)
+            case ${prev_word} in
+                analyze)
+                    COMPREPLY=($(compgen -W "--help blame dump show" -- $cur_word))
+                    ;;
+                clean)
+                    COMPREPLY=($(compgen -W "--help --logs --reboot --seed" -- $cur_word))
+                    ;;
+                collect-logs)
+                    COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word))
+                    ;;
+                devel)
+                    COMPREPLY=($(compgen -W "--help schema" -- $cur_word))
+                    ;;
+                dhclient-hook|features)
+                    COMPREPLY=($(compgen -W "--help" -- $cur_word))
+                    ;;
+                init)
+                    COMPREPLY=($(compgen -W "--help --local" -- $cur_word))
+                    ;;
+                modules)
+                    COMPREPLY=($(compgen -W "--help --mode" -- $cur_word))
+                    ;;
+
+                single)
+                    COMPREPLY=($(compgen -W "--help --name --frequency --report" -- $cur_word))
+                    ;;
+                status)
+                    COMPREPLY=($(compgen -W "--help --long --wait" -- $cur_word))
+                    ;;
+            esac
+            ;;
+        3)
+            case ${prev_word} in
+                blame|dump)
+                    COMPREPLY=($(compgen -W "--help --infile --outfile" -- $cur_word))
+                    ;;
+                --mode)
+                    COMPREPLY=($(compgen -W "--help init config final" -- $cur_word))
+                    ;;
+                --frequency)
+                    COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word))
+                    ;;
+                schema)
+                    COMPREPLY=($(compgen -W "--help --config-file --doc --annotate" -- $cur_word))
+                    ;;
+                show)
+                    COMPREPLY=($(compgen -W "--help --format --infile --outfile" -- $cur_word))
+                    ;;
+            esac
+            ;;
+        *)
+            COMPREPLY=()
+            ;;
+    esac
+}
+complete -F _cloudinit_complete cloud-init
+
+# vi: syntax=bash expandtab
diff --git a/cloudinit/analyze/__main__.py b/cloudinit/analyze/__main__.py
index 3ba5903..f861365 100644
--- a/cloudinit/analyze/__main__.py
+++ b/cloudinit/analyze/__main__.py
@@ -69,7 +69,7 @@ def analyze_blame(name, args):
     """
     (infh, outfh) = configure_io(args)
     blame_format = '     %ds (%n)'
-    r = re.compile('(^\s+\d+\.\d+)', re.MULTILINE)
+    r = re.compile(r'(^\s+\d+\.\d+)', re.MULTILINE)
     for idx, record in enumerate(show.show_events(_get_events(infh),
                                                   blame_format)):
         srecs = sorted(filter(r.match, record), reverse=True)
diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py
index 5b9cbca..afaca46 100644
--- a/cloudinit/config/cc_apt_configure.py
+++ b/cloudinit/config/cc_apt_configure.py
@@ -121,7 +121,7 @@ and https protocols respectively. The ``proxy`` key also exists as an alias for
 All source entries in ``apt-sources`` that match regex in
 ``add_apt_repo_match`` will be added to the system using
 ``add-apt-repository``. If ``add_apt_repo_match`` is not specified, it defaults
-to ``^[\w-]+:\w``
+to ``^[\\w-]+:\\w``
 
 **Add source list entries:**
 
diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py
index c56319b..885b313 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('ip'):
+            reject_cmd = REJECT_CMD_IP
+        elif util.which('ifconfig'):
+            reject_cmd = REJECT_CMD_IF
+        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/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py
index 4da3a58..50b3747 100644
--- a/cloudinit/config/cc_power_state_change.py
+++ b/cloudinit/config/cc_power_state_change.py
@@ -74,7 +74,7 @@ def givecmdline(pid):
         if util.is_FreeBSD():
             (output, _err) = util.subp(['procstat', '-c', str(pid)])
             line = output.splitlines()[1]
-            m = re.search('\d+ (\w|\.|-)+\s+(/\w.+)', line)
+            m = re.search(r'\d+ (\w|\.|-)+\s+(/\w.+)', line)
             return m.group(2)
         else:
             return util.load_file("/proc/%s/cmdline" % pid)
diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
index af08788..27d2366 100644
--- a/cloudinit/config/cc_rsyslog.py
+++ b/cloudinit/config/cc_rsyslog.py
@@ -203,8 +203,8 @@ LOG = logging.getLogger(__name__)
 COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
 HOST_PORT_RE = re.compile(
     r'^(?P<proto>[@]{0,2})'
-    '(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
-    '([:](?P<port>[0-9]+))?$')
+    r'(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
+    r'([:](?P<port>[0-9]+))?$')
 
 
 def reload_syslog(command=DEF_RELOAD, systemd=False):
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..67646b0
--- /dev/null
+++ b/cloudinit/config/tests/test_disable_ec2_metadata.py
@@ -0,0 +1,50 @@
+# 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):
+
+    with_logs = True
+
+    @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_which.side_effect = lambda x: x if x == 'ifconfig' else None
+        ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+        m_subp.assert_called_with(
+            ['route', 'add', '-host', '169.254.169.254', 'reject'],
+            capture=False)
+
+    @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_which.side_effect = lambda x: x if x == 'ip' else None
+        ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+        m_subp.assert_called_with(
+            ['ip', 'route', 'add', 'prohibit', '169.254.169.254'],
+            capture=False)
+
+    @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):
+        """Log error when neither route nor ip commands are available"""
+        m_which.return_value = None  # Find neither ifconfig nor ip
+        ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+        self.assertEqual(
+            [mock.call('ip'), mock.call('ifconfig')], m_which.call_args_list)
+        m_subp.assert_not_called()
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index 754d3df..099fac5 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -110,7 +110,7 @@ class Distro(distros.Distro):
         if dev.startswith('lo'):
             return dev
 
-        n = re.search('\d+$', dev)
+        n = re.search(r'\d+$', dev)
         index = n.group(0)
 
         (out, err) = util.subp(['ifconfig', '-a'])
@@ -118,7 +118,7 @@ class Distro(distros.Distro):
                           if len(x.split()) > 0]
         bsddev = 'NOT_FOUND'
         for line in ifconfigoutput:
-            m = re.match('^\w+', line)
+            m = re.match(r'^\w+', line)
             if m:
                 if m.group(0).startswith('lo'):
                     continue
@@ -128,7 +128,7 @@ class Distro(distros.Distro):
                 break
 
         # Replace the index with the one we're after.
-        bsddev = re.sub('\d+$', index, bsddev)
+        bsddev = re.sub(r'\d+$', index, bsddev)
         LOG.debug("Using network interface %s", bsddev)
         return bsddev
 
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 6d63e5c..72c803e 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -7,6 +7,8 @@
 import copy
 import functools
 import logging
+import socket
+import struct
 
 import six
 
@@ -886,12 +888,9 @@ def net_prefix_to_ipv4_mask(prefix):
     This is the inverse of ipv4_mask_to_net_prefix.
         24 -> "255.255.255.0"
     Also supports input as a string."""
-
-    mask = [0, 0, 0, 0]
-    for i in list(range(0, int(prefix))):
-        idx = int(i / 8)
-        mask[idx] = mask[idx] + (1 << (7 - i % 8))
-    return ".".join([str(x) for x in mask])
+    mask = socket.inet_ntoa(
+        struct.pack(">I", (0xffffffff << (32 - int(prefix)) & 0xffffffff)))
+    return mask
 
 
 def ipv4_mask_to_net_prefix(mask):
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 993b26c..f090616 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: '):
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
index 86bfa5d..c8998b4 100644
--- a/cloudinit/sources/DataSourceSmartOS.py
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -1,4 +1,5 @@
 # Copyright (C) 2013 Canonical Ltd.
+# Copyright (c) 2018, Joyent, Inc.
 #
 # Author: Ben Howard <ben.howard@xxxxxxxxxxxxx>
 #
@@ -21,6 +22,7 @@
 
 import base64
 import binascii
+import errno
 import json
 import os
 import random
@@ -108,7 +110,7 @@ BUILTIN_CLOUD_CONFIG = {
                        'overwrite': False}
     },
     'fs_setup': [{'label': 'ephemeral0',
-                  'filesystem': 'ext3',
+                  'filesystem': 'ext4',
                   'device': 'ephemeral0'}],
 }
 
@@ -229,6 +231,9 @@ class DataSourceSmartOS(sources.DataSource):
                       self.md_client)
             return False
 
+        # Open once for many requests, rather than once for each request
+        self.md_client.open_transport()
+
         for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items():
             smartos_noun, strip = attribute
             md[ci_noun] = self.md_client.get(smartos_noun, strip=strip)
@@ -236,6 +241,8 @@ class DataSourceSmartOS(sources.DataSource):
         for ci_noun, smartos_noun in SMARTOS_ATTRIB_JSON.items():
             md[ci_noun] = self.md_client.get_json(smartos_noun)
 
+        self.md_client.close_transport()
+
         # @datadictionary: This key may contain a program that is written
         # to a file in the filesystem of the guest on each boot and then
         # executed. It may be of any format that would be considered
@@ -316,6 +323,10 @@ class JoyentMetadataFetchException(Exception):
     pass
 
 
+class JoyentMetadataTimeoutException(JoyentMetadataFetchException):
+    pass
+
+
 class JoyentMetadataClient(object):
     """
     A client implementing v2 of the Joyent Metadata Protocol Specification.
@@ -360,6 +371,47 @@ class JoyentMetadataClient(object):
         LOG.debug('Value "%s" found.', value)
         return value
 
+    def _readline(self):
+        """
+           Reads a line a byte at a time until \n is encountered.  Returns an
+           ascii string with the trailing newline removed.
+
+           If a timeout (per-byte) is set and it expires, a
+           JoyentMetadataFetchException will be thrown.
+        """
+        response = []
+
+        def as_ascii():
+            return b''.join(response).decode('ascii')
+
+        msg = "Partial response: '%s'"
+        while True:
+            try:
+                byte = self.fp.read(1)
+                if len(byte) == 0:
+                    raise JoyentMetadataTimeoutException(msg % as_ascii())
+                if byte == b'\n':
+                    return as_ascii()
+                response.append(byte)
+            except OSError as exc:
+                if exc.errno == errno.EAGAIN:
+                    raise JoyentMetadataTimeoutException(msg % as_ascii())
+                raise
+
+    def _write(self, msg):
+        self.fp.write(msg.encode('ascii'))
+        self.fp.flush()
+
+    def _negotiate(self):
+        LOG.debug('Negotiating protocol V2')
+        self._write('NEGOTIATE V2\n')
+        response = self._readline()
+        LOG.debug('read "%s"', response)
+        if response != 'V2_OK':
+            raise JoyentMetadataFetchException(
+                'Invalid response "%s" to "NEGOTIATE V2"' % response)
+        LOG.debug('Negotiation complete')
+
     def request(self, rtype, param=None):
         request_id = '{0:08x}'.format(random.randint(0, 0xffffffff))
         message_body = ' '.join((request_id, rtype,))
@@ -374,18 +426,11 @@ class JoyentMetadataClient(object):
             self.open_transport()
             need_close = True
 
-        self.fp.write(msg.encode('ascii'))
-        self.fp.flush()
-
-        response = bytearray()
-        response.extend(self.fp.read(1))
-        while response[-1:] != b'\n':
-            response.extend(self.fp.read(1))
-
+        self._write(msg)
+        response = self._readline()
         if need_close:
             self.close_transport()
 
-        response = response.rstrip().decode('ascii')
         LOG.debug('Read "%s" from metadata transport.', response)
 
         if 'SUCCESS' not in response:
@@ -450,6 +495,7 @@ class JoyentMetadataSocketClient(JoyentMetadataClient):
         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
         sock.connect(self.socketpath)
         self.fp = sock.makefile('rwb')
+        self._negotiate()
 
     def exists(self):
         return os.path.exists(self.socketpath)
@@ -459,8 +505,9 @@ class JoyentMetadataSocketClient(JoyentMetadataClient):
 
 
 class JoyentMetadataSerialClient(JoyentMetadataClient):
-    def __init__(self, device, timeout=10, smartos_type=SMARTOS_ENV_KVM):
-        super(JoyentMetadataSerialClient, self).__init__(smartos_type)
+    def __init__(self, device, timeout=10, smartos_type=SMARTOS_ENV_KVM,
+                 fp=None):
+        super(JoyentMetadataSerialClient, self).__init__(smartos_type, fp)
         self.device = device
         self.timeout = timeout
 
@@ -468,10 +515,50 @@ class JoyentMetadataSerialClient(JoyentMetadataClient):
         return os.path.exists(self.device)
 
     def open_transport(self):
-        ser = serial.Serial(self.device, timeout=self.timeout)
-        if not ser.isOpen():
-            raise SystemError("Unable to open %s" % self.device)
-        self.fp = ser
+        if self.fp is None:
+            ser = serial.Serial(self.device, timeout=self.timeout)
+            if not ser.isOpen():
+                raise SystemError("Unable to open %s" % self.device)
+            self.fp = ser
+        self._flush()
+        self._negotiate()
+
+    def _flush(self):
+        LOG.debug('Flushing input')
+        # Read any pending data
+        timeout = self.fp.timeout
+        self.fp.timeout = 0.1
+        while True:
+            try:
+                self._readline()
+            except JoyentMetadataTimeoutException:
+                break
+        LOG.debug('Input empty')
+
+        # Send a newline and expect "invalid command".  Keep trying until
+        # successful.  Retry rather frequently so that the "Is the host
+        # metadata service running" appears on the console soon after someone
+        # attaches in an effort to debug.
+        if timeout > 5:
+            self.fp.timeout = 5
+        else:
+            self.fp.timeout = timeout
+        while True:
+            LOG.debug('Writing newline, expecting "invalid command"')
+            self._write('\n')
+            try:
+                response = self._readline()
+                if response == 'invalid command':
+                    break
+                if response == 'FAILURE':
+                    LOG.debug('Got "FAILURE".  Retrying.')
+                    continue
+                LOG.warning('Unexpected response "%s" during flush', response)
+            except JoyentMetadataTimeoutException:
+                LOG.warning('Timeout while initializing metadata client. ' +
+                            'Is the host metadata service running?')
+        LOG.debug('Got "invalid command".  Flush complete.')
+        self.fp.timeout = timeout
 
     def __repr__(self):
         return "%s(device=%s, timeout=%s)" % (
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 999b1d7..82fd347 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -190,35 +190,11 @@ class ResourceUsingTestCase(CiTestCase):
         super(ResourceUsingTestCase, self).setUp()
         self.resource_path = None
 
-    def resourceLocation(self, subname=None):
-        if self.resource_path is None:
-            paths = [
-                os.path.join('tests', 'data'),
-                os.path.join('data'),
-                os.path.join(os.pardir, 'tests', 'data'),
-                os.path.join(os.pardir, 'data'),
-            ]
-            for p in paths:
-                if os.path.isdir(p):
-                    self.resource_path = p
-                    break
-        self.assertTrue((self.resource_path and
-                         os.path.isdir(self.resource_path)),
-                        msg="Unable to locate test resource data path!")
-        if not subname:
-            return self.resource_path
-        return os.path.join(self.resource_path, subname)
-
-    def readResource(self, name):
-        where = self.resourceLocation(name)
-        with open(where, 'r') as fh:
-            return fh.read()
-
     def getCloudPaths(self, ds=None):
         tmpdir = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, tmpdir)
         cp = ch.Paths({'cloud_dir': tmpdir,
-                       'templates_dir': self.resourceLocation()},
+                       'templates_dir': resourceLocation()},
                       ds=ds)
         return cp
 
@@ -234,7 +210,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):
         ResourceUsingTestCase.tearDown(self)
 
     def replicateTestRoot(self, example_root, target_root):
-        real_root = self.resourceLocation()
+        real_root = resourceLocation()
         real_root = os.path.join(real_root, 'roots', example_root)
         for (dir_path, _dirnames, filenames) in os.walk(real_root):
             real_path = dir_path
@@ -399,6 +375,18 @@ def wrap_and_call(prefix, mocks, func, *args, **kwargs):
             p.stop()
 
 
+def resourceLocation(subname=None):
+    path = os.path.join('tests', 'data')
+    if not subname:
+        return path
+    return os.path.join(path, subname)
+
+
+def readResource(name, mode='r'):
+    with open(resourceLocation(name), mode) as fh:
+        return fh.read()
+
+
 try:
     skipIf = unittest.skipIf
 except AttributeError:
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
index 7dea2e4..2537c1c 100644
--- a/cloudinit/tests/test_netinfo.py
+++ b/cloudinit/tests/test_netinfo.py
@@ -2,105 +2,121 @@
 
 """Tests netinfo module functions and classes."""
 
+from copy import copy
+
 from cloudinit.netinfo import netdev_pformat, route_pformat
-from cloudinit.tests.helpers import CiTestCase, mock
+from cloudinit.tests.helpers import CiTestCase, mock, readResource
 
 
 # Example ifconfig and route output
-SAMPLE_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
-          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
-          RX packets:8106427 errors:55 dropped:0 overruns:0 frame:37
-          TX packets:9339739 errors:0 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:1000
-          RX bytes:4953721719 (4.9 GB)  TX bytes:7731890194 (7.7 GB)
-          Interrupt:20 Memory:e1200000-e1220000
-
-lo        Link encap:Local Loopback
-          inet addr:127.0.0.1  Mask:255.0.0.0
-          inet6 addr: ::1/128 Scope:Host
-          UP LOOPBACK RUNNING  MTU:65536  Metric:1
-          RX packets:579230851 errors:0 dropped:0 overruns:0 frame:0
-          TX packets:579230851 errors:0 dropped:0 overruns:0 carrier:0
-          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'])
-
-
-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 |'
-    '         .         |',
-    '+---------+------+------------------------------+---------------+-------+'
-    '-------------------+'])
-
-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   |',
-    '+-------+-------------+-------------+---------------+---------------+'
-    '-----------------+-------+'])
+SAMPLE_OLD_IFCONFIG_OUT = readResource("netinfo/old-ifconfig-output")
+SAMPLE_NEW_IFCONFIG_OUT = readResource("netinfo/new-ifconfig-output")
+SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
+SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
+SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
+SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
+SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
+NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
+ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
 
 
 class TestNetInfo(CiTestCase):
 
     maxDiff = None
+    with_logs = True
+
+    @mock.patch('cloudinit.netinfo.util.which')
+    @mock.patch('cloudinit.netinfo.util.subp')
+    def test_netdev_old_nettools_pformat(self, m_subp, m_which):
+        """netdev_pformat properly rendering old nettools info."""
+        m_subp.return_value = (SAMPLE_OLD_IFCONFIG_OUT, '')
+        m_which.side_effect = lambda x: x if x == 'ifconfig' else None
+        content = netdev_pformat()
+        self.assertEqual(NETDEV_FORMATTED_OUT, content)
 
+    @mock.patch('cloudinit.netinfo.util.which')
     @mock.patch('cloudinit.netinfo.util.subp')
-    def test_netdev_pformat(self, m_subp):
-        """netdev_pformat properly rendering network device information."""
-        m_subp.return_value = (SAMPLE_IFCONFIG_OUT, '')
+    def test_netdev_new_nettools_pformat(self, m_subp, m_which):
+        """netdev_pformat properly rendering netdev new nettools info."""
+        m_subp.return_value = (SAMPLE_NEW_IFCONFIG_OUT, '')
+        m_which.side_effect = lambda x: x if x == 'ifconfig' else None
         content = netdev_pformat()
         self.assertEqual(NETDEV_FORMATTED_OUT, content)
 
+    @mock.patch('cloudinit.netinfo.util.which')
+    @mock.patch('cloudinit.netinfo.util.subp')
+    def test_netdev_iproute_pformat(self, m_subp, m_which):
+        """netdev_pformat properly rendering ip route info."""
+        m_subp.return_value = (SAMPLE_IPADDRSHOW_OUT, '')
+        m_which.side_effect = lambda x: x if x == 'ip' else None
+        content = netdev_pformat()
+        new_output = copy(NETDEV_FORMATTED_OUT)
+        # ip route show describes global scopes on ipv4 addresses
+        # whereas ifconfig does not. Add proper global/host scope to output.
+        new_output = new_output.replace('|   .    | 50:7b', '| global | 50:7b')
+        new_output = new_output.replace(
+            '255.0.0.0   |   .    |', '255.0.0.0   |  host  |')
+        self.assertEqual(new_output, content)
+
+    @mock.patch('cloudinit.netinfo.util.which')
+    @mock.patch('cloudinit.netinfo.util.subp')
+    def test_netdev_warn_on_missing_commands(self, m_subp, m_which):
+        """netdev_pformat warns when missing both ip and 'netstat'."""
+        m_which.return_value = None  # Niether ip nor netstat found
+        content = netdev_pformat()
+        self.assertEqual('\n', content)
+        self.assertEqual(
+            "WARNING: Could not print networks: missing 'ip' and 'ifconfig'"
+            " commands\n",
+            self.logs.getvalue())
+        m_subp.assert_not_called()
+
+    @mock.patch('cloudinit.netinfo.util.which')
     @mock.patch('cloudinit.netinfo.util.subp')
-    def test_route_pformat(self, m_subp):
-        """netdev_pformat properly rendering network device information."""
-        m_subp.return_value = (SAMPLE_ROUTE_OUT, '')
+    def test_route_nettools_pformat(self, m_subp, m_which):
+        """route_pformat properly rendering nettools route info."""
+
+        def subp_netstat_route_selector(*args, **kwargs):
+            if args[0] == ['netstat', '--route', '--numeric', '--extend']:
+                return (SAMPLE_ROUTE_OUT_V4, '')
+            if args[0] == ['netstat', '-A', 'inet6', '--route', '--numeric']:
+                return (SAMPLE_ROUTE_OUT_V6, '')
+            raise Exception('Unexpected subp call %s' % args[0])
+
+        m_subp.side_effect = subp_netstat_route_selector
+        m_which.side_effect = lambda x: x if x == 'netstat' else None
         content = route_pformat()
         self.assertEqual(ROUTE_FORMATTED_OUT, content)
+
+    @mock.patch('cloudinit.netinfo.util.which')
+    @mock.patch('cloudinit.netinfo.util.subp')
+    def test_route_iproute_pformat(self, m_subp, m_which):
+        """route_pformat properly rendering ip route info."""
+
+        def subp_iproute_selector(*args, **kwargs):
+            if ['ip', '-o', 'route', 'list'] == args[0]:
+                return (SAMPLE_IPROUTE_OUT_V4, '')
+            v6cmd = ['ip', '--oneline', '-6', 'route', 'list', 'table', 'all']
+            if v6cmd == args[0]:
+                return (SAMPLE_IPROUTE_OUT_V6, '')
+            raise Exception('Unexpected subp call %s' % args[0])
+
+        m_subp.side_effect = subp_iproute_selector
+        m_which.side_effect = lambda x: x if x == 'ip' else None
+        content = route_pformat()
+        self.assertEqual(ROUTE_FORMATTED_OUT, content)
+
+    @mock.patch('cloudinit.netinfo.util.which')
+    @mock.patch('cloudinit.netinfo.util.subp')
+    def test_route_warn_on_missing_commands(self, m_subp, m_which):
+        """route_pformat warns when missing both ip and 'netstat'."""
+        m_which.return_value = None  # Niether ip nor netstat found
+        content = route_pformat()
+        self.assertEqual('\n', content)
+        self.assertEqual(
+            "WARNING: Could not print routes: missing 'ip' and 'netstat'"
+            " commands\n",
+            self.logs.getvalue())
+        m_subp.assert_not_called()
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/util.py b/cloudinit/util.py
index acdc0d8..1717b52 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -1446,7 +1446,7 @@ def get_config_logfiles(cfg):
     for fmt in get_output_cfg(cfg, None):
         if not fmt:
             continue
-        match = re.match('(?P<type>\||>+)\s*(?P<target>.*)', fmt)
+        match = re.match(r'(?P<type>\||>+)\s*(?P<target>.*)', fmt)
         if not match:
             continue
         target = match.group('target')
@@ -2275,8 +2275,8 @@ def parse_mount(path):
     # the regex is a bit complex. to better understand this regex see:
     # https://regex101.com/r/2F6c1k/1
     # https://regex101.com/r/T2en7a/1
-    regex = r'^(/dev/[\S]+|.*zroot\S*?) on (/[\S]*) ' + \
-            '(?=(?:type)[\s]+([\S]+)|\(([^,]*))'
+    regex = (r'^(/dev/[\S]+|.*zroot\S*?) on (/[\S]*) '
+             r'(?=(?:type)[\s]+([\S]+)|\(([^,]*))')
     for line in mount_locs:
         m = re.search(regex, line)
         if not m:
diff --git a/debian/changelog b/debian/changelog
index d3a4234..45016a5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
+cloud-init (18.2-14-g6d48d265-0ubuntu1) bionic; urgency=medium
+
+  * New upstream snapshot.
+    - net: Depend on iproute2's ip instead of net-tools ifconfig or route
+    - DataSourceSmartOS: fix hang when metadata service is down
+      [Mike Gerdts] (LP: #1667735)
+    - DataSourceSmartOS: change default fs on ephemeral disk from ext3 to
+      ext4. [Mike Gerdts] (LP: #1763511)
+    - pycodestyle: Fix invalid escape sequences in string literals.
+    - Implement bash completion script for cloud-init command line
+
+ -- Chad Smith <chad.smith@xxxxxxxxxxxxx>  Wed, 18 Apr 2018 15:25:53 -0600
+
 cloud-init (18.2-9-g49b562c9-0ubuntu1) bionic; urgency=medium
 
   * New upstream snapshot.
diff --git a/doc/examples/cloud-config-disk-setup.txt b/doc/examples/cloud-config-disk-setup.txt
index dd91477..43a62a2 100644
--- a/doc/examples/cloud-config-disk-setup.txt
+++ b/doc/examples/cloud-config-disk-setup.txt
@@ -37,7 +37,7 @@ fs_setup:
 # Default disk definitions for SmartOS
 # ------------------------------------
 
-device_aliases: {'ephemeral0': '/dev/sdb'}
+device_aliases: {'ephemeral0': '/dev/vdb'}
 disk_setup:
     ephemeral0:
          table_type: mbr
@@ -46,7 +46,7 @@ disk_setup:
 
 fs_setup:
     - label: ephemeral0
-      filesystem: ext3
+      filesystem: ext4
       device: ephemeral0.0
 
 # Cavaut for SmartOS: if ephemeral disk is not defined, then the disk will
diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in
index 6ab0d20..91faf3c 100644
--- a/packages/redhat/cloud-init.spec.in
+++ b/packages/redhat/cloud-init.spec.in
@@ -197,6 +197,7 @@ fi
 %dir                    %{_sysconfdir}/cloud/templates
 %config(noreplace)      %{_sysconfdir}/cloud/templates/*
 %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf
+%{_sysconfdir}/bash_completion.d/cloud-init
 
 %{_libexecdir}/%{name}
 %dir %{_sharedstatedir}/cloud
diff --git a/packages/suse/cloud-init.spec.in b/packages/suse/cloud-init.spec.in
index 86e18b1..bbb965a 100644
--- a/packages/suse/cloud-init.spec.in
+++ b/packages/suse/cloud-init.spec.in
@@ -136,6 +136,7 @@ mkdir -p %{buildroot}/var/lib/cloud
 %config(noreplace) %{_sysconfdir}/cloud/cloud.cfg.d/README
 %dir               %{_sysconfdir}/cloud/templates
 %config(noreplace) %{_sysconfdir}/cloud/templates/*
+%{_sysconfdir}/bash_completion.d/cloud-init
 
 # Python code is here...
 %{python_sitelib}/*
diff --git a/setup.py b/setup.py
index bc3f52a..85b2337 100755
--- a/setup.py
+++ b/setup.py
@@ -228,6 +228,7 @@ if not in_virtualenv():
         INITSYS_ROOTS[k] = "/" + INITSYS_ROOTS[k]
 
 data_files = [
+    (ETC + '/bash_completion.d', ['bash_completion/cloud-init']),
     (ETC + '/cloud', [render_tmpl("config/cloud.cfg.tmpl")]),
     (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),
     (ETC + '/cloud/templates', glob('templates/*')),
diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
index 7598d46..4fda8f9 100644
--- a/tests/cloud_tests/testcases/base.py
+++ b/tests/cloud_tests/testcases/base.py
@@ -235,7 +235,7 @@ class CloudTestCase(unittest.TestCase):
             'found unexpected kvm availability-zone %s' %
             v1_data['availability-zone'])
         self.assertIsNotNone(
-            re.match('[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}',
+            re.match(r'[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}',
                      v1_data['instance-id']),
             'kvm instance-id is not a UUID: %s' % v1_data['instance-id'])
         self.assertIn('ubuntu', v1_data['local-hostname'])
diff --git a/tests/data/netinfo/netdev-formatted-output b/tests/data/netinfo/netdev-formatted-output
new file mode 100644
index 0000000..283ab4a
--- /dev/null
+++ b/tests/data/netinfo/netdev-formatted-output
@@ -0,0 +1,10 @@
++++++++++++++++++++++++++++++++++++++++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::7777:2222:1111:eeee/64 |       .       | global | 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  |         .         |
++---------+------+------------------------------+---------------+--------+-------------------+
diff --git a/tests/data/netinfo/new-ifconfig-output b/tests/data/netinfo/new-ifconfig-output
new file mode 100644
index 0000000..83d4ad1
--- /dev/null
+++ b/tests/data/netinfo/new-ifconfig-output
@@ -0,0 +1,18 @@
+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::7777:2222:1111:eeee  prefixlen 64  scopeid 0x30<global>
+        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
diff --git a/tests/data/netinfo/old-ifconfig-output b/tests/data/netinfo/old-ifconfig-output
new file mode 100644
index 0000000..e01f763
--- /dev/null
+++ b/tests/data/netinfo/old-ifconfig-output
@@ -0,0 +1,18 @@
+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::7777:2222:1111:eeee/64 Scope:Global
+          inet6 addr: fe80::8107:2b92:867e:f8a6/64 Scope:Link
+          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
+          RX packets:8106427 errors:55 dropped:0 overruns:0 frame:37
+          TX packets:9339739 errors:0 dropped:0 overruns:0 carrier:0
+          collisions:0 txqueuelen:1000
+          RX bytes:4953721719 (4.9 GB)  TX bytes:7731890194 (7.7 GB)
+          Interrupt:20 Memory:e1200000-e1220000
+
+lo        Link encap:Local Loopback
+          inet addr:127.0.0.1  Mask:255.0.0.0
+          inet6 addr: ::1/128 Scope:Host
+          UP LOOPBACK RUNNING  MTU:65536  Metric:1
+          RX packets:579230851 errors:0 dropped:0 overruns:0 frame:0
+          TX packets:579230851 errors:0 dropped:0 overruns:0 carrier:0
+          collisions:0 txqueuelen:1
diff --git a/tests/data/netinfo/route-formatted-output b/tests/data/netinfo/route-formatted-output
new file mode 100644
index 0000000..9d2c5dd
--- /dev/null
+++ b/tests/data/netinfo/route-formatted-output
@@ -0,0 +1,22 @@
++++++++++++++++++++++++++++++Route IPv4 info+++++++++++++++++++++++++++++
++-------+-------------+-------------+---------------+-----------+-------+
+| Route | Destination |   Gateway   |    Genmask    | Interface | Flags |
++-------+-------------+-------------+---------------+-----------+-------+
+|   0   |   0.0.0.0   | 192.168.2.1 |    0.0.0.0    |  enp0s25  |   UG  |
+|   1   |   0.0.0.0   | 192.168.2.1 |    0.0.0.0    |   wlp3s0  |   UG  |
+|   2   | 192.168.2.0 |   0.0.0.0   | 255.255.255.0 |  enp0s25  |   U   |
++-------+-------------+-------------+---------------+-----------+-------+
++++++++++++++++++++++++++++++++++++Route IPv6 info+++++++++++++++++++++++++++++++++++
++-------+---------------------------+---------------------------+-----------+-------+
+| Route |        Destination        |          Gateway          | Interface | Flags |
++-------+---------------------------+---------------------------+-----------+-------+
+|   0   |  2a00:abcd:82ae:cd33::657 |             ::            |  enp0s25  |   Ue  |
+|   1   |  2a00:abcd:82ae:cd33::/64 |             ::            |  enp0s25  |   U   |
+|   2   |  2a00:abcd:82ae:cd33::/56 | fe80::32ee:54de:cd43:b4e1 |  enp0s25  |   UG  |
+|   3   |     fd81:123f:654::657    |             ::            |  enp0s25  |   U   |
+|   4   |     fd81:123f:654::/64    |             ::            |  enp0s25  |   U   |
+|   5   |     fd81:123f:654::/48    | fe80::32ee:54de:cd43:b4e1 |  enp0s25  |   UG  |
+|   6   | fe80::abcd:ef12:bc34:da21 |             ::            |  enp0s25  |   U   |
+|   7   |         fe80::/64         |             ::            |  enp0s25  |   U   |
+|   8   |            ::/0           | fe80::32ee:54de:cd43:b4e1 |  enp0s25  |   UG  |
++-------+---------------------------+---------------------------+-----------+-------+
diff --git a/tests/data/netinfo/sample-ipaddrshow-output b/tests/data/netinfo/sample-ipaddrshow-output
new file mode 100644
index 0000000..b2fa267
--- /dev/null
+++ b/tests/data/netinfo/sample-ipaddrshow-output
@@ -0,0 +1,13 @@
+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
+    inet 127.0.0.1/8 scope host lo\       valid_lft forever preferred_lft forever
+    inet6 ::1/128 scope host \       valid_lft forever preferred_lft forever
+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
+    inet 192.168.2.18/24 brd 192.168.2.255 scope global dynamic enp0s25
+       valid_lft 84174sec preferred_lft 84174sec
+    inet6 fe80::7777:2222:1111:eeee/64 scope global
+       valid_lft forever preferred_lft forever
+    inet6 fe80::8107:2b92:867e:f8a6/64 scope link
+       valid_lft forever preferred_lft forever
+
diff --git a/tests/data/netinfo/sample-iproute-output-v4 b/tests/data/netinfo/sample-iproute-output-v4
new file mode 100644
index 0000000..904cb03
--- /dev/null
+++ b/tests/data/netinfo/sample-iproute-output-v4
@@ -0,0 +1,3 @@
+default via 192.168.2.1 dev enp0s25 proto static metric 100
+default via 192.168.2.1 dev wlp3s0 proto static metric 150
+192.168.2.0/24 dev enp0s25 proto kernel scope link src 192.168.2.18 metric 100
diff --git a/tests/data/netinfo/sample-iproute-output-v6 b/tests/data/netinfo/sample-iproute-output-v6
new file mode 100644
index 0000000..12bb1c1
--- /dev/null
+++ b/tests/data/netinfo/sample-iproute-output-v6
@@ -0,0 +1,11 @@
+2a00:abcd:82ae:cd33::657 dev enp0s25 proto kernel metric 256 expires 2334sec pref medium
+2a00:abcd:82ae:cd33::/64 dev enp0s25 proto ra metric 100 pref medium
+2a00:abcd:82ae:cd33::/56 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium
+fd81:123f:654::657 dev enp0s25 proto kernel metric 256 pref medium
+fd81:123f:654::/64 dev enp0s25 proto ra metric 100 pref medium
+fd81:123f:654::/48 via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto ra metric 100 pref medium
+fe80::abcd:ef12:bc34:da21 dev enp0s25 proto static metric 100 pref medium
+fe80::/64 dev enp0s25 proto kernel metric 256 pref medium
+default via fe80::32ee:54de:cd43:b4e1 dev enp0s25 proto static metric 100 pref medium
+local ::1 dev lo  table local  proto none  metric 0  pref medium
+local 2600:1f16:b80:ad00:90a:c915:bca6:5ff2 dev lo  table local  proto none  metric 0  pref medium
diff --git a/tests/data/netinfo/sample-route-output-v4 b/tests/data/netinfo/sample-route-output-v4
new file mode 100644
index 0000000..ecc31d9
--- /dev/null
+++ b/tests/data/netinfo/sample-route-output-v4
@@ -0,0 +1,5 @@
+Kernel IP routing table
+Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
+0.0.0.0         192.168.2.1     0.0.0.0         UG        100 0          0 enp0s25
+0.0.0.0         192.168.2.1     0.0.0.0         UG        150 0          0 wlp3s0
+192.168.2.0     0.0.0.0         255.255.255.0   U         100 0          0 enp0s25
diff --git a/tests/data/netinfo/sample-route-output-v6 b/tests/data/netinfo/sample-route-output-v6
new file mode 100644
index 0000000..4712b73
--- /dev/null
+++ b/tests/data/netinfo/sample-route-output-v6
@@ -0,0 +1,13 @@
+Kernel IPv6 routing table
+Destination                     Next Hop                   Flag Met Re  Use If
+2a00:abcd:82ae:cd33::657/128    ::                         Ue   256 1     0 enp0s25
+2a00:abcd:82ae:cd33::/64        ::                         U    100 1     0 enp0s25
+2a00:abcd:82ae:cd33::/56        fe80::32ee:54de:cd43:b4e1  UG   100 1     0 enp0s25
+fd81:123f:654::657/128          ::                         U    256 1     0 enp0s25
+fd81:123f:654::/64              ::                         U    100 1     0 enp0s25
+fd81:123f:654::/48              fe80::32ee:54de:cd43:b4e1  UG   100 1     0 enp0s25
+fe80::abcd:ef12:bc34:da21/128   ::                         U    100 1     2 enp0s25
+fe80::/64                       ::                         U    256 1 16880 enp0s25
+::/0                            fe80::32ee:54de:cd43:b4e1  UG   100 1     0 enp0s25
+::/0                            ::                         !n   -1  1424956 lo
+::1/128                         ::                         Un   0   4 26289 lo
diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py
index 88bae5f..2bea7a1 100644
--- a/tests/unittests/test_datasource/test_smartos.py
+++ b/tests/unittests/test_datasource/test_smartos.py
@@ -1,4 +1,5 @@
 # Copyright (C) 2013 Canonical Ltd.
+# Copyright (c) 2018, Joyent, Inc.
 #
 # Author: Ben Howard <ben.howard@xxxxxxxxxxxxx>
 #
@@ -324,6 +325,7 @@ class PsuedoJoyentClient(object):
         if data is None:
             data = MOCK_RETURNS.copy()
         self.data = data
+        self._is_open = False
         return
 
     def get(self, key, default=None, strip=False):
@@ -344,6 +346,14 @@ class PsuedoJoyentClient(object):
     def exists(self):
         return True
 
+    def open_transport(self):
+        assert(not self._is_open)
+        self._is_open = True
+
+    def close_transport(self):
+        assert(self._is_open)
+        self._is_open = False
+
 
 class TestSmartOSDataSource(FilesystemMockingTestCase):
     def setUp(self):
@@ -592,8 +602,46 @@ class TestSmartOSDataSource(FilesystemMockingTestCase):
                          mydscfg['disk_aliases']['FOO'])
 
 
+class ShortReader(object):
+    """Implements a 'read' interface for bytes provided.
+    much like io.BytesIO but the 'endbyte' acts as if EOF.
+    When it is reached a short will be returned."""
+    def __init__(self, initial_bytes, endbyte=b'\0'):
+        self.data = initial_bytes
+        self.index = 0
+        self.len = len(self.data)
+        self.endbyte = endbyte
+
+    @property
+    def emptied(self):
+        return self.index >= self.len
+
+    def read(self, size=-1):
+        """Read size bytes but not past a null."""
+        if size == 0 or self.index >= self.len:
+            return b''
+
+        rsize = size
+        if size < 0 or size + self.index > self.len:
+            rsize = self.len - self.index
+
+        next_null = self.data.find(self.endbyte, self.index, rsize)
+        if next_null >= 0:
+            rsize = next_null - self.index + 1
+        i = self.index
+        self.index += rsize
+        ret = self.data[i:i + rsize]
+        if len(ret) and ret[-1:] == self.endbyte:
+            ret = ret[:-1]
+        return ret
+
+
 class TestJoyentMetadataClient(FilesystemMockingTestCase):
 
+    invalid = b'invalid command\n'
+    failure = b'FAILURE\n'
+    v2_ok = b'V2_OK\n'
+
     def setUp(self):
         super(TestJoyentMetadataClient, self).setUp()
 
@@ -636,6 +684,11 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
         return DataSourceSmartOS.JoyentMetadataClient(
             fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM)
 
+    def _get_serial_client(self):
+        self.serial.timeout = 1
+        return DataSourceSmartOS.JoyentMetadataSerialClient(None,
+                                                            fp=self.serial)
+
     def assertEndsWith(self, haystack, prefix):
         self.assertTrue(haystack.endswith(prefix),
                         "{0} does not end with '{1}'".format(
@@ -646,12 +699,14 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
                         "{0} does not start with '{1}'".format(
                             repr(haystack), prefix))
 
+    def assertNoMoreSideEffects(self, obj):
+        self.assertRaises(StopIteration, obj)
+
     def test_get_metadata_writes_a_single_line(self):
         client = self._get_client()
         client.get('some_key')
         self.assertEqual(1, self.serial.write.call_count)
         written_line = self.serial.write.call_args[0][0]
-        print(type(written_line))
         self.assertEndsWith(written_line.decode('ascii'),
                             b'\n'.decode('ascii'))
         self.assertEqual(1, written_line.count(b'\n'))
@@ -737,6 +792,52 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
         client._checksum = lambda _: self.response_parts['crc']
         self.assertIsNone(client.get('some_key'))
 
+    def test_negotiate(self):
+        client = self._get_client()
+        reader = ShortReader(self.v2_ok)
+        client.fp.read.side_effect = reader.read
+        client._negotiate()
+        self.assertTrue(reader.emptied)
+
+    def test_negotiate_short_response(self):
+        client = self._get_client()
+        # chopped '\n' from v2_ok.
+        reader = ShortReader(self.v2_ok[:-1] + b'\0')
+        client.fp.read.side_effect = reader.read
+        self.assertRaises(DataSourceSmartOS.JoyentMetadataTimeoutException,
+                          client._negotiate)
+        self.assertTrue(reader.emptied)
+
+    def test_negotiate_bad_response(self):
+        client = self._get_client()
+        reader = ShortReader(b'garbage\n' + self.v2_ok)
+        client.fp.read.side_effect = reader.read
+        self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
+                          client._negotiate)
+        self.assertEqual(self.v2_ok, client.fp.read())
+
+    def test_serial_open_transport(self):
+        client = self._get_serial_client()
+        reader = ShortReader(b'garbage\0' + self.invalid + self.v2_ok)
+        client.fp.read.side_effect = reader.read
+        client.open_transport()
+        self.assertTrue(reader.emptied)
+
+    def test_flush_failure(self):
+        client = self._get_serial_client()
+        reader = ShortReader(b'garbage' + b'\0' + self.failure +
+                             self.invalid + self.v2_ok)
+        client.fp.read.side_effect = reader.read
+        client.open_transport()
+        self.assertTrue(reader.emptied)
+
+    def test_flush_many_timeouts(self):
+        client = self._get_serial_client()
+        reader = ShortReader(b'\0' * 100 + self.invalid + self.v2_ok)
+        client.fp.read.side_effect = reader.read
+        client.open_transport()
+        self.assertTrue(reader.emptied)
+
 
 class TestNetworkConversion(TestCase):
     def test_convert_simple(self):
diff --git a/tests/unittests/test_filters/test_launch_index.py b/tests/unittests/test_filters/test_launch_index.py
index 6364d38..e1a5d2c 100644
--- a/tests/unittests/test_filters/test_launch_index.py
+++ b/tests/unittests/test_filters/test_launch_index.py
@@ -55,7 +55,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
         return True
 
     def testMultiEmailIndex(self):
-        test_data = self.readResource('filter_cloud_multipart_2.email')
+        test_data = helpers.readResource('filter_cloud_multipart_2.email')
         ud_proc = ud.UserDataProcessor(self.getCloudPaths())
         message = ud_proc.process(test_data)
         self.assertTrue(count_messages(message) > 0)
@@ -70,7 +70,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
         self.assertCounts(message, expected_counts)
 
     def testHeaderEmailIndex(self):
-        test_data = self.readResource('filter_cloud_multipart_header.email')
+        test_data = helpers.readResource('filter_cloud_multipart_header.email')
         ud_proc = ud.UserDataProcessor(self.getCloudPaths())
         message = ud_proc.process(test_data)
         self.assertTrue(count_messages(message) > 0)
@@ -85,7 +85,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
         self.assertCounts(message, expected_counts)
 
     def testConfigEmailIndex(self):
-        test_data = self.readResource('filter_cloud_multipart_1.email')
+        test_data = helpers.readResource('filter_cloud_multipart_1.email')
         ud_proc = ud.UserDataProcessor(self.getCloudPaths())
         message = ud_proc.process(test_data)
         self.assertTrue(count_messages(message) > 0)
@@ -99,7 +99,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
         self.assertCounts(message, expected_counts)
 
     def testNoneIndex(self):
-        test_data = self.readResource('filter_cloud_multipart.yaml')
+        test_data = helpers.readResource('filter_cloud_multipart.yaml')
         ud_proc = ud.UserDataProcessor(self.getCloudPaths())
         message = ud_proc.process(test_data)
         start_count = count_messages(message)
@@ -108,7 +108,7 @@ class TestLaunchFilter(helpers.ResourceUsingTestCase):
         self.assertTrue(self.equivalentMessage(message, filtered_message))
 
     def testIndexes(self):
-        test_data = self.readResource('filter_cloud_multipart.yaml')
+        test_data = helpers.readResource('filter_cloud_multipart.yaml')
         ud_proc = ud.UserDataProcessor(self.getCloudPaths())
         message = ud_proc.process(test_data)
         start_count = count_messages(message)
diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py
index f51358d..3a5072c 100644
--- a/tests/unittests/test_merging.py
+++ b/tests/unittests/test_merging.py
@@ -100,7 +100,7 @@ def make_dict(max_depth, seed=None):
 
 class TestSimpleRun(helpers.ResourceUsingTestCase):
     def _load_merge_files(self):
-        merge_root = self.resourceLocation('merge_sources')
+        merge_root = helpers.resourceLocation('merge_sources')
         tests = []
         source_ids = collections.defaultdict(list)
         expected_files = {}
diff --git a/tests/unittests/test_runs/test_merge_run.py b/tests/unittests/test_runs/test_merge_run.py
index 5d3f1ca..d1ac494 100644
--- a/tests/unittests/test_runs/test_merge_run.py
+++ b/tests/unittests/test_runs/test_merge_run.py
@@ -25,7 +25,7 @@ class TestMergeRun(helpers.FilesystemMockingTestCase):
             'cloud_init_modules': ['write-files'],
             'system_info': {'paths': {'run_dir': new_root}}
         }
-        ud = self.readResource('user_data.1.txt')
+        ud = helpers.readResource('user_data.1.txt')
         cloud_cfg = util.yaml_dumps(cfg)
         util.ensure_dir(os.path.join(new_root, 'etc', 'cloud'))
         util.write_file(os.path.join(new_root, 'etc',
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index 5010190..e04ea03 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -325,7 +325,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
 
     def test_precise_ext4_root(self):
 
-        lines = self.readResource('mountinfo_precise_ext4.txt').splitlines()
+        lines = helpers.readResource('mountinfo_precise_ext4.txt').splitlines()
 
         expected = ('/dev/mapper/vg0-root', 'ext4', '/')
         self.assertEqual(expected, util.parse_mount_info('/', lines))
@@ -347,7 +347,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
         self.assertEqual(expected, util.parse_mount_info('/run/lock', lines))
 
     def test_raring_btrfs_root(self):
-        lines = self.readResource('mountinfo_raring_btrfs.txt').splitlines()
+        lines = helpers.readResource('mountinfo_raring_btrfs.txt').splitlines()
 
         expected = ('/dev/vda1', 'btrfs', '/')
         self.assertEqual(expected, util.parse_mount_info('/', lines))
@@ -373,7 +373,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
         m_os.path.exists.return_value = True
         # mock subp command from util.get_mount_info_fs_on_zpool
         zpool_output.return_value = (
-            self.readResource('zpool_status_simple.txt'), ''
+            helpers.readResource('zpool_status_simple.txt'), ''
         )
         # save function return values and do asserts
         ret = util.get_device_info_from_zpool('vmzroot')
@@ -406,7 +406,7 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
         m_os.path.exists.return_value = True
         # mock subp command from util.get_mount_info_fs_on_zpool
         zpool_output.return_value = (
-            self.readResource('zpool_status_simple.txt'), 'error'
+            helpers.readResource('zpool_status_simple.txt'), 'error'
         )
         # save function return values and do asserts
         ret = util.get_device_info_from_zpool('vmzroot')
@@ -414,7 +414,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
 
     @mock.patch('cloudinit.util.subp')
     def test_parse_mount_with_ext(self, mount_out):
-        mount_out.return_value = (self.readResource('mount_parse_ext.txt'), '')
+        mount_out.return_value = (
+            helpers.readResource('mount_parse_ext.txt'), '')
         # this one is valid and exists in mount_parse_ext.txt
         ret = util.parse_mount('/var')
         self.assertEqual(('/dev/mapper/vg00-lv_var', 'ext4', '/var'), ret)
@@ -430,7 +431,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase):
 
     @mock.patch('cloudinit.util.subp')
     def test_parse_mount_with_zfs(self, mount_out):
-        mount_out.return_value = (self.readResource('mount_parse_zfs.txt'), '')
+        mount_out.return_value = (
+            helpers.readResource('mount_parse_zfs.txt'), '')
         # this one is valid and exists in mount_parse_zfs.txt
         ret = util.parse_mount('/var')
         self.assertEqual(('vmzroot/ROOT/freebsd/var', 'zfs', '/var'), ret)
@@ -800,7 +802,7 @@ class TestSubp(helpers.CiTestCase):
 
         os.chmod(noshebang, os.stat(noshebang).st_mode | stat.S_IEXEC)
         self.assertRaisesRegex(util.ProcessExecutionError,
-                               'Missing #! in script\?',
+                               r'Missing #! in script\?',
                                util.subp, (noshebang,))
 
     def test_returns_none_if_no_capture(self):

Follow ups