cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #05747
[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
-
[Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Chad Smith, 2018-12-18
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-12-06
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-12-05
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-12-05
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-12-05
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-12-03
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Chad Smith, 2018-12-03
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-12-03
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Chad Smith, 2018-11-30
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Chad Smith, 2018-11-30
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Chad Smith, 2018-11-30
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Chad Smith, 2018-11-28
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Igor Galić, 2018-11-28
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Igor Galić, 2018-11-28
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-11-28
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-11-28
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-11-28
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-11-26
-
Re: [Merge] ~i.galic/cloud-init:refactor/net-fbsd into cloud-init:master
From: Server Team CI bot, 2018-11-15