← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master

 

Igor Galić has proposed merging ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master.

Commit message:
WIP / DO NOT MERGE YET / THIS IS JUST FOR REVIEWS

Requested reviews:
  cloud-init commiters (cloud-init-dev): refactor, architecture

For more details, see:
https://code.launchpad.net/~i.galic/cloud-init/+git/cloud-init/+merge/358228

refactor cloudinit.net for multi-platform usage
as per https://lists.launchpad.net/cloud-init/msg00187.html
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master.
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index ad98a59..f787c7a 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -13,94 +13,26 @@ import re
 from cloudinit.net.network_state import mask_to_net_prefix
 from cloudinit import util
 
-LOG = logging.getLogger(__name__)
-SYS_CLASS_NET = "/sys/class/net/"
-DEFAULT_PRIMARY_INTERFACE = 'eth0'
-
-
-def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
-    """Sorting for Humans: natural sort order. Can be use as the key to sort
-    functions.
-    This will sort ['eth0', 'ens3', 'ens10', 'ens12', 'ens8', 'ens0'] as
-    ['ens0', 'ens3', 'ens8', 'ens10', 'ens12', 'eth0'] instead of the simple
-    python way which will produce ['ens0', 'ens10', 'ens12', 'ens3', 'ens8',
-    'eth0']."""
-    return [int(text) if text.isdigit() else text.lower()
-            for text in re.split(_nsre, s)]
-
-
-def get_sys_class_path():
-    """Simple function to return the global SYS_CLASS_NET."""
-    return SYS_CLASS_NET
-
-
-def sys_dev_path(devname, path=""):
-    return get_sys_class_path() + devname + "/" + path
-
-
-def read_sys_net(devname, path, translate=None,
-                 on_enoent=None, on_keyerror=None,
-                 on_einval=None):
-    dev_path = sys_dev_path(devname, path)
-    try:
-        contents = util.load_file(dev_path)
-    except (OSError, IOError) as e:
-        e_errno = getattr(e, 'errno', None)
-        if e_errno in (errno.ENOENT, errno.ENOTDIR):
-            if on_enoent is not None:
-                return on_enoent(e)
-        if e_errno in (errno.EINVAL,):
-            if on_einval is not None:
-                return on_einval(e)
-        raise
-    contents = contents.strip()
-    if translate is None:
-        return contents
-    try:
-        return translate[contents]
-    except KeyError as e:
-        if on_keyerror is not None:
-            return on_keyerror(e)
-        else:
-            LOG.debug("Found unexpected (not translatable) value"
-                      " '%s' in '%s", contents, dev_path)
-            raise
-
-
-def read_sys_net_safe(iface, field, translate=None):
-    def on_excp_false(e):
-        return False
-    return read_sys_net(iface, field,
-                        on_keyerror=on_excp_false,
-                        on_enoent=on_excp_false,
-                        on_einval=on_excp_false,
-                        translate=translate)
+if not util.is_FreeBSD():
+    from cloudinit.net import linux as netimpl
+else:
+    from cloudinit.net import freebsd as netimpl
 
 
-def read_sys_net_int(iface, field):
-    val = read_sys_net_safe(iface, field)
-    if val is False:
-        return None
-    try:
-        return int(val)
-    except ValueError:
-        return None
+LOG = logging.getLogger(__name__)
+LO_DEVS = ['lo', 'lo0']
 
 
 def is_up(devname):
-    # The linux kernel says to consider devices in 'unknown'
-    # operstate as up for the purposes of network configuration. See
-    # Documentation/networking/operstates.txt in the kernel source.
-    translate = {'up': True, 'unknown': True, 'down': False}
-    return read_sys_net_safe(devname, "operstate", translate=translate)
+    return netimpl.is_up(devname)
 
 
 def is_wireless(devname):
-    return os.path.exists(sys_dev_path(devname, "wireless"))
+    return netimpl.is_wireless(devname)
 
 
 def is_bridge(devname):
-    return os.path.exists(sys_dev_path(devname, "bridge"))
+    return netimpl.is_wireless(devname)
 
 
 def is_bond(devname):
@@ -108,76 +40,35 @@ def is_bond(devname):
 
 
 def is_renamed(devname):
-    """
-    /* interface name assignment types (sysfs name_assign_type attribute) */
-    #define NET_NAME_UNKNOWN	0	/* unknown origin (not exposed to user) */
-    #define NET_NAME_ENUM		1	/* enumerated by kernel */
-    #define NET_NAME_PREDICTABLE	2	/* predictably named by the kernel */
-    #define NET_NAME_USER		3	/* provided by user-space */
-    #define NET_NAME_RENAMED	4	/* renamed by user-space */
-    """
-    name_assign_type = read_sys_net_safe(devname, 'name_assign_type')
-    if name_assign_type and name_assign_type in ['3', '4']:
-        return True
-    return False
+    return netimpl.is_renamed(devname)
 
 
 def is_vlan(devname):
-    uevent = str(read_sys_net_safe(devname, "uevent"))
-    return 'DEVTYPE=vlan' in uevent.splitlines()
+    return netimpl.is_renamed(devname)
 
 
 def is_connected(devname):
-    # is_connected isn't really as simple as that.  2 is
-    # 'physically connected'. 3 is 'not connected'. but a wlan interface will
-    # always show 3.
-    iflink = read_sys_net_safe(devname, "iflink")
-    if iflink == "2":
-        return True
-    if not is_wireless(devname):
-        return False
-    LOG.debug("'%s' is wireless, basing 'connected' on carrier", devname)
-    return read_sys_net_safe(devname, "carrier",
-                             translate={'0': False, '1': True})
+    return netimpl.is_connected(devname)
 
 
 def is_physical(devname):
-    return os.path.exists(sys_dev_path(devname, "device"))
+    return netimpl.is_physical(devname)
 
 
 def is_present(devname):
-    return os.path.exists(sys_dev_path(devname))
+    return netimpl.is_present(devname)
 
 
 def device_driver(devname):
-    """Return the device driver for net device named 'devname'."""
-    driver = None
-    driver_path = sys_dev_path(devname, "device/driver")
-    # driver is a symlink to the driver *dir*
-    if os.path.islink(driver_path):
-        driver = os.path.basename(os.readlink(driver_path))
-
-    return driver
+    return netimpl.device_driver(devname)
 
 
 def device_devid(devname):
-    """Return the device id string for net device named 'devname'."""
-    dev_id = read_sys_net_safe(devname, "device/device")
-    if dev_id is False:
-        return None
-
-    return dev_id
+    return netimpl.device_devid(devname)
 
 
 def get_devicelist():
-    try:
-        devs = os.listdir(get_sys_class_path())
-    except OSError as e:
-        if e.errno == errno.ENOENT:
-            devs = []
-        else:
-            raise
-    return devs
+    return netimpl.get_devicelist()
 
 
 class ParserError(Exception):
@@ -191,75 +82,22 @@ def is_disabled_cfg(cfg):
 
 
 def find_fallback_nic(blacklist_drivers=None):
-    """Return the name of the 'fallback' network device."""
-    if not blacklist_drivers:
-        blacklist_drivers = []
+    return netimpl.find_fallback_nic(blacklist_drivers)
 
-    if 'net.ifnames=0' in util.get_cmdline():
-        LOG.debug('Stable ifnames disabled by net.ifnames=0 in /proc/cmdline')
-    else:
-        unstable = [device for device in get_devicelist()
-                    if device != 'lo' and not is_renamed(device)]
-        if len(unstable):
-            LOG.debug('Found unstable nic names: %s; calling udevadm settle',
-                      unstable)
-            msg = 'Waiting for udev events to settle'
-            util.log_time(LOG.debug, msg, func=util.udevadm_settle)
-
-    # get list of interfaces that could have connections
-    invalid_interfaces = set(['lo'])
-    potential_interfaces = set([device for device in get_devicelist()
-                                if device_driver(device) not in
-                                blacklist_drivers])
-    potential_interfaces = potential_interfaces.difference(invalid_interfaces)
-    # sort into interfaces with carrier, interfaces which could have carrier,
-    # and ignore interfaces that are definitely disconnected
-    connected = []
-    possibly_connected = []
-    for interface in potential_interfaces:
-        if interface.startswith("veth"):
-            continue
-        if is_bridge(interface):
-            # skip any bridges
-            continue
-        if is_bond(interface):
-            # skip any bonds
-            continue
-        carrier = read_sys_net_int(interface, 'carrier')
-        if carrier:
-            connected.append(interface)
-            continue
-        # check if nic is dormant or down, as this may make a nick appear to
-        # not have a carrier even though it could acquire one when brought
-        # online by dhclient
-        dormant = read_sys_net_int(interface, 'dormant')
-        if dormant:
-            possibly_connected.append(interface)
-            continue
-        operstate = read_sys_net_safe(interface, 'operstate')
-        if operstate in ['dormant', 'down', 'lowerlayerdown', 'unknown']:
-            possibly_connected.append(interface)
-            continue
-
-    # don't bother with interfaces that might not be connected if there are
-    # some that definitely are
-    if connected:
-        potential_interfaces = connected
-    else:
-        potential_interfaces = possibly_connected
 
-    # if eth0 exists use it above anything else, otherwise get the interface
-    # that we can read 'first' (using the sorted defintion of first).
-    names = list(sorted(potential_interfaces, key=natural_sort_key))
-    if DEFAULT_PRIMARY_INTERFACE in names:
-        names.remove(DEFAULT_PRIMARY_INTERFACE)
-        names.insert(0, DEFAULT_PRIMARY_INTERFACE)
+def interface_has_own_mac(ifname, strict=False):
+    return netimpl.interface_has_own_mac(ifname, strict=strict)
+
 
-    # pick the first that has a mac-address
-    for name in names:
-        if read_sys_net_safe(name, 'address'):
-            return name
-    return None
+def _rename_interfaces(renames,
+                       strict_present=True,
+                       strict_busy=True,
+                       current_info=None):
+    return netimpl._rename_interfaces(
+        renames,
+        strict_present=strict_present,
+        strict_busy=strict_busy,
+        current_info=current_info)
 
 
 def generate_fallback_config(blacklist_drivers=None, config_driver=None):
@@ -271,10 +109,16 @@ def generate_fallback_config(blacklist_drivers=None, config_driver=None):
 
     target_name = find_fallback_nic(blacklist_drivers=blacklist_drivers)
     if target_name:
-        target_mac = read_sys_net_safe(target_name, 'address')
+        target_mac = get_interface_mac(target_name)
         nconf = {'config': [], 'version': 1}
-        cfg = {'type': 'physical', 'name': target_name,
-               'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]}
+        cfg = {
+            'type': 'physical',
+            'name': target_name,
+            'mac_address': target_mac,
+            'subnets': [{
+                'type': 'dhcp'
+            }]
+        }
         # inject the device driver name, dev_id into config if enabled and
         # device has a valid device driver value
         if config_driver:
@@ -348,295 +192,12 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
                        ' network config version: %s' % netcfg.get('version'))
 
 
-def interface_has_own_mac(ifname, strict=False):
-    """return True if the provided interface has its own address.
-
-    Based on addr_assign_type in /sys.  Return true for any interface
-    that does not have a 'stolen' address. Examples of such devices
-    are bonds or vlans that inherit their mac from another device.
-    Possible values are:
-      0: permanent address    2: stolen from another device
-      1: randomly generated   3: set using dev_set_mac_address"""
-
-    assign_type = read_sys_net_int(ifname, "addr_assign_type")
-    if assign_type is None:
-        # None is returned if this nic had no 'addr_assign_type' entry.
-        # if strict, raise an error, if not return True.
-        if strict:
-            raise ValueError("%s had no addr_assign_type.")
-        return True
-    return assign_type in (0, 1, 3)
-
-
-def _get_current_rename_info(check_downable=True):
-    """Collect information necessary for rename_interfaces.
-
-    returns a dictionary by mac address like:
-       {name:
-         {
-          'downable': None or boolean indicating that the
-                      device has only automatically assigned ip addrs.
-          'device_id': Device id value (if it has one)
-          'driver': Device driver (if it has one)
-          'mac': mac address (in lower case)
-          'name': name
-          'up': boolean: is_up(name)
-         }}
-    """
-    cur_info = {}
-    for (name, mac, driver, device_id) in get_interfaces():
-        cur_info[name] = {
-            'downable': None,
-            'device_id': device_id,
-            'driver': driver,
-            'mac': mac.lower(),
-            'name': name,
-            'up': is_up(name),
-        }
-
-    if check_downable:
-        nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]")
-        ipv6, _err = util.subp(['ip', '-6', 'addr', 'show', 'permanent',
-                                'scope', 'global'], capture=True)
-        ipv4, _err = util.subp(['ip', '-4', 'addr', 'show'], capture=True)
-
-        nics_with_addresses = set()
-        for bytes_out in (ipv6, ipv4):
-            nics_with_addresses.update(nmatch.findall(bytes_out))
-
-        for d in cur_info.values():
-            d['downable'] = (d['up'] is False or
-                             d['name'] not in nics_with_addresses)
-
-    return cur_info
-
-
-def _rename_interfaces(renames, strict_present=True, strict_busy=True,
-                       current_info=None):
-
-    if not len(renames):
-        LOG.debug("no interfaces to rename")
-        return
-
-    if current_info is None:
-        current_info = _get_current_rename_info()
-
-    cur_info = {}
-    for name, data in current_info.items():
-        cur = data.copy()
-        if cur.get('mac'):
-            cur['mac'] = cur['mac'].lower()
-        cur['name'] = name
-        cur_info[name] = cur
-
-    def update_byname(bymac):
-        return dict((data['name'], data)
-                    for data in cur_info.values())
-
-    def rename(cur, new):
-        util.subp(["ip", "link", "set", cur, "name", new], capture=True)
-
-    def down(name):
-        util.subp(["ip", "link", "set", name, "down"], capture=True)
-
-    def up(name):
-        util.subp(["ip", "link", "set", name, "up"], capture=True)
-
-    ops = []
-    errors = []
-    ups = []
-    cur_byname = update_byname(cur_info)
-    tmpname_fmt = "cirename%d"
-    tmpi = -1
-
-    def entry_match(data, mac, driver, device_id):
-        """match if set and in data"""
-        if mac and driver and device_id:
-            return (data['mac'] == mac and
-                    data['driver'] == driver and
-                    data['device_id'] == device_id)
-        elif mac and driver:
-            return (data['mac'] == mac and
-                    data['driver'] == driver)
-        elif mac:
-            return (data['mac'] == mac)
-
-        return False
-
-    def find_entry(mac, driver, device_id):
-        match = [data for data in cur_info.values()
-                 if entry_match(data, mac, driver, device_id)]
-        if len(match):
-            if len(match) > 1:
-                msg = ('Failed to match a single device. Matched devices "%s"'
-                       ' with search values "(mac:%s driver:%s device_id:%s)"'
-                       % (match, mac, driver, device_id))
-                raise ValueError(msg)
-            return match[0]
-
-        return None
-
-    for mac, new_name, driver, device_id in renames:
-        if mac:
-            mac = mac.lower()
-        cur_ops = []
-        cur = find_entry(mac, driver, device_id)
-        if not cur:
-            if strict_present:
-                errors.append(
-                    "[nic not present] Cannot rename mac=%s to %s"
-                    ", not available." % (mac, new_name))
-            continue
-
-        cur_name = cur.get('name')
-        if cur_name == new_name:
-            # nothing to do
-            continue
-
-        if not cur_name:
-            if strict_present:
-                errors.append(
-                    "[nic not present] Cannot rename mac=%s to %s"
-                    ", not available." % (mac, new_name))
-            continue
-
-        if cur['up']:
-            msg = "[busy] Error renaming mac=%s from %s to %s"
-            if not cur['downable']:
-                if strict_busy:
-                    errors.append(msg % (mac, cur_name, new_name))
-                continue
-            cur['up'] = False
-            cur_ops.append(("down", mac, new_name, (cur_name,)))
-            ups.append(("up", mac, new_name, (new_name,)))
-
-        if new_name in cur_byname:
-            target = cur_byname[new_name]
-            if target['up']:
-                msg = "[busy-target] Error renaming mac=%s from %s to %s."
-                if not target['downable']:
-                    if strict_busy:
-                        errors.append(msg % (mac, cur_name, new_name))
-                    continue
-                else:
-                    cur_ops.append(("down", mac, new_name, (new_name,)))
-
-            tmp_name = None
-            while tmp_name is None or tmp_name in cur_byname:
-                tmpi += 1
-                tmp_name = tmpname_fmt % tmpi
-
-            cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
-            target['name'] = tmp_name
-            cur_byname = update_byname(cur_info)
-            if target['up']:
-                ups.append(("up", mac, new_name, (tmp_name,)))
-
-        cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
-        cur['name'] = new_name
-        cur_byname = update_byname(cur_info)
-        ops += cur_ops
-
-    opmap = {'rename': rename, 'down': down, 'up': up}
-
-    if len(ops) + len(ups) == 0:
-        if len(errors):
-            LOG.debug("unable to do any work for renaming of %s", renames)
-        else:
-            LOG.debug("no work necessary for renaming of %s", renames)
-    else:
-        LOG.debug("achieving renaming of %s with ops %s", renames, ops + ups)
-
-        for op, mac, new_name, params in ops + ups:
-            try:
-                opmap.get(op)(*params)
-            except Exception as e:
-                errors.append(
-                    "[unknown] Error performing %s%s for %s, %s: %s" %
-                    (op, params, mac, new_name, e))
-
-    if len(errors):
-        raise Exception('\n'.join(errors))
-
-
-def get_interface_mac(ifname):
-    """Returns the string value of an interface's MAC Address"""
-    path = "address"
-    if os.path.isdir(sys_dev_path(ifname, "bonding_slave")):
-        # for a bond slave, get the nic's hwaddress, not the address it
-        # is using because its part of a bond.
-        path = "bonding_slave/perm_hwaddr"
-    return read_sys_net_safe(ifname, path)
-
-
-def get_ib_interface_hwaddr(ifname, ethernet_format):
-    """Returns the string value of an Infiniband interface's hardware
-    address. If ethernet_format is True, an Ethernet MAC-style 6 byte
-    representation of the address will be returned.
-    """
-    # Type 32 is Infiniband.
-    if read_sys_net_safe(ifname, 'type') == '32':
-        mac = get_interface_mac(ifname)
-        if mac and ethernet_format:
-            # Use bytes 13-15 and 18-20 of the hardware address.
-            mac = mac[36:-14] + mac[51:]
-        return mac
-
-
-def get_interfaces_by_mac():
-    """Build a dictionary of tuples {mac: name}.
-
-    Bridges and any devices that have a 'stolen' mac are excluded."""
-    ret = {}
-    for name, mac, _driver, _devid in get_interfaces():
-        if mac in ret:
-            raise RuntimeError(
-                "duplicate mac found! both '%s' and '%s' have mac '%s'" %
-                (name, ret[mac], mac))
-        ret[mac] = name
-        # Try to get an Infiniband hardware address (in 6 byte Ethernet format)
-        # for the interface.
-        ib_mac = get_ib_interface_hwaddr(name, True)
-        if ib_mac:
-            if ib_mac in ret:
-                raise RuntimeError(
-                    "duplicate mac found! both '%s' and '%s' have mac '%s'" %
-                    (name, ret[ib_mac], ib_mac))
-            ret[ib_mac] = name
-    return ret
-
-
-def get_interfaces():
-    """Return list of interface tuples (name, mac, driver, device_id)
-
-    Bridges and any devices that have a 'stolen' mac are excluded."""
-    ret = []
-    devs = get_devicelist()
-    # 16 somewhat arbitrarily chosen.  Normally a mac is 6 '00:' tokens.
-    zero_mac = ':'.join(('00',) * 16)
-    for name in devs:
-        if not interface_has_own_mac(name):
-            continue
-        if is_bridge(name):
-            continue
-        if is_vlan(name):
-            continue
-        mac = get_interface_mac(name)
-        # some devices may not have a mac (tun0)
-        if not mac:
-            continue
-        # skip nics that have no mac (00:00....)
-        if name != 'lo' and mac == zero_mac[:len(mac)]:
-            continue
-        ret.append((name, mac, device_driver(name), device_devid(name)))
-    return ret
-
-
 def get_ib_hwaddrs_by_interface():
     """Build a dictionary mapping Infiniband interface names to their hardware
     address."""
     ret = {}
-    for name, _, _, _ in get_interfaces():
+    from cloudinit.net import common
+    for name, _, _, _ in common.get_interfaces():
         ib_mac = get_ib_interface_hwaddr(name, False)
         if ib_mac:
             if ib_mac in ret:
@@ -646,6 +207,21 @@ def get_ib_hwaddrs_by_interface():
             ret[name] = ib_mac
     return ret
 
+def get_ib_interface_hwaddr(ifname, ethernet_format):
+    return netimpl.get_ib_interface_hwaddr(ifname, ethernet_format=ethernet_format)
+
+
+def get_interface_mac(ifname):
+    return netimpl.get_interface_mac(ifname)
+
+
+def net_setup_link(run=False):
+    return netimpl.net_setup_link(run)
+
+
+def get_interfaces_by_mac():
+    return netimpl.get_interfaces_by_mac()
+
 
 class EphemeralIPv4Network(object):
     """Context manager which sets up temporary static network configuration.
@@ -673,8 +249,7 @@ class EphemeralIPv4Network(object):
         try:
             self.prefix = mask_to_net_prefix(prefix_or_mask)
         except ValueError as e:
-            raise ValueError(
-                'Cannot setup network: {0}'.format(e))
+            raise ValueError('Cannot setup network: {0}'.format(e))
         self.interface = interface
         self.ip = ip
         self.broadcast = broadcast
@@ -693,62 +268,20 @@ class EphemeralIPv4Network(object):
             util.subp(cmd, capture=True)
 
     def _delete_address(self, address, prefix):
-        """Perform the ip command to remove the specified address."""
-        util.subp(
-            ['ip', '-family', 'inet', 'addr', 'del',
-             '%s/%s' % (address, prefix), 'dev', self.interface],
-            capture=True)
+        """Perform the command to remove the specified address."""
+
+    def _delete_address(self, address, prefix):
+        """Perform the command to remove the specified address."""
 
     def _bringup_device(self):
-        """Perform the ip comands to fully setup the device."""
-        cidr = '{0}/{1}'.format(self.ip, self.prefix)
-        LOG.debug(
-            'Attempting setup of ephemeral network on %s with %s brd %s',
-            self.interface, cidr, self.broadcast)
-        try:
-            util.subp(
-                ['ip', '-family', 'inet', 'addr', 'add', cidr, 'broadcast',
-                 self.broadcast, 'dev', self.interface],
-                capture=True, update_env={'LANG': 'C'})
-        except util.ProcessExecutionError as e:
-            if "File exists" not in e.stderr:
-                raise
-            LOG.debug(
-                'Skip ephemeral network setup, %s already has address %s',
-                self.interface, self.ip)
-        else:
-            # Address creation success, bring up device and queue cleanup
-            util.subp(
-                ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface,
-                 'up'], capture=True)
-            self.cleanup_cmds.append(
-                ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface,
-                 'down'])
-            self.cleanup_cmds.append(
-                ['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev',
-                 self.interface])
+        """Perform the comands to fully setup the device."""
 
-    def _bringup_router(self):
-        """Perform the ip commands to fully setup the router if needed."""
+    def bringup_router(self):
+        """Perform the commands to fully setup the router if needed."""
         # Check if a default route exists and exit if it does
-        out, _ = util.subp(['ip', 'route', 'show', '0.0.0.0/0'], capture=True)
-        if 'default' in out:
-            LOG.debug(
-                'Skip ephemeral route setup. %s already has default route: %s',
-                self.interface, out.strip())
-            return
-        util.subp(
-            ['ip', '-4', 'route', 'add', self.router, 'dev', self.interface,
-             'src', self.ip], capture=True)
-        self.cleanup_cmds.insert(
-            0,
-            ['ip', '-4', 'route', 'del', self.router, 'dev', self.interface,
-             'src', self.ip])
-        util.subp(
-            ['ip', '-4', 'route', 'add', 'default', 'via', self.router,
-             'dev', self.interface], capture=True)
-        self.cleanup_cmds.insert(
-            0, ['ip', '-4', 'route', 'del', 'default', 'dev', self.interface])
+
+    def _bringup_router(self):
+        """Perform the commands to fully setup the router if needed."""
 
 
 class RendererNotFoundError(RuntimeError):
diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
index f89a0f7..657e9f4 100755
--- a/cloudinit/net/cmdline.py
+++ b/cloudinit/net/cmdline.py
@@ -12,7 +12,7 @@ import io
 import os
 
 from . import get_devicelist
-from . import read_sys_net_safe
+from . import get_interface_mac
 
 from cloudinit import util
 
@@ -198,7 +198,7 @@ def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
     if mac_addrs is None:
         mac_addrs = {}
         for k in get_devicelist():
-            mac_addr = read_sys_net_safe(k, 'address')
+            mac_addr = get_interface_mac(k)
             if mac_addr:
                 mac_addrs[k] = mac_addr
 
diff --git a/cloudinit/net/common.py b/cloudinit/net/common.py
new file mode 100644
index 0000000..579739c
--- /dev/null
+++ b/cloudinit/net/common.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2018 Canonical Ltd.
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+"""
+This module is used for all high-level functions common to linux & freebsd,
+that are also used in linux & freebsd!
+They cannot be put them into net/__init__ because otherwise we'd end up with
+cyclic imports.
+"""
+
+from . import LO_DEVS, get_devicelist, interface_has_own_mac, is_bridge, is_vlan, get_interface_mac, device_driver, device_devid
+
+
+def get_interfaces():
+    """Return list of interface tuples (name, mac, driver, device_id)
+
+    Bridges and any devices that have a 'stolen' mac are excluded."""
+    ret = []
+    devs = get_devicelist()
+    # 16 somewhat arbitrarily chosen.  Normally a mac is 6 '00:' tokens.
+    zero_mac = ':'.join(('00', ) * 16)
+    for name in devs:
+        if not interface_has_own_mac(name):
+            continue
+        if is_bridge(name):
+            continue
+        if is_vlan(name):
+            continue
+        mac = get_interface_mac(name)
+        # some devices may not have a mac (tun0)
+        if not mac:
+            continue
+        # skip nics that have no mac (00:00....)
+        if name not in LO_DEVS and mac == zero_mac[:len(mac)]:
+            continue
+        ret.append((name, mac, device_driver(name), device_devid(name)))
+    return ret
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
new file mode 100644
index 0000000..17d5dbe
--- /dev/null
+++ b/cloudinit/net/freebsd.py
@@ -0,0 +1,125 @@
+# Copyright (C) 2018 Canonical Ltd.
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import errno
+import logging
+import os
+import re
+
+from cloudinit.net.network_state import mask_to_net_prefix
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+DEFAULT_PRIMARY_INTERFACE = 'vtnet0'
+
+def is_up(devname):
+    raise NotImplemented("TODO")
+
+
+def is_wireless(devname):
+    raise NotImplemented("TODO")
+
+
+def is_bridge(devname):
+    raise NotImplemented("TODO")
+
+
+def is_bond(devname):
+    raise NotImplemented("TODO")
+
+
+def is_renamed(devname):
+    raise NotImplemented("TODO")
+
+
+def is_vlan(devname):
+    raise NotImplemented("TODO")
+
+
+def is_connected(devname):
+    raise NotImplemented("TODO")
+
+
+def is_physical(devname):
+    raise NotImplemented("TODO")
+
+
+def is_present(devname):
+    raise NotImplemented("TODO")
+
+
+def device_driver(devname):
+    raise NotImplemented("TODO")
+
+
+def device_devid(devname):
+    raise NotImplemented("TODO")
+
+
+def get_devicelist():
+    raise NotImplemented("TODO")
+
+
+class ParserError(Exception):
+    """Raised when a parser has issue parsing a file/content."""
+
+
+def find_fallback_nic(blacklist_drivers=None):
+    raise NotImplemented("TODO")
+
+
+def interface_has_own_mac(ifname, strict=False):
+    raise NotImplemented("TODO")
+
+
+def _get_current_rename_info(check_downable=True):
+    """Collect information necessary for rename_interfaces.
+
+    returns a dictionary by mac address like:
+       {name:
+         {
+          'downable': None or boolean indicating that the
+                      device has only automatically assigned ip addrs.
+          'device_id': Device id value (if it has one)
+          'driver': Device driver (if it has one)
+          'mac': mac address (in lower case)
+          'name': name
+          'up': boolean: is_up(name)
+         }}
+    """
+    raise NotImplemented("TODO")
+
+def _rename_interfaces(renames, strict_present=True, strict_busy=True,
+                       current_info=None):
+
+    raise NotImplemented("TODO")
+
+
+def get_interface_mac(ifname):
+    raise NotImplemented("TODO")
+
+
+def get_ib_interface_hwaddr(ifname, ethernet_format):
+    raise NotImplemented("TODO")
+
+
+def get_interfaces_by_mac():
+    raise NotImplemented("TODO")
+
+
+
+
+class EphemeralIPv4Network(object):
+    """Context manager which sets up temporary static network configuration.
+
+    No operations are performed if the provided interface is already connected.
+    If unconnected, bring up the interface with valid ip, prefix and broadcast.
+    If router is provided setup a default route for that interface. Upon
+    context exit, clean up the interface leaving no configuration behind.
+    """
+
+    raise NotImplemented("TODO")
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/linux.py b/cloudinit/net/linux.py
new file mode 100644
index 0000000..40fda19
--- /dev/null
+++ b/cloudinit/net/linux.py
@@ -0,0 +1,624 @@
+# Copyright (C) 2018 Canonical Ltd.
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import errno
+import logging
+import os
+import re
+
+from cloudinit.net.network_state import mask_to_net_prefix
+from cloudinit import util
+
+
+LOG = logging.getLogger(__name__)
+SYS_CLASS_NET = "/sys/class/net/"
+DEFAULT_PRIMARY_INTERFACE = 'eth0'
+LO_DEVS = ['lo', 'lo0']
+
+
+def get_sys_class_path():
+    """Simple function to return the global SYS_CLASS_NET."""
+    return SYS_CLASS_NET
+
+
+def sys_dev_path(devname, path=""):
+    return get_sys_class_path() + devname + "/" + path
+
+
+def read_sys_net(devname, path, translate=None,
+                 on_enoent=None, on_keyerror=None,
+                 on_einval=None):
+    dev_path = sys_dev_path(devname, path)
+    try:
+        contents = util.load_file(dev_path)
+    except (OSError, IOError) as e:
+        e_errno = getattr(e, 'errno', None)
+        if e_errno in (errno.ENOENT, errno.ENOTDIR):
+            if on_enoent is not None:
+                return on_enoent(e)
+        if e_errno in (errno.EINVAL,):
+            if on_einval is not None:
+                return on_einval(e)
+        raise
+    contents = contents.strip()
+    if translate is None:
+        return contents
+    try:
+        return translate[contents]
+    except KeyError as e:
+        if on_keyerror is not None:
+            return on_keyerror(e)
+        else:
+            LOG.debug("Found unexpected (not translatable) value"
+                      " '%s' in '%s", contents, dev_path)
+            raise
+
+
+def read_sys_net_safe(iface, field, translate=None):
+    def on_excp_false(e):
+        return False
+    return read_sys_net(iface, field,
+                        on_keyerror=on_excp_false,
+                        on_enoent=on_excp_false,
+                        on_einval=on_excp_false,
+                        translate=translate)
+
+
+def read_sys_net_int(iface, field):
+    val = read_sys_net_safe(iface, field)
+    if val is False:
+        return None
+    try:
+        return int(val)
+    except ValueError:
+        return None
+
+
+def is_up(devname):
+    # The linux kernel says to consider devices in 'unknown'
+    # operstate as up for the purposes of network configuration. See
+    # Documentation/networking/operstates.txt in the kernel source.
+    translate = {'up': True, 'unknown': True, 'down': False}
+    return read_sys_net_safe(devname, "operstate", translate=translate)
+
+
+def is_wireless(devname):
+    return os.path.exists(sys_dev_path(devname, "wireless"))
+
+
+def is_bridge(devname):
+    return os.path.exists(sys_dev_path(devname, "bridge"))
+
+
+def is_bond(devname):
+    return os.path.exists(sys_dev_path(devname, "bonding"))
+
+
+def is_renamed(devname):
+    """
+    /* interface name assignment types (sysfs name_assign_type attribute) */
+    #define NET_NAME_UNKNOWN	0	/* unknown origin (not exposed to user) */
+    #define NET_NAME_ENUM		1	/* enumerated by kernel */
+    #define NET_NAME_PREDICTABLE	2	/* predictably named by the kernel */
+    #define NET_NAME_USER		3	/* provided by user-space */
+    #define NET_NAME_RENAMED	4	/* renamed by user-space */
+    """
+    name_assign_type = read_sys_net_safe(devname, 'name_assign_type')
+    if name_assign_type and name_assign_type in ['3', '4']:
+        return True
+    return False
+
+
+def is_vlan(devname):
+    uevent = str(read_sys_net_safe(devname, "uevent"))
+    return 'DEVTYPE=vlan' in uevent.splitlines()
+
+
+def is_connected(devname):
+    # is_connected isn't really as simple as that.  2 is
+    # 'physically connected'. 3 is 'not connected'. but a wlan interface will
+    # always show 3.
+    iflink = read_sys_net_safe(devname, "iflink")
+    if iflink == "2":
+        return True
+    if not is_wireless(devname):
+        return False
+    LOG.debug("'%s' is wireless, basing 'connected' on carrier", devname)
+    return read_sys_net_safe(devname, "carrier",
+                             translate={'0': False, '1': True})
+
+
+def is_physical(devname):
+    return os.path.exists(sys_dev_path(devname, "device"))
+
+
+def is_present(devname):
+    return os.path.exists(sys_dev_path(devname))
+
+
+def device_driver(devname):
+    """Return the device driver for net device named 'devname'."""
+    driver = None
+    driver_path = sys_dev_path(devname, "device/driver")
+    # driver is a symlink to the driver *dir*
+    if os.path.islink(driver_path):
+        driver = os.path.basename(os.readlink(driver_path))
+
+    return driver
+
+
+def device_devid(devname):
+    """Return the device id string for net device named 'devname'."""
+    dev_id = read_sys_net_safe(devname, "device/device")
+    if dev_id is False:
+        return None
+
+    return dev_id
+
+
+def get_devicelist():
+    try:
+        devs = os.listdir(get_sys_class_path())
+    except OSError as e:
+        if e.errno == errno.ENOENT:
+            devs = []
+        else:
+            raise
+    return devs
+
+
+def is_disabled_cfg(cfg):
+    if not cfg or not isinstance(cfg, dict):
+        return False
+    return cfg.get('config') == "disabled"
+
+
+def find_fallback_nic(blacklist_drivers=None):
+    """Return the name of the 'fallback' network device."""
+    if not blacklist_drivers:
+        blacklist_drivers = []
+
+    if 'net.ifnames=0' in util.get_cmdline():
+        LOG.debug('Stable ifnames disabled by net.ifnames=0 in /proc/cmdline')
+    else:
+        unstable = [device for device in get_devicelist()
+                    if device not in LO_DEVS and not is_renamed(device)]
+        if len(unstable):
+            LOG.debug('Found unstable nic names: %s; calling udevadm settle',
+                      unstable)
+            msg = 'Waiting for udev events to settle'
+            util.log_time(LOG.debug, msg, func=util.udevadm_settle)
+
+    # get list of interfaces that could have connections
+    invalid_interfaces = set(LO_DEVS)
+    potential_interfaces = set([device for device in get_devicelist()
+                                if device_driver(device) not in
+                                blacklist_drivers])
+    potential_interfaces = potential_interfaces.difference(invalid_interfaces)
+    # sort into interfaces with carrier, interfaces which could have carrier,
+    # and ignore interfaces that are definitely disconnected
+    connected = []
+    possibly_connected = []
+    for interface in potential_interfaces:
+        if interface.startswith("veth"):
+            continue
+        if is_bridge(interface):
+            # skip any bridges
+            continue
+        if is_bond(interface):
+            # skip any bonds
+            continue
+        carrier = read_sys_net_int(interface, 'carrier')
+        if carrier:
+            connected.append(interface)
+            continue
+        # check if nic is dormant or down, as this may make a nick appear to
+        # not have a carrier even though it could acquire one when brought
+        # online by dhclient
+        dormant = read_sys_net_int(interface, 'dormant')
+        if dormant:
+            possibly_connected.append(interface)
+            continue
+        operstate = read_sys_net_safe(interface, 'operstate')
+        if operstate in ['dormant', 'down', 'lowerlayerdown', 'unknown']:
+            possibly_connected.append(interface)
+            continue
+
+    # don't bother with interfaces that might not be connected if there are
+    # some that definitely are
+    if connected:
+        potential_interfaces = connected
+    else:
+        potential_interfaces = possibly_connected
+
+    # if eth0 exists use it above anything else, otherwise get the interface
+    # that we can read 'first' (using the sorted defintion of first).
+    names = list(sorted(potential_interfaces, key=util.natural_sort_key))
+    if DEFAULT_PRIMARY_INTERFACE in names:
+        names.remove(DEFAULT_PRIMARY_INTERFACE)
+        names.insert(0, DEFAULT_PRIMARY_INTERFACE)
+
+    # pick the first that has a mac-address
+    for name in names:
+        if read_sys_net_safe(name, 'address'):
+            return name
+    return None
+
+
+
+def interface_has_own_mac(ifname, strict=False):
+    """return True if the provided interface has its own address.
+
+    Based on addr_assign_type in /sys.  Return true for any interface
+    that does not have a 'stolen' address. Examples of such devices
+    are bonds or vlans that inherit their mac from another device.
+    Possible values are:
+      0: permanent address    2: stolen from another device
+      1: randomly generated   3: set using dev_set_mac_address"""
+
+    assign_type = read_sys_net_int(ifname, "addr_assign_type")
+    if assign_type is None:
+        # None is returned if this nic had no 'addr_assign_type' entry.
+        # if strict, raise an error, if not return True.
+        if strict:
+            raise ValueError("%s had no addr_assign_type.")
+        return True
+    return assign_type in (0, 1, 3)
+
+
+def _get_current_rename_info(check_downable=True):
+    """Collect information necessary for rename_interfaces.
+
+    returns a dictionary by mac address like:
+       {name:
+         {
+          'downable': None or boolean indicating that the
+                      device has only automatically assigned ip addrs.
+          'device_id': Device id value (if it has one)
+          'driver': Device driver (if it has one)
+          'mac': mac address (in lower case)
+          'name': name
+          'up': boolean: is_up(name)
+         }}
+    """
+    cur_info = {}
+    for (name, mac, driver, device_id) in get_interfaces():
+        cur_info[name] = {
+            'downable': None,
+            'device_id': device_id,
+            'driver': driver,
+            'mac': mac.lower(),
+            'name': name,
+            'up': is_up(name),
+        }
+
+    if check_downable:
+        nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]")
+        ipv6, _err = util.subp(['ip', '-6', 'addr', 'show', 'permanent',
+                                'scope', 'global'], capture=True)
+        ipv4, _err = util.subp(['ip', '-4', 'addr', 'show'], capture=True)
+
+        nics_with_addresses = set()
+        for bytes_out in (ipv6, ipv4):
+            nics_with_addresses.update(nmatch.findall(bytes_out))
+
+        for d in cur_info.values():
+            d['downable'] = (d['up'] is False or
+                             d['name'] not in nics_with_addresses)
+
+    return cur_info
+
+
+def _rename_interfaces(renames, strict_present=True, strict_busy=True,
+                       current_info=None):
+
+    if not len(renames):
+        LOG.debug("no interfaces to rename")
+        return
+
+    if current_info is None:
+        current_info = _get_current_rename_info()
+
+    cur_info = {}
+    for name, data in current_info.items():
+        cur = data.copy()
+        if cur.get('mac'):
+            cur['mac'] = cur['mac'].lower()
+        cur['name'] = name
+        cur_info[name] = cur
+
+    def update_byname(bymac):
+        return dict((data['name'], data)
+                    for data in cur_info.values())
+
+    def rename(cur, new):
+        util.subp(["ip", "link", "set", cur, "name", new], capture=True)
+
+    def down(name):
+        util.subp(["ip", "link", "set", name, "down"], capture=True)
+
+    def up(name):
+        util.subp(["ip", "link", "set", name, "up"], capture=True)
+
+    ops = []
+    errors = []
+    ups = []
+    cur_byname = update_byname(cur_info)
+    tmpname_fmt = "cirename%d"
+    tmpi = -1
+
+    def entry_match(data, mac, driver, device_id):
+        """match if set and in data"""
+        if mac and driver and device_id:
+            return (data['mac'] == mac and
+                    data['driver'] == driver and
+                    data['device_id'] == device_id)
+        elif mac and driver:
+            return (data['mac'] == mac and
+                    data['driver'] == driver)
+        elif mac:
+            return (data['mac'] == mac)
+
+        return False
+
+    def find_entry(mac, driver, device_id):
+        match = [data for data in cur_info.values()
+                 if entry_match(data, mac, driver, device_id)]
+        if len(match):
+            if len(match) > 1:
+                msg = ('Failed to match a single device. Matched devices "%s"'
+                       ' with search values "(mac:%s driver:%s device_id:%s)"'
+                       % (match, mac, driver, device_id))
+                raise ValueError(msg)
+            return match[0]
+
+        return None
+
+    for mac, new_name, driver, device_id in renames:
+        if mac:
+            mac = mac.lower()
+        cur_ops = []
+        cur = find_entry(mac, driver, device_id)
+        if not cur:
+            if strict_present:
+                errors.append(
+                    "[nic not present] Cannot rename mac=%s to %s"
+                    ", not available." % (mac, new_name))
+            continue
+
+        cur_name = cur.get('name')
+        if cur_name == new_name:
+            # nothing to do
+            continue
+
+        if not cur_name:
+            if strict_present:
+                errors.append(
+                    "[nic not present] Cannot rename mac=%s to %s"
+                    ", not available." % (mac, new_name))
+            continue
+
+        if cur['up']:
+            msg = "[busy] Error renaming mac=%s from %s to %s"
+            if not cur['downable']:
+                if strict_busy:
+                    errors.append(msg % (mac, cur_name, new_name))
+                continue
+            cur['up'] = False
+            cur_ops.append(("down", mac, new_name, (cur_name,)))
+            ups.append(("up", mac, new_name, (new_name,)))
+
+        if new_name in cur_byname:
+            target = cur_byname[new_name]
+            if target['up']:
+                msg = "[busy-target] Error renaming mac=%s from %s to %s."
+                if not target['downable']:
+                    if strict_busy:
+                        errors.append(msg % (mac, cur_name, new_name))
+                    continue
+                else:
+                    cur_ops.append(("down", mac, new_name, (new_name,)))
+
+            tmp_name = None
+            while tmp_name is None or tmp_name in cur_byname:
+                tmpi += 1
+                tmp_name = tmpname_fmt % tmpi
+
+            cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
+            target['name'] = tmp_name
+            cur_byname = update_byname(cur_info)
+            if target['up']:
+                ups.append(("up", mac, new_name, (tmp_name,)))
+
+        cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
+        cur['name'] = new_name
+        cur_byname = update_byname(cur_info)
+        ops += cur_ops
+
+    opmap = {'rename': rename, 'down': down, 'up': up}
+
+    if len(ops) + len(ups) == 0:
+        if len(errors):
+            LOG.debug("unable to do any work for renaming of %s", renames)
+        else:
+            LOG.debug("no work necessary for renaming of %s", renames)
+    else:
+        LOG.debug("achieving renaming of %s with ops %s", renames, ops + ups)
+
+        for op, mac, new_name, params in ops + ups:
+            try:
+                opmap.get(op)(*params)
+            except Exception as e:
+                errors.append(
+                    "[unknown] Error performing %s%s for %s, %s: %s" %
+                    (op, params, mac, new_name, e))
+
+    if len(errors):
+        raise Exception('\n'.join(errors))
+
+
+def get_interface_mac(ifname):
+    """Returns the string value of an interface's MAC Address"""
+    path = "address"
+    if os.path.isdir(sys_dev_path(ifname, "bonding_slave")):
+        # for a bond slave, get the nic's hwaddress, not the address it
+        # is using because its part of a bond.
+        path = "bonding_slave/perm_hwaddr"
+    return read_sys_net_safe(ifname, path)
+
+def net_setup_link(run=False):
+    """To ensure device link properties are applied, we poke
+        udev to re-evaluate networkd .link files and call
+        the setup_link udev builtin command
+    """
+    if not run:
+        LOG.debug("netplan net_setup_link postcmd disabled")
+        return
+    setup_lnk = ['udevadm', 'test-builtin', 'net_setup_link']
+    for cmd in [setup_lnk + [SYS_CLASS_NET + iface]
+                for iface in get_devicelist() if
+                os.path.islink(SYS_CLASS_NET + iface)]:
+        util.subp(cmd, capture=True)
+
+def get_ib_interface_hwaddr(ifname, ethernet_format):
+    """Returns the string value of an Infiniband interface's hardware
+    address. If ethernet_format is True, an Ethernet MAC-style 6 byte
+    representation of the address will be returned.
+    """
+    # Type 32 is Infiniband.
+    if read_sys_net_safe(ifname, 'type') == '32':
+        mac = get_interface_mac(ifname)
+        if mac and ethernet_format:
+            # Use bytes 13-15 and 18-20 of the hardware address.
+            mac = mac[36:-14] + mac[51:]
+        return mac
+
+
+def get_interfaces_by_mac():
+    """Build a dictionary of tuples {mac: name}.
+
+    Bridges and any devices that have a 'stolen' mac are excluded."""
+    ret = {}
+    from cloudinit.net import common
+    for name, mac, _driver, _devid in common.get_interfaces():
+        if mac in ret:
+            raise RuntimeError(
+                "duplicate mac found! both '%s' and '%s' have mac '%s'" %
+                (name, ret[mac], mac))
+        ret[mac] = name
+        # Try to get an Infiniband hardware address (in 6 byte Ethernet format)
+        # for the interface.
+        ib_mac = get_ib_interface_hwaddr(name, True)
+        if ib_mac:
+            if ib_mac in ret:
+                raise RuntimeError(
+                    "duplicate mac found! both '%s' and '%s' have mac '%s'" %
+                    (name, ret[ib_mac], ib_mac))
+            ret[ib_mac] = name
+    return ret
+
+class EphemeralIPv4Network(object):
+    """Context manager which sets up temporary static network configuration.
+
+    No operations are performed if the provided interface is already connected.
+    If unconnected, bring up the interface with valid ip, prefix and broadcast.
+    If router is provided setup a default route for that interface. Upon
+    context exit, clean up the interface leaving no configuration behind.
+    """
+
+    def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None):
+        """Setup context manager and validate call signature.
+
+        @param interface: Name of the network interface to bring up.
+        @param ip: IP address to assign to the interface.
+        @param prefix_or_mask: Either netmask of the format X.X.X.X or an int
+            prefix.
+        @param broadcast: Broadcast address for the IPv4 network.
+        @param router: Optionally the default gateway IP.
+        """
+        if not all([interface, ip, prefix_or_mask, broadcast]):
+            raise ValueError(
+                'Cannot init network on {0} with {1}/{2} and bcast {3}'.format(
+                    interface, ip, prefix_or_mask, broadcast))
+        try:
+            self.prefix = mask_to_net_prefix(prefix_or_mask)
+        except ValueError as e:
+            raise ValueError(
+                'Cannot setup network: {0}'.format(e))
+        self.interface = interface
+        self.ip = ip
+        self.broadcast = broadcast
+        self.router = router
+        self.cleanup_cmds = []  # List of commands to run to cleanup state.
+
+    def __enter__(self):
+        """Perform ephemeral network setup if interface is not connected."""
+        self._bringup_device()
+        if self.router:
+            self._bringup_router()
+
+    def __exit__(self, excp_type, excp_value, excp_traceback):
+        """Teardown anything we set up."""
+        for cmd in self.cleanup_cmds:
+            util.subp(cmd, capture=True)
+
+    def _delete_address(self, address, prefix):
+        """Perform the ip command to remove the specified address."""
+        util.subp(
+            ['ip', '-family', 'inet', 'addr', 'del',
+             '%s/%s' % (address, prefix), 'dev', self.interface],
+            capture=True)
+
+    def _bringup_device(self):
+        """Perform the ip comands to fully setup the device."""
+        cidr = '{0}/{1}'.format(self.ip, self.prefix)
+        LOG.debug(
+            'Attempting setup of ephemeral network on %s with %s brd %s',
+            self.interface, cidr, self.broadcast)
+        try:
+            util.subp(
+                ['ip', '-family', 'inet', 'addr', 'add', cidr, 'broadcast',
+                 self.broadcast, 'dev', self.interface],
+                capture=True, update_env={'LANG': 'C'})
+        except util.ProcessExecutionError as e:
+            if "File exists" not in e.stderr:
+                raise
+            LOG.debug(
+                'Skip ephemeral network setup, %s already has address %s',
+                self.interface, self.ip)
+        else:
+            # Address creation success, bring up device and queue cleanup
+            util.subp(
+                ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface,
+                 'up'], capture=True)
+            self.cleanup_cmds.append(
+                ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface,
+                 'down'])
+            self.cleanup_cmds.append(
+                ['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev',
+                 self.interface])
+
+    def _bringup_router(self):
+        """Perform the ip commands to fully setup the router if needed."""
+        # Check if a default route exists and exit if it does
+        out, _ = util.subp(['ip', 'route', 'show', '0.0.0.0/0'], capture=True)
+        if 'default' in out:
+            LOG.debug(
+                'Skip ephemeral route setup. %s already has default route: %s',
+                self.interface, out.strip())
+            return
+        util.subp(
+            ['ip', '-4', 'route', 'add', self.router, 'dev', self.interface,
+             'src', self.ip], capture=True)
+        self.cleanup_cmds.insert(
+            0,
+            ['ip', '-4', 'route', 'del', self.router, 'dev', self.interface,
+             'src', self.ip])
+        util.subp(
+            ['ip', '-4', 'route', 'add', 'default', 'via', self.router,
+             'dev', self.interface], capture=True)
+        self.cleanup_cmds.insert(
+            0, ['ip', '-4', 'route', 'del', 'default', 'dev', self.interface])
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index bc1087f..95cc600 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -8,7 +8,7 @@ from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2
 
 from cloudinit import log as logging
 from cloudinit import util
-from cloudinit.net import SYS_CLASS_NET, get_devicelist
+from cloudinit.net import net_setup_link, get_devicelist
 
 KNOWN_SNAPD_CONFIG = b"""\
 # This is the initial network config.
@@ -208,7 +208,7 @@ class Renderer(renderer.Renderer):
         if self.clean_default:
             _clean_default(target=target)
         self._netplan_generate(run=self._postcmds)
-        self._net_setup_link(run=self._postcmds)
+        net_setup_link(run=self._postcmds)
 
     def _netplan_generate(self, run=False):
         if not run:
@@ -216,19 +216,6 @@ class Renderer(renderer.Renderer):
             return
         util.subp(self.NETPLAN_GENERATE, capture=True)
 
-    def _net_setup_link(self, run=False):
-        """To ensure device link properties are applied, we poke
-           udev to re-evaluate networkd .link files and call
-           the setup_link udev builtin command
-        """
-        if not run:
-            LOG.debug("netplan net_setup_link postcmd disabled")
-            return
-        setup_lnk = ['udevadm', 'test-builtin', 'net_setup_link']
-        for cmd in [setup_lnk + [SYS_CLASS_NET + iface]
-                    for iface in get_devicelist() if
-                    os.path.islink(SYS_CLASS_NET + iface)]:
-            util.subp(cmd, capture=True)
 
     def _render_content(self, network_state):
 
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index 58e0a59..1e33a61 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -12,524 +12,8 @@ from cloudinit.util import ensure_file, write_file, ProcessExecutionError
 from cloudinit.tests.helpers import CiTestCase
 
 
-class TestSysDevPath(CiTestCase):
 
-    def test_sys_dev_path(self):
-        """sys_dev_path returns a path under SYS_CLASS_NET for a device."""
-        dev = 'something'
-        path = 'attribute'
-        expected = net.SYS_CLASS_NET + dev + '/' + path
-        self.assertEqual(expected, net.sys_dev_path(dev, path))
 
-    def test_sys_dev_path_without_path(self):
-        """When path param isn't provided it defaults to empty string."""
-        dev = 'something'
-        expected = net.SYS_CLASS_NET + dev + '/'
-        self.assertEqual(expected, net.sys_dev_path(dev))
-
-
-class TestReadSysNet(CiTestCase):
-    with_logs = True
-
-    def setUp(self):
-        super(TestReadSysNet, self).setUp()
-        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
-        self.m_sys_path = sys_mock.start()
-        self.sysdir = self.tmp_dir() + '/'
-        self.m_sys_path.return_value = self.sysdir
-        self.addCleanup(sys_mock.stop)
-
-    def test_read_sys_net_strips_contents_of_sys_path(self):
-        """read_sys_net strips whitespace from the contents of a sys file."""
-        content = 'some stuff with trailing whitespace\t\r\n'
-        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
-        self.assertEqual(content.strip(), net.read_sys_net('dev', 'attr'))
-
-    def test_read_sys_net_reraises_oserror(self):
-        """read_sys_net raises OSError/IOError when file doesn't exist."""
-        # Non-specific Exception because versions of python OSError vs IOError.
-        with self.assertRaises(Exception) as context_manager:  # noqa: H202
-            net.read_sys_net('dev', 'attr')
-        error = context_manager.exception
-        self.assertIn('No such file or directory', str(error))
-
-    def test_read_sys_net_handles_error_with_on_enoent(self):
-        """read_sys_net handles OSError/IOError with on_enoent if provided."""
-        handled_errors = []
-
-        def on_enoent(e):
-            handled_errors.append(e)
-
-        net.read_sys_net('dev', 'attr', on_enoent=on_enoent)
-        error = handled_errors[0]
-        self.assertIsInstance(error, Exception)
-        self.assertIn('No such file or directory', str(error))
-
-    def test_read_sys_net_translates_content(self):
-        """read_sys_net translates content when translate dict is provided."""
-        content = "you're welcome\n"
-        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
-        translate = {"you're welcome": 'de nada'}
-        self.assertEqual(
-            'de nada',
-            net.read_sys_net('dev', 'attr', translate=translate))
-
-    def test_read_sys_net_errors_on_translation_failures(self):
-        """read_sys_net raises a KeyError and logs details on failure."""
-        content = "you're welcome\n"
-        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
-        with self.assertRaises(KeyError) as context_manager:
-            net.read_sys_net('dev', 'attr', translate={})
-        error = context_manager.exception
-        self.assertEqual('"you\'re welcome"', str(error))
-        self.assertIn(
-            "Found unexpected (not translatable) value 'you're welcome' in "
-            "'{0}dev/attr".format(self.sysdir),
-            self.logs.getvalue())
-
-    def test_read_sys_net_handles_handles_with_onkeyerror(self):
-        """read_sys_net handles translation errors calling on_keyerror."""
-        content = "you're welcome\n"
-        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
-        handled_errors = []
-
-        def on_keyerror(e):
-            handled_errors.append(e)
-
-        net.read_sys_net('dev', 'attr', translate={}, on_keyerror=on_keyerror)
-        error = handled_errors[0]
-        self.assertIsInstance(error, KeyError)
-        self.assertEqual('"you\'re welcome"', str(error))
-
-    def test_read_sys_net_safe_false_on_translate_failure(self):
-        """read_sys_net_safe returns False on translation failures."""
-        content = "you're welcome\n"
-        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
-        self.assertFalse(net.read_sys_net_safe('dev', 'attr', translate={}))
-
-    def test_read_sys_net_safe_returns_false_on_noent_failure(self):
-        """read_sys_net_safe returns False on file not found failures."""
-        self.assertFalse(net.read_sys_net_safe('dev', 'attr'))
-
-    def test_read_sys_net_int_returns_none_on_error(self):
-        """read_sys_net_safe returns None on failures."""
-        self.assertFalse(net.read_sys_net_int('dev', 'attr'))
-
-    def test_read_sys_net_int_returns_none_on_valueerror(self):
-        """read_sys_net_safe returns None when content is not an int."""
-        write_file(os.path.join(self.sysdir, 'dev', 'attr'), 'NOTINT\n')
-        self.assertFalse(net.read_sys_net_int('dev', 'attr'))
-
-    def test_read_sys_net_int_returns_integer_from_content(self):
-        """read_sys_net_safe returns None on failures."""
-        write_file(os.path.join(self.sysdir, 'dev', 'attr'), '1\n')
-        self.assertEqual(1, net.read_sys_net_int('dev', 'attr'))
-
-    def test_is_up_true(self):
-        """is_up is True if sys/net/devname/operstate is 'up' or 'unknown'."""
-        for state in ['up', 'unknown']:
-            write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
-            self.assertTrue(net.is_up('eth0'))
-
-    def test_is_up_false(self):
-        """is_up is False if sys/net/devname/operstate is 'down' or invalid."""
-        for state in ['down', 'incomprehensible']:
-            write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
-            self.assertFalse(net.is_up('eth0'))
-
-    def test_is_wireless(self):
-        """is_wireless is True when /sys/net/devname/wireless exists."""
-        self.assertFalse(net.is_wireless('eth0'))
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'wireless'))
-        self.assertTrue(net.is_wireless('eth0'))
-
-    def test_is_bridge(self):
-        """is_bridge is True when /sys/net/devname/bridge exists."""
-        self.assertFalse(net.is_bridge('eth0'))
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
-        self.assertTrue(net.is_bridge('eth0'))
-
-    def test_is_bond(self):
-        """is_bond is True when /sys/net/devname/bonding exists."""
-        self.assertFalse(net.is_bond('eth0'))
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
-        self.assertTrue(net.is_bond('eth0'))
-
-    def test_is_vlan(self):
-        """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan."""
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent'))
-        self.assertFalse(net.is_vlan('eth0'))
-        content = 'junk\nDEVTYPE=vlan\njunk\n'
-        write_file(os.path.join(self.sysdir, 'eth0', 'uevent'), content)
-        self.assertTrue(net.is_vlan('eth0'))
-
-    def test_is_connected_when_physically_connected(self):
-        """is_connected is True when /sys/net/devname/iflink reports 2."""
-        self.assertFalse(net.is_connected('eth0'))
-        write_file(os.path.join(self.sysdir, 'eth0', 'iflink'), "2")
-        self.assertTrue(net.is_connected('eth0'))
-
-    def test_is_connected_when_wireless_and_carrier_active(self):
-        """is_connected is True if wireless /sys/net/devname/carrier is 1."""
-        self.assertFalse(net.is_connected('eth0'))
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'wireless'))
-        self.assertFalse(net.is_connected('eth0'))
-        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), "1")
-        self.assertTrue(net.is_connected('eth0'))
-
-    def test_is_physical(self):
-        """is_physical is True when /sys/net/devname/device exists."""
-        self.assertFalse(net.is_physical('eth0'))
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'device'))
-        self.assertTrue(net.is_physical('eth0'))
-
-    def test_is_present(self):
-        """is_present is True when /sys/net/devname exists."""
-        self.assertFalse(net.is_present('eth0'))
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'device'))
-        self.assertTrue(net.is_present('eth0'))
-
-
-class TestGenerateFallbackConfig(CiTestCase):
-
-    def setUp(self):
-        super(TestGenerateFallbackConfig, self).setUp()
-        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
-        self.m_sys_path = sys_mock.start()
-        self.sysdir = self.tmp_dir() + '/'
-        self.m_sys_path.return_value = self.sysdir
-        self.addCleanup(sys_mock.stop)
-        self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
-                       return_value=False)
-        self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
-
-    def test_generate_fallback_finds_connected_eth_with_mac(self):
-        """generate_fallback_config finds any connected device with a mac."""
-        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
-        write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
-        expected = {
-            'config': [{'type': 'physical', 'mac_address': mac,
-                        'name': 'eth1', 'subnets': [{'type': 'dhcp'}]}],
-            'version': 1}
-        self.assertEqual(expected, net.generate_fallback_config())
-
-    def test_generate_fallback_finds_dormant_eth_with_mac(self):
-        """generate_fallback_config finds any dormant device with a mac."""
-        write_file(os.path.join(self.sysdir, 'eth0', 'dormant'), '1')
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
-        expected = {
-            'config': [{'type': 'physical', 'mac_address': mac,
-                        'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
-            'version': 1}
-        self.assertEqual(expected, net.generate_fallback_config())
-
-    def test_generate_fallback_finds_eth_by_operstate(self):
-        """generate_fallback_config finds any dormant device with a mac."""
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
-        expected = {
-            'config': [{'type': 'physical', 'mac_address': mac,
-                        'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
-            'version': 1}
-        valid_operstates = ['dormant', 'down', 'lowerlayerdown', 'unknown']
-        for state in valid_operstates:
-            write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
-            self.assertEqual(expected, net.generate_fallback_config())
-        write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'noworky')
-        self.assertIsNone(net.generate_fallback_config())
-
-    def test_generate_fallback_config_skips_veth(self):
-        """generate_fallback_config will skip any veth interfaces."""
-        # A connected veth which gets ignored
-        write_file(os.path.join(self.sysdir, 'veth0', 'carrier'), '1')
-        self.assertIsNone(net.generate_fallback_config())
-
-    def test_generate_fallback_config_skips_bridges(self):
-        """generate_fallback_config will skip any bridges interfaces."""
-        # A connected veth which gets ignored
-        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
-        self.assertIsNone(net.generate_fallback_config())
-
-    def test_generate_fallback_config_skips_bonds(self):
-        """generate_fallback_config will skip any bonded interfaces."""
-        # A connected veth which gets ignored
-        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
-        self.assertIsNone(net.generate_fallback_config())
-
-
-class TestGetDeviceList(CiTestCase):
-
-    def setUp(self):
-        super(TestGetDeviceList, self).setUp()
-        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
-        self.m_sys_path = sys_mock.start()
-        self.sysdir = self.tmp_dir() + '/'
-        self.m_sys_path.return_value = self.sysdir
-        self.addCleanup(sys_mock.stop)
-
-    def test_get_devicelist_raise_oserror(self):
-        """get_devicelist raise any non-ENOENT OSerror."""
-        error = OSError('Can not do it')
-        error.errno = errno.EPERM  # Set non-ENOENT
-        self.m_sys_path.side_effect = error
-        with self.assertRaises(OSError) as context_manager:
-            net.get_devicelist()
-        exception = context_manager.exception
-        self.assertEqual('Can not do it', str(exception))
-
-    def test_get_devicelist_empty_without_sys_net(self):
-        """get_devicelist returns empty list when missing SYS_CLASS_NET."""
-        self.m_sys_path.return_value = 'idontexist'
-        self.assertEqual([], net.get_devicelist())
-
-    def test_get_devicelist_empty_with_no_devices_in_sys_net(self):
-        """get_devicelist returns empty directoty listing for SYS_CLASS_NET."""
-        self.assertEqual([], net.get_devicelist())
-
-    def test_get_devicelist_lists_any_subdirectories_in_sys_net(self):
-        """get_devicelist returns a directory listing for SYS_CLASS_NET."""
-        write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'up')
-        write_file(os.path.join(self.sysdir, 'eth1', 'operstate'), 'up')
-        self.assertItemsEqual(['eth0', 'eth1'], net.get_devicelist())
-
-
-class TestGetInterfaceMAC(CiTestCase):
-
-    def setUp(self):
-        super(TestGetInterfaceMAC, self).setUp()
-        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
-        self.m_sys_path = sys_mock.start()
-        self.sysdir = self.tmp_dir() + '/'
-        self.m_sys_path.return_value = self.sysdir
-        self.addCleanup(sys_mock.stop)
-
-    def test_get_interface_mac_false_with_no_mac(self):
-        """get_device_list returns False when no mac is reported."""
-        ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
-        mac_path = os.path.join(self.sysdir, 'eth0', 'address')
-        self.assertFalse(os.path.exists(mac_path))
-        self.assertFalse(net.get_interface_mac('eth0'))
-
-    def test_get_interface_mac(self):
-        """get_interfaces returns the mac from SYS_CLASS_NET/dev/address."""
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
-        self.assertEqual(mac, net.get_interface_mac('eth1'))
-
-    def test_get_interface_mac_grabs_bonding_address(self):
-        """get_interfaces returns the source device mac for bonded devices."""
-        source_dev_mac = 'aa:bb:cc:aa:bb:cc'
-        bonded_mac = 'dd:ee:ff:dd:ee:ff'
-        write_file(os.path.join(self.sysdir, 'eth1', 'address'), bonded_mac)
-        write_file(
-            os.path.join(self.sysdir, 'eth1', 'bonding_slave', 'perm_hwaddr'),
-            source_dev_mac)
-        self.assertEqual(source_dev_mac, net.get_interface_mac('eth1'))
-
-    def test_get_interfaces_empty_list_without_sys_net(self):
-        """get_interfaces returns an empty list when missing SYS_CLASS_NET."""
-        self.m_sys_path.return_value = 'idontexist'
-        self.assertEqual([], net.get_interfaces())
-
-    def test_get_interfaces_by_mac_skips_empty_mac(self):
-        """Ignore 00:00:00:00:00:00 addresses from get_interfaces_by_mac."""
-        empty_mac = '00:00:00:00:00:00'
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth1', 'address'), empty_mac)
-        write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
-        write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
-        write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
-        expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
-        self.assertEqual(expected, net.get_interfaces())
-
-    def test_get_interfaces_by_mac_skips_missing_mac(self):
-        """Ignore interfaces without an address from get_interfaces_by_mac."""
-        write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
-        address_path = os.path.join(self.sysdir, 'eth1', 'address')
-        self.assertFalse(os.path.exists(address_path))
-        mac = 'aa:bb:cc:aa:bb:cc'
-        write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
-        write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
-        expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
-        self.assertEqual(expected, net.get_interfaces())
-
-
-class TestInterfaceHasOwnMAC(CiTestCase):
-
-    def setUp(self):
-        super(TestInterfaceHasOwnMAC, self).setUp()
-        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
-        self.m_sys_path = sys_mock.start()
-        self.sysdir = self.tmp_dir() + '/'
-        self.m_sys_path.return_value = self.sysdir
-        self.addCleanup(sys_mock.stop)
-
-    def test_interface_has_own_mac_false_when_stolen(self):
-        """Return False from interface_has_own_mac when address is stolen."""
-        write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '2')
-        self.assertFalse(net.interface_has_own_mac('eth1'))
-
-    def test_interface_has_own_mac_true_when_not_stolen(self):
-        """Return False from interface_has_own_mac when mac isn't stolen."""
-        valid_assign_types = ['0', '1', '3']
-        assign_path = os.path.join(self.sysdir, 'eth1', 'addr_assign_type')
-        for _type in valid_assign_types:
-            write_file(assign_path, _type)
-            self.assertTrue(net.interface_has_own_mac('eth1'))
-
-    def test_interface_has_own_mac_strict_errors_on_absent_assign_type(self):
-        """When addr_assign_type is absent, interface_has_own_mac errors."""
-        with self.assertRaises(ValueError):
-            net.interface_has_own_mac('eth1', strict=True)
-
-
-@mock.patch('cloudinit.net.util.subp')
-class TestEphemeralIPV4Network(CiTestCase):
-
-    with_logs = True
-
-    def setUp(self):
-        super(TestEphemeralIPV4Network, self).setUp()
-        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
-        self.m_sys_path = sys_mock.start()
-        self.sysdir = self.tmp_dir() + '/'
-        self.m_sys_path.return_value = self.sysdir
-        self.addCleanup(sys_mock.stop)
-
-    def test_ephemeral_ipv4_network_errors_on_missing_params(self, m_subp):
-        """No required params for EphemeralIPv4Network can be None."""
-        required_params = {
-            'interface': 'eth0', 'ip': '192.168.2.2',
-            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
-        for key in required_params.keys():
-            params = copy.deepcopy(required_params)
-            params[key] = None
-            with self.assertRaises(ValueError) as context_manager:
-                net.EphemeralIPv4Network(**params)
-            error = context_manager.exception
-            self.assertIn('Cannot init network on', str(error))
-            self.assertEqual(0, m_subp.call_count)
-
-    def test_ephemeral_ipv4_network_errors_invalid_mask_prefix(self, m_subp):
-        """Raise an error when prefix_or_mask is not a netmask or prefix."""
-        params = {
-            'interface': 'eth0', 'ip': '192.168.2.2',
-            'broadcast': '192.168.2.255'}
-        invalid_masks = ('invalid', 'invalid.', '123.123.123')
-        for error_val in invalid_masks:
-            params['prefix_or_mask'] = error_val
-            with self.assertRaises(ValueError) as context_manager:
-                with net.EphemeralIPv4Network(**params):
-                    pass
-            error = context_manager.exception
-            self.assertIn('Cannot setup network: netmask', str(error))
-            self.assertEqual(0, m_subp.call_count)
-
-    def test_ephemeral_ipv4_network_performs_teardown(self, m_subp):
-        """EphemeralIPv4Network performs teardown on the device if setup."""
-        expected_setup_calls = [
-            mock.call(
-                ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
-                 'broadcast', '192.168.2.255', 'dev', 'eth0'],
-                capture=True, update_env={'LANG': 'C'}),
-            mock.call(
-                ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
-                capture=True)]
-        expected_teardown_calls = [
-            mock.call(
-                ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0',
-                 'down'], capture=True),
-            mock.call(
-                ['ip', '-family', 'inet', 'addr', 'del', '192.168.2.2/24',
-                 'dev', 'eth0'], capture=True)]
-        params = {
-            'interface': 'eth0', 'ip': '192.168.2.2',
-            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
-        with net.EphemeralIPv4Network(**params):
-            self.assertEqual(expected_setup_calls, m_subp.call_args_list)
-        m_subp.assert_has_calls(expected_teardown_calls)
-
-    def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp):
-        """EphemeralIPv4Network handles exception when address is setup.
-
-        It performs no cleanup as the interface was already setup.
-        """
-        params = {
-            'interface': 'eth0', 'ip': '192.168.2.2',
-            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
-        m_subp.side_effect = ProcessExecutionError(
-            '', 'RTNETLINK answers: File exists', 2)
-        expected_calls = [
-            mock.call(
-                ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
-                 'broadcast', '192.168.2.255', 'dev', 'eth0'],
-                capture=True, update_env={'LANG': 'C'})]
-        with net.EphemeralIPv4Network(**params):
-            pass
-        self.assertEqual(expected_calls, m_subp.call_args_list)
-        self.assertIn(
-            'Skip ephemeral network setup, eth0 already has address',
-            self.logs.getvalue())
-
-    def test_ephemeral_ipv4_network_with_prefix(self, m_subp):
-        """EphemeralIPv4Network takes a valid prefix to setup the network."""
-        params = {
-            'interface': 'eth0', 'ip': '192.168.2.2',
-            'prefix_or_mask': '24', 'broadcast': '192.168.2.255'}
-        for prefix_val in ['24', 16]:  # prefix can be int or string
-            params['prefix_or_mask'] = prefix_val
-            with net.EphemeralIPv4Network(**params):
-                pass
-        m_subp.assert_has_calls([mock.call(
-            ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
-             'broadcast', '192.168.2.255', 'dev', 'eth0'],
-            capture=True, update_env={'LANG': 'C'})])
-        m_subp.assert_has_calls([mock.call(
-            ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/16',
-             'broadcast', '192.168.2.255', 'dev', 'eth0'],
-            capture=True, update_env={'LANG': 'C'})])
-
-    def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp):
-        """Add the route when router is set and no default route exists."""
-        params = {
-            'interface': 'eth0', 'ip': '192.168.2.2',
-            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
-            'router': '192.168.2.1'}
-        m_subp.return_value = '', ''  # Empty response from ip route gw check
-        expected_setup_calls = [
-            mock.call(
-                ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
-                 'broadcast', '192.168.2.255', 'dev', 'eth0'],
-                capture=True, update_env={'LANG': 'C'}),
-            mock.call(
-                ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
-                capture=True),
-            mock.call(
-                ['ip', 'route', 'show', '0.0.0.0/0'], capture=True),
-            mock.call(['ip', '-4', 'route', 'add', '192.168.2.1',
-                       'dev', 'eth0', 'src', '192.168.2.2'], capture=True),
-            mock.call(
-                ['ip', '-4', 'route', 'add', 'default', 'via',
-                 '192.168.2.1', 'dev', 'eth0'], capture=True)]
-        expected_teardown_calls = [
-            mock.call(['ip', '-4', 'route', 'del', 'default', 'dev', 'eth0'],
-                      capture=True),
-            mock.call(['ip', '-4', 'route', 'del', '192.168.2.1',
-                       'dev', 'eth0', 'src', '192.168.2.2'], capture=True),
-        ]
-
-        with net.EphemeralIPv4Network(**params):
-            self.assertEqual(expected_setup_calls, m_subp.call_args_list)
-        m_subp.assert_has_calls(expected_teardown_calls)
 
 
 class TestApplyNetworkCfgNames(CiTestCase):
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
index e62e972..75707cf 100644
--- a/cloudinit/sources/DataSourceOpenNebula.py
+++ b/cloudinit/sources/DataSourceOpenNebula.py
@@ -134,7 +134,7 @@ class OpenNebulaNetwork(object):
             system_nics_by_mac = get_physical_nics_by_mac()
         self.ifaces = collections.OrderedDict(
             [k for k in sorted(system_nics_by_mac.items(),
-                               key=lambda k: net.natural_sort_key(k[1]))])
+                               key=lambda k: util.natural_sort_key(k[1]))])
 
         # OpenNebula 4.14+ provide macaddr for ETHX in variable ETH_MAC.
         # context_devname provides {mac.lower():ETHX, mac2.lower():ETHX}
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 7800f7b..969a92c 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -79,6 +79,17 @@ PROC_CMDLINE = None
 _LSB_RELEASE = {}
 PY26 = sys.version_info[0:2] == (2, 6)
 
+def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
+    """Sorting for Humans: natural sort order. Can be use as the key to sort
+    functions.
+    This will sort ['eth0', 'ens3', 'ens10', 'ens12', 'ens8', 'ens0'] as
+    ['ens0', 'ens3', 'ens8', 'ens10', 'ens12', 'eth0'] instead of the simple
+    python way which will produce ['ens0', 'ens10', 'ens12', 'ens3', 'ens8',
+    'eth0']."""
+    return [
+        int(text) if text.isdigit() else text.lower()
+        for text in re.split(_nsre, s)
+    ]
 
 def get_architecture(target=None):
     out, _ = subp(['dpkg', '--print-architecture'], capture=True,
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 8e38373..e8b86c3 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -4,8 +4,8 @@ from cloudinit import net
 from cloudinit import distros
 from cloudinit.net import cmdline
 from cloudinit.net import (
-    eni, interface_has_own_mac, natural_sort_key, netplan, network_state,
-    renderers, sysconfig)
+    eni, interface_has_own_mac, netplan, network_state,
+    renderers, sysconfig, common)
 from cloudinit.sources.helpers import openstack
 from cloudinit import temp_utils
 from cloudinit import util
@@ -1619,8 +1619,7 @@ DEFAULT_DEV_ATTRS = {
 }
 
 
-def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,
-                mock_sys_dev_path, dev_attrs=None):
+def _setup_test(tmp_dir, mock_get_devicelist, dev_attrs=None):
     if not dev_attrs:
         dev_attrs = DEFAULT_DEV_ATTRS
 
@@ -1631,25 +1630,6 @@ def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,
                   on_einval=None):
         return dev_attrs[devname][path]
 
-    mock_read_sys_net.side_effect = fake_read
-
-    def sys_dev_path(devname, path=""):
-        return tmp_dir + "/" + devname + "/" + path
-
-    for dev in dev_attrs:
-        os.makedirs(os.path.join(tmp_dir, dev))
-        with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
-            fh.write(dev_attrs[dev]['operstate'])
-        os.makedirs(os.path.join(tmp_dir, dev, "device"))
-        for key in ['device/driver']:
-            if key in dev_attrs[dev] and dev_attrs[dev][key]:
-                target = dev_attrs[dev][key]
-                link = os.path.join(tmp_dir, dev, key)
-                print('symlink %s -> %s' % (link, target))
-                os.symlink(target, link)
-
-    mock_sys_dev_path.side_effect = sys_dev_path
-
 
 class TestGenerateFallbackConfig(CiTestCase):
 
@@ -1659,11 +1639,8 @@ class TestGenerateFallbackConfig(CiTestCase):
             "cloudinit.util.get_cmdline", "m_get_cmdline",
             return_value="root=/dev/sda1")
 
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
-    def test_device_driver(self, mock_get_devicelist, mock_read_sys_net,
-                           mock_sys_dev_path):
+    def test_device_driver(self, mock_get_devicelist):
         devices = {
             'eth0': {
                 'bridge': False, 'carrier': False, 'dormant': False,
@@ -1680,7 +1657,6 @@ class TestGenerateFallbackConfig(CiTestCase):
 
         tmp_dir = self.tmp_dir()
         _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path,
                     dev_attrs=devices)
 
         network_cfg = net.generate_fallback_config(config_driver=True)
@@ -1722,11 +1698,8 @@ iface eth0 inet dhcp
         ]
         self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip())
 
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
-    def test_device_driver_blacklist(self, mock_get_devicelist,
-                                     mock_read_sys_net, mock_sys_dev_path):
+    def test_device_driver_blacklist(self, mock_get_devicelist):
         devices = {
             'eth1': {
                 'bridge': False, 'carrier': False, 'dormant': False,
@@ -1742,7 +1715,6 @@ iface eth0 inet dhcp
 
         tmp_dir = self.tmp_dir()
         _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path,
                     dev_attrs=devices)
 
         blacklist = ['mlx4_core']
@@ -1788,11 +1760,9 @@ iface eth1 inet dhcp
 
     @mock.patch("cloudinit.util.get_cmdline")
     @mock.patch("cloudinit.util.udevadm_settle")
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
-    def test_unstable_names(self, mock_get_devicelist, mock_read_sys_net,
-                            mock_sys_dev_path, mock_settle, m_get_cmdline):
+    def test_unstable_names(self, mock_get_devicelist,
+                            mock_settle, m_get_cmdline):
         """verify that udevadm settle is called when we find unstable names"""
         devices = {
             'eth0': {
@@ -1811,18 +1781,14 @@ iface eth1 inet dhcp
         m_get_cmdline.return_value = ''
         tmp_dir = self.tmp_dir()
         _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path,
                     dev_attrs=devices)
         net.generate_fallback_config(config_driver=True)
         self.assertEqual(1, mock_settle.call_count)
 
     @mock.patch("cloudinit.util.get_cmdline")
     @mock.patch("cloudinit.util.udevadm_settle")
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
     def test_unstable_names_disabled(self, mock_get_devicelist,
-                                     mock_read_sys_net, mock_sys_dev_path,
                                      mock_settle, m_get_cmdline):
         """verify udevadm settle not called when cmdline has net.ifnames=0"""
         devices = {
@@ -1842,7 +1808,6 @@ iface eth1 inet dhcp
         m_get_cmdline.return_value = 'net.ifnames=0'
         tmp_dir = self.tmp_dir()
         _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path,
                     dev_attrs=devices)
         net.generate_fallback_config(config_driver=True)
         self.assertEqual(0, mock_settle.call_count)
@@ -1902,15 +1867,11 @@ class TestRhelSysConfigRendering(CiTestCase):
             raise AssertionError("Missing headers in: %s" % missing)
 
     @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot")
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
     def test_default_generation(self, mock_get_devicelist,
-                                mock_read_sys_net,
-                                mock_sys_dev_path, m_get_cmdline):
+                                m_get_cmdline):
         tmp_dir = self.tmp_dir()
-        _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path)
+        _setup_test(tmp_dir, mock_get_devicelist)
 
         network_cfg = net.generate_fallback_config()
         ns = network_state.parse_net_config_data(network_cfg,
@@ -2191,15 +2152,11 @@ class TestOpenSuseSysConfigRendering(CiTestCase):
             raise AssertionError("Missing headers in: %s" % missing)
 
     @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot")
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
     def test_default_generation(self, mock_get_devicelist,
-                                mock_read_sys_net,
-                                mock_sys_dev_path, m_get_cmdline):
+                                m_get_cmdline):
         tmp_dir = self.tmp_dir()
-        _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path)
+        _setup_test(tmp_dir, mock_get_devicelist)
 
         network_cfg = net.generate_fallback_config()
         ns = network_state.parse_net_config_data(network_cfg,
@@ -2438,15 +2395,11 @@ USERCTL=no
 class TestEniNetRendering(CiTestCase):
 
     @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot")
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
     def test_default_generation(self, mock_get_devicelist,
-                                mock_read_sys_net,
-                                mock_sys_dev_path, m_get_cmdline):
+                                m_get_cmdline):
         tmp_dir = self.tmp_dir()
-        _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path)
+        _setup_test(tmp_dir, mock_get_devicelist)
 
         network_cfg = net.generate_fallback_config()
         ns = network_state.parse_net_config_data(network_cfg,
@@ -2493,16 +2446,11 @@ class TestNetplanNetRendering(CiTestCase):
 
     @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot")
     @mock.patch("cloudinit.net.netplan._clean_default")
-    @mock.patch("cloudinit.net.sys_dev_path")
-    @mock.patch("cloudinit.net.read_sys_net")
     @mock.patch("cloudinit.net.get_devicelist")
     def test_default_generation(self, mock_get_devicelist,
-                                mock_read_sys_net,
-                                mock_sys_dev_path,
                                 mock_clean_default, m_get_cmdline):
         tmp_dir = self.tmp_dir()
-        _setup_test(tmp_dir, mock_get_devicelist,
-                    mock_read_sys_net, mock_sys_dev_path)
+        _setup_test(tmp_dir, mock_get_devicelist)
 
         network_cfg = net.generate_fallback_config()
         ns = network_state.parse_net_config_data(network_cfg,
@@ -3143,7 +3091,7 @@ class TestGetInterfaces(CiTestCase):
 
     def test_gi_includes_duplicate_macs(self):
         self._mock_setup()
-        ret = net.get_interfaces()
+        ret = common.get_interfaces()
 
         self.assertIn('enp0s1', self._se_get_devicelist())
         self.assertIn('eth1', self._se_get_devicelist())
@@ -3152,7 +3100,7 @@ class TestGetInterfaces(CiTestCase):
 
     def test_gi_excludes_any_without_mac_address(self):
         self._mock_setup()
-        ret = net.get_interfaces()
+        ret = common.get_interfaces()
 
         self.assertIn('tun0', self._se_get_devicelist())
         found = [ent for ent in ret if 'tun0' in ent]
@@ -3160,7 +3108,7 @@ class TestGetInterfaces(CiTestCase):
 
     def test_gi_excludes_stolen_macs(self):
         self._mock_setup()
-        ret = net.get_interfaces()
+        ret = common.get_interfaces()
         self.mocks['interface_has_own_mac'].assert_has_calls(
             [mock.call('enp0s1'), mock.call('bond1')], any_order=True)
         expected = [
@@ -3183,7 +3131,7 @@ class TestGetInterfaces(CiTestCase):
         self.data['bonds'] = []
         self.data['own_macs'] = self.data['devices']
         self.data['bridges'] = [f for f in self.data['devices'] if f != "b1"]
-        ret = net.get_interfaces()
+        ret = common.get_interfaces()
         self.assertEqual([('b1', 'aa:aa:aa:aa:aa:b1', None, '0x0')], ret)
         self.mocks['is_bridge'].assert_has_calls(
             [mock.call('bridge1'), mock.call('enp0s1'), mock.call('bond1'),
@@ -3194,8 +3142,7 @@ class TestGetInterfaces(CiTestCase):
 class TestInterfaceHasOwnMac(CiTestCase):
     """Test interface_has_own_mac.  This is admittedly a bit whitebox."""
 
-    @mock.patch('cloudinit.net.read_sys_net_int', return_value=None)
-    def test_non_strict_with_no_addr_assign_type(self, m_read_sys_net_int):
+    def test_non_strict_with_no_addr_assign_type(self):
         """If nic does not have addr_assign_type, it is not "stolen".
 
         SmartOS containers do not provide the addr_assign_type in /sys.
@@ -3211,20 +3158,16 @@ class TestInterfaceHasOwnMac(CiTestCase):
         """
         self.assertTrue(interface_has_own_mac("eth0"))
 
-    @mock.patch('cloudinit.net.read_sys_net_int', return_value=None)
-    def test_strict_with_no_addr_assign_type_raises(self, m_read_sys_net_int):
+    def test_strict_with_no_addr_assign_type_raises(self):
         with self.assertRaises(ValueError):
             interface_has_own_mac("eth0", True)
 
-    @mock.patch('cloudinit.net.read_sys_net_int')
-    def test_expected_values(self, m_read_sys_net_int):
+    def test_expected_values(self):
         msg = "address_assign_type=%d said to not have own mac"
         for address_assign_type in (0, 1, 3):
-            m_read_sys_net_int.return_value = address_assign_type
             self.assertTrue(
                 interface_has_own_mac("eth0", msg % address_assign_type))
 
-        m_read_sys_net_int.return_value = 2
         self.assertFalse(interface_has_own_mac("eth0"))
 
 
@@ -3376,11 +3319,11 @@ class TestInterfacesSorting(CiTestCase):
     def test_natural_order(self):
         data = ['ens5', 'ens6', 'ens3', 'ens20', 'ens13', 'ens2']
         self.assertEqual(
-            sorted(data, key=natural_sort_key),
+            sorted(data, key=util.natural_sort_key),
             ['ens2', 'ens3', 'ens5', 'ens6', 'ens13', 'ens20'])
         data2 = ['enp2s0', 'enp2s3', 'enp0s3', 'enp0s13', 'enp0s8', 'enp1s2']
         self.assertEqual(
-            sorted(data2, key=natural_sort_key),
+            sorted(data2, key=util.natural_sort_key),
             ['enp0s3', 'enp0s8', 'enp0s13', 'enp1s2', 'enp2s0', 'enp2s3'])
 
 

Follow ups