summaryrefslogtreecommitdiff
path: root/cloudinit/net
diff options
context:
space:
mode:
authorzsdc <taras@vyos.io>2020-03-11 21:20:58 +0200
committerzsdc <taras@vyos.io>2020-03-11 21:22:23 +0200
commitc6627bc05a57645e6af8b9a5a67e452d9f37e487 (patch)
treeb754b3991e5e57a9ae9155819f73fa0cbd4be269 /cloudinit/net
parentca9a4eb26b41c204d1bd3a15586b14a5dde950bb (diff)
parent13e82554728b1cb524438163784e5b955c7c5ed0 (diff)
downloadvyos-cloud-init-c6627bc05a57645e6af8b9a5a67e452d9f37e487.tar.gz
vyos-cloud-init-c6627bc05a57645e6af8b9a5a67e452d9f37e487.zip
Cloud-init: T2117: Updated to 20.1
- Merge 20.1 version from the Canonical repository - Removed unneeded changes in datasources (now only OVF datasource is not equal to upstream's version) - Adapted cc_vyos module to new Cloud-init version - Changed Jenkinsfile to use build scripts, provided by upstream
Diffstat (limited to 'cloudinit/net')
-rw-r--r--cloudinit/net/__init__.py368
-rwxr-xr-xcloudinit/net/cmdline.py133
-rw-r--r--cloudinit/net/dhcp.py166
-rw-r--r--cloudinit/net/eni.py49
-rw-r--r--cloudinit/net/freebsd.py175
-rw-r--r--cloudinit/net/netplan.py60
-rw-r--r--cloudinit/net/network_state.py125
-rw-r--r--cloudinit/net/renderer.py4
-rw-r--r--cloudinit/net/renderers.py4
-rw-r--r--cloudinit/net/sysconfig.py428
-rw-r--r--cloudinit/net/tests/test_dhcp.py201
-rw-r--r--cloudinit/net/tests/test_init.py651
-rw-r--r--cloudinit/net/tests/test_network_state.py48
13 files changed, 2151 insertions, 261 deletions
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index 3642fb1f..1d5eb535 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -9,6 +9,7 @@ import errno
import logging
import os
import re
+from functools import partial
from cloudinit.net.network_state import mask_to_net_prefix
from cloudinit import util
@@ -108,6 +109,141 @@ def is_bond(devname):
return os.path.exists(sys_dev_path(devname, "bonding"))
+def get_master(devname):
+ """Return the master path for devname, or None if no master"""
+ path = sys_dev_path(devname, path="master")
+ if os.path.exists(path):
+ return path
+ return None
+
+
+def master_is_bridge_or_bond(devname):
+ """Return a bool indicating if devname's master is a bridge or bond"""
+ master_path = get_master(devname)
+ if master_path is None:
+ return False
+ bonding_path = os.path.join(master_path, "bonding")
+ bridge_path = os.path.join(master_path, "bridge")
+ return (os.path.exists(bonding_path) or os.path.exists(bridge_path))
+
+
+def is_netfailover(devname, driver=None):
+ """ netfailover driver uses 3 nics, master, primary and standby.
+ this returns True if the device is either the primary or standby
+ as these devices are to be ignored.
+ """
+ if driver is None:
+ driver = device_driver(devname)
+ if is_netfail_primary(devname, driver) or is_netfail_standby(devname,
+ driver):
+ return True
+ return False
+
+
+def get_dev_features(devname):
+ """ Returns a str from reading /sys/class/net/<devname>/device/features."""
+ features = ''
+ try:
+ features = read_sys_net(devname, 'device/features')
+ except Exception:
+ pass
+ return features
+
+
+def has_netfail_standby_feature(devname):
+ """ Return True if VIRTIO_NET_F_STANDBY bit (62) is set.
+
+ https://github.com/torvalds/linux/blob/ \
+ 089cf7f6ecb266b6a4164919a2e69bd2f938374a/ \
+ include/uapi/linux/virtio_net.h#L60
+ """
+ features = get_dev_features(devname)
+ if not features or len(features) < 64:
+ return False
+ return features[62] == "1"
+
+
+def is_netfail_master(devname, driver=None):
+ """ A device is a "netfail master" device if:
+
+ - The device does NOT have the 'master' sysfs attribute
+ - The device driver is 'virtio_net'
+ - The device has the standby feature bit set
+
+ Return True if all of the above is True.
+ """
+ if get_master(devname) is not None:
+ return False
+
+ if driver is None:
+ driver = device_driver(devname)
+
+ if driver != "virtio_net":
+ return False
+
+ if not has_netfail_standby_feature(devname):
+ return False
+
+ return True
+
+
+def is_netfail_primary(devname, driver=None):
+ """ A device is a "netfail primary" device if:
+
+ - the device has a 'master' sysfs file
+ - the device driver is not 'virtio_net'
+ - the 'master' sysfs file points to device with virtio_net driver
+ - the 'master' device has the 'standby' feature bit set
+
+ Return True if all of the above is True.
+ """
+ # /sys/class/net/<devname>/master -> ../../<master devname>
+ master_sysfs_path = sys_dev_path(devname, path='master')
+ if not os.path.exists(master_sysfs_path):
+ return False
+
+ if driver is None:
+ driver = device_driver(devname)
+
+ if driver == "virtio_net":
+ return False
+
+ master_devname = os.path.basename(os.path.realpath(master_sysfs_path))
+ master_driver = device_driver(master_devname)
+ if master_driver != "virtio_net":
+ return False
+
+ master_has_standby = has_netfail_standby_feature(master_devname)
+ if not master_has_standby:
+ return False
+
+ return True
+
+
+def is_netfail_standby(devname, driver=None):
+ """ A device is a "netfail standby" device if:
+
+ - The device has a 'master' sysfs attribute
+ - The device driver is 'virtio_net'
+ - The device has the standby feature bit set
+
+ Return True if all of the above is True.
+ """
+ if get_master(devname) is None:
+ return False
+
+ if driver is None:
+ driver = device_driver(devname)
+
+ if driver != "virtio_net":
+ return False
+
+ if not has_netfail_standby_feature(devname):
+ return False
+
+ return True
+
+
def is_renamed(devname):
"""
/* interface name assignment types (sysfs name_assign_type attribute) */
@@ -171,6 +307,9 @@ def device_devid(devname):
def get_devicelist():
+ if util.is_FreeBSD():
+ return list(get_interfaces_by_mac().values())
+
try:
devs = os.listdir(get_sys_class_path())
except OSError as e:
@@ -193,6 +332,35 @@ def is_disabled_cfg(cfg):
def find_fallback_nic(blacklist_drivers=None):
"""Return the name of the 'fallback' network device."""
+ if util.is_FreeBSD():
+ return find_fallback_nic_on_freebsd(blacklist_drivers)
+ else:
+ return find_fallback_nic_on_linux(blacklist_drivers)
+
+
+def find_fallback_nic_on_freebsd(blacklist_drivers=None):
+ """Return the name of the 'fallback' network device on FreeBSD.
+
+ @param blacklist_drivers: currently ignored
+ @return default interface, or None
+
+
+ we'll use the first interface from ``ifconfig -l -u ether``
+ """
+ stdout, _stderr = util.subp(['ifconfig', '-l', '-u', 'ether'])
+ values = stdout.split()
+ if values:
+ return values[0]
+ # On FreeBSD <= 10, 'ifconfig -l' ignores the interfaces with DOWN
+ # status
+ values = list(get_interfaces_by_mac().values())
+ values.sort()
+ if values:
+ return values[0]
+
+
+def find_fallback_nic_on_linux(blacklist_drivers=None):
+ """Return the name of the 'fallback' network device on Linux."""
if not blacklist_drivers:
blacklist_drivers = []
@@ -226,6 +394,9 @@ def find_fallback_nic(blacklist_drivers=None):
if is_bond(interface):
# skip any bonds
continue
+ if is_netfailover(interface):
+ # ignore netfailover primary/standby interfaces
+ continue
carrier = read_sys_net_int(interface, 'carrier')
if carrier:
connected.append(interface)
@@ -250,7 +421,7 @@ def find_fallback_nic(blacklist_drivers=None):
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).
+ # that we can read 'first' (using the sorted definition of first).
names = list(sorted(potential_interfaces, key=natural_sort_key))
if DEFAULT_PRIMARY_INTERFACE in names:
names.remove(DEFAULT_PRIMARY_INTERFACE)
@@ -264,46 +435,34 @@ def find_fallback_nic(blacklist_drivers=None):
def generate_fallback_config(blacklist_drivers=None, config_driver=None):
- """Determine which attached net dev is most likely to have a connection and
- generate network state to run dhcp on that interface"""
-
+ """Generate network cfg v2 for dhcp on the NIC most likely connected."""
if not config_driver:
config_driver = False
target_name = find_fallback_nic(blacklist_drivers=blacklist_drivers)
- if target_name:
- target_mac = read_sys_net_safe(target_name, 'address')
- nconf = {'config': [], 'version': 1}
- 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:
- driver = device_driver(target_name)
- if driver:
- cfg['params'] = {
- 'driver': driver,
- 'device_id': device_devid(target_name),
- }
- nconf['config'].append(cfg)
- return nconf
- else:
+ if not target_name:
# can't read any interfaces addresses (or there are none); give up
return None
+ # netfail cannot use mac for matching, they have duplicate macs
+ if is_netfail_master(target_name):
+ match = {'name': target_name}
+ else:
+ match = {
+ 'macaddress': read_sys_net_safe(target_name, 'address').lower()}
+ cfg = {'dhcp4': True, 'set-name': target_name, 'match': match}
+ if config_driver:
+ driver = device_driver(target_name)
+ if driver:
+ cfg['match']['driver'] = driver
+ nconf = {'ethernets': {target_name: cfg}, 'version': 2}
+ return nconf
-def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
- """read the network config and rename devices accordingly.
- if strict_present is false, then do not raise exception if no devices
- match. if strict_busy is false, then do not raise exception if the
- device cannot be renamed because it is currently configured.
- renames are only attempted for interfaces of type 'physical'. It is
- expected that the network system will create other devices with the
- correct name in place."""
+def extract_physdevs(netcfg):
def _version_1(netcfg):
- renames = []
+ physdevs = []
for ent in netcfg.get('config', {}):
if ent.get('type') != 'physical':
continue
@@ -317,11 +476,11 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
driver = device_driver(name)
if not device_id:
device_id = device_devid(name)
- renames.append([mac, name, driver, device_id])
- return renames
+ physdevs.append([mac, name, driver, device_id])
+ return physdevs
def _version_2(netcfg):
- renames = []
+ physdevs = []
for ent in netcfg.get('ethernets', {}).values():
# only rename if configured to do so
name = ent.get('set-name')
@@ -337,16 +496,69 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
driver = device_driver(name)
if not device_id:
device_id = device_devid(name)
- renames.append([mac, name, driver, device_id])
- return renames
+ physdevs.append([mac, name, driver, device_id])
+ return physdevs
+
+ version = netcfg.get('version')
+ if version == 1:
+ return _version_1(netcfg)
+ elif version == 2:
+ return _version_2(netcfg)
+
+ raise RuntimeError('Unknown network config version: %s' % version)
- if netcfg.get('version') == 1:
- return _rename_interfaces(_version_1(netcfg))
- elif netcfg.get('version') == 2:
- return _rename_interfaces(_version_2(netcfg))
- raise RuntimeError('Failed to apply network config names. Found bad'
- ' network config version: %s' % netcfg.get('version'))
+def wait_for_physdevs(netcfg, strict=True):
+ physdevs = extract_physdevs(netcfg)
+
+ # set of expected iface names and mac addrs
+ expected_ifaces = dict([(iface[0], iface[1]) for iface in physdevs])
+ expected_macs = set(expected_ifaces.keys())
+
+ # set of current macs
+ present_macs = get_interfaces_by_mac().keys()
+
+ # compare the set of expected mac address values to
+ # the current macs present; we only check MAC as cloud-init
+ # has not yet renamed interfaces and the netcfg may include
+ # such renames.
+ for _ in range(0, 5):
+ if expected_macs.issubset(present_macs):
+ LOG.debug('net: all expected physical devices present')
+ return
+
+ missing = expected_macs.difference(present_macs)
+ LOG.debug('net: waiting for expected net devices: %s', missing)
+ for mac in missing:
+ # trigger a settle, unless this interface exists
+ syspath = sys_dev_path(expected_ifaces[mac])
+ settle = partial(util.udevadm_settle, exists=syspath)
+ msg = 'Waiting for udev events to settle or %s exists' % syspath
+ util.log_time(LOG.debug, msg, func=settle)
+
+ # update present_macs after settles
+ present_macs = get_interfaces_by_mac().keys()
+
+ msg = 'Not all expected physical devices present: %s' % missing
+ LOG.warning(msg)
+ if strict:
+ raise RuntimeError(msg)
+
+
+def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
+ """read the network config and rename devices accordingly.
+ if strict_present is false, then do not raise exception if no devices
+ match. if strict_busy is false, then do not raise exception if the
+ device cannot be renamed because it is currently configured.
+
+ renames are only attempted for interfaces of type 'physical'. It is
+ expected that the network system will create other devices with the
+ correct name in place."""
+
+ try:
+ _rename_interfaces(extract_physdevs(netcfg))
+ except RuntimeError as e:
+ raise RuntimeError('Failed to apply network config names: %s' % e)
def interface_has_own_mac(ifname, strict=False):
@@ -585,6 +797,40 @@ def get_ib_interface_hwaddr(ifname, ethernet_format):
def get_interfaces_by_mac():
+ if util.is_FreeBSD():
+ return get_interfaces_by_mac_on_freebsd()
+ else:
+ return get_interfaces_by_mac_on_linux()
+
+
+def get_interfaces_by_mac_on_freebsd():
+ (out, _) = util.subp(['ifconfig', '-a', 'ether'])
+
+ # flatten each interface block in a single line
+ def flatten(out):
+ curr_block = ''
+ for l in out.split('\n'):
+ if l.startswith('\t'):
+ curr_block += l
+ else:
+ if curr_block:
+ yield curr_block
+ curr_block = l
+ yield curr_block
+
+ # looks for interface and mac in a list of flatten block
+ def find_mac(flat_list):
+ for block in flat_list:
+ m = re.search(
+ r"^(?P<ifname>\S*): .*ether\s(?P<mac>[\da-f:]{17}).*",
+ block)
+ if m:
+ yield (m.group('mac'), m.group('ifname'))
+ results = {mac: ifname for mac, ifname in find_mac(flatten(out))}
+ return results
+
+
+def get_interfaces_by_mac_on_linux():
"""Build a dictionary of tuples {mac: name}.
Bridges and any devices that have a 'stolen' mac are excluded."""
@@ -622,6 +868,12 @@ def get_interfaces():
continue
if is_vlan(name):
continue
+ if is_bond(name):
+ continue
+ if get_master(name) is not None and not master_is_bridge_or_bond(name):
+ continue
+ if is_netfailover(name):
+ continue
mac = get_interface_mac(name)
# some devices may not have a mac (tun0)
if not mac:
@@ -677,7 +929,7 @@ class EphemeralIPv4Network(object):
"""
def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None,
- connectivity_url=None):
+ connectivity_url=None, static_routes=None):
"""Setup context manager and validate call signature.
@param interface: Name of the network interface to bring up.
@@ -688,6 +940,7 @@ class EphemeralIPv4Network(object):
@param router: Optionally the default gateway IP.
@param connectivity_url: Optionally, a URL to verify if a usable
connection already exists.
+ @param static_routes: Optionally a list of static routes from DHCP
"""
if not all([interface, ip, prefix_or_mask, broadcast]):
raise ValueError(
@@ -704,6 +957,7 @@ class EphemeralIPv4Network(object):
self.ip = ip
self.broadcast = broadcast
self.router = router
+ self.static_routes = static_routes
self.cleanup_cmds = [] # List of commands to run to cleanup state.
def __enter__(self):
@@ -716,7 +970,21 @@ class EphemeralIPv4Network(object):
return
self._bringup_device()
- if self.router:
+
+ # rfc3442 requires us to ignore the router config *if* classless static
+ # routes are provided.
+ #
+ # https://tools.ietf.org/html/rfc3442
+ #
+ # If the DHCP server returns both a Classless Static Routes option and
+ # a Router option, the DHCP client MUST ignore the Router option.
+ #
+ # Similarly, if the DHCP server returns both a Classless Static Routes
+ # option and a Static Routes option, the DHCP client MUST ignore the
+ # Static Routes option.
+ if self.static_routes:
+ self._bringup_static_routes()
+ elif self.router:
self._bringup_router()
def __exit__(self, excp_type, excp_value, excp_traceback):
@@ -760,6 +1028,20 @@ class EphemeralIPv4Network(object):
['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev',
self.interface])
+ def _bringup_static_routes(self):
+ # static_routes = [("169.254.169.254/32", "130.56.248.255"),
+ # ("0.0.0.0/0", "130.56.240.1")]
+ for net_address, gateway in self.static_routes:
+ via_arg = []
+ if gateway != "0.0.0.0/0":
+ via_arg = ['via', gateway]
+ util.subp(
+ ['ip', '-4', 'route', 'add', net_address] + via_arg +
+ ['dev', self.interface], capture=True)
+ self.cleanup_cmds.insert(
+ 0, ['ip', '-4', 'route', 'del', net_address] + via_arg +
+ ['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
diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
index f89a0f73..64e1c699 100755
--- a/cloudinit/net/cmdline.py
+++ b/cloudinit/net/cmdline.py
@@ -5,20 +5,92 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+import abc
import base64
import glob
import gzip
import io
import os
+from cloudinit import util
+
from . import get_devicelist
from . import read_sys_net_safe
-from cloudinit import util
-
_OPEN_ISCSI_INTERFACE_FILE = "/run/initramfs/open-iscsi.interface"
+class InitramfsNetworkConfigSource(metaclass=abc.ABCMeta):
+ """ABC for net config sources that read config written by initramfses"""
+
+ @abc.abstractmethod
+ def is_applicable(self):
+ # type: () -> bool
+ """Is this initramfs config source applicable to the current system?"""
+ pass
+
+ @abc.abstractmethod
+ def render_config(self):
+ # type: () -> dict
+ """Render a v1 network config from the initramfs configuration"""
+ pass
+
+
+class KlibcNetworkConfigSource(InitramfsNetworkConfigSource):
+ """InitramfsNetworkConfigSource for klibc initramfs (i.e. Debian/Ubuntu)
+
+ Has three parameters, but they are intended to make testing simpler, _not_
+ for use in production code. (This is indicated by the prepended
+ underscores.)
+ """
+
+ def __init__(self, _files=None, _mac_addrs=None, _cmdline=None):
+ self._files = _files
+ self._mac_addrs = _mac_addrs
+ self._cmdline = _cmdline
+
+ # Set defaults here, as they require computation that we don't want to
+ # do at method definition time
+ if self._files is None:
+ self._files = _get_klibc_net_cfg_files()
+ if self._cmdline is None:
+ self._cmdline = util.get_cmdline()
+ if self._mac_addrs is None:
+ self._mac_addrs = {}
+ for k in get_devicelist():
+ mac_addr = read_sys_net_safe(k, 'address')
+ if mac_addr:
+ self._mac_addrs[k] = mac_addr
+
+ def is_applicable(self):
+ # type: () -> bool
+ """
+ Return whether this system has klibc initramfs network config or not
+
+ Will return True if:
+ (a) klibc files exist in /run, AND
+ (b) either:
+ (i) ip= or ip6= are on the kernel cmdline, OR
+ (ii) an open-iscsi interface file is present in the system
+ """
+ if self._files:
+ if 'ip=' in self._cmdline or 'ip6=' in self._cmdline:
+ return True
+ if os.path.exists(_OPEN_ISCSI_INTERFACE_FILE):
+ # iBft can configure networking without ip=
+ return True
+ return False
+
+ def render_config(self):
+ # type: () -> dict
+ return config_from_klibc_net_cfg(
+ files=self._files, mac_addrs=self._mac_addrs,
+ )
+
+
+_INITRAMFS_CONFIG_SOURCES = [KlibcNetworkConfigSource]
+
+
def _klibc_to_config_entry(content, mac_addrs=None):
"""Convert a klibc written shell content file to a 'config' entry
When ip= is seen on the kernel command line in debian initramfs
@@ -29,9 +101,12 @@ def _klibc_to_config_entry(content, mac_addrs=None):
provided here. There is no good documentation on this unfortunately.
DEVICE=<name> is expected/required and PROTO should indicate if
- this is 'static' or 'dhcp' or 'dhcp6' (LP: #1621507).
+ this is 'none' (static) or 'dhcp' or 'dhcp6' (LP: #1621507).
note that IPV6PROTO is also written by newer code to address the
possibility of both ipv4 and ipv6 getting addresses.
+
+ Full syntax is documented at:
+ https://git.kernel.org/pub/scm/libs/klibc/klibc.git/plain/usr/kinit/ipconfig/README.ipconfig
"""
if mac_addrs is None:
@@ -50,9 +125,9 @@ def _klibc_to_config_entry(content, mac_addrs=None):
if data.get('filename'):
proto = 'dhcp'
else:
- proto = 'static'
+ proto = 'none'
- if proto not in ('static', 'dhcp', 'dhcp6'):
+ if proto not in ('none', 'dhcp', 'dhcp6'):
raise ValueError("Unexpected value for PROTO: %s" % proto)
iface = {
@@ -72,6 +147,9 @@ def _klibc_to_config_entry(content, mac_addrs=None):
# PROTO for ipv4, IPV6PROTO for ipv6
cur_proto = data.get(pre + 'PROTO', proto)
+ # ipconfig's 'none' is called 'static'
+ if cur_proto == 'none':
+ cur_proto = 'static'
subnet = {'type': cur_proto, 'control': 'manual'}
# only populate address for static types. While the rendered config
@@ -137,6 +215,24 @@ def config_from_klibc_net_cfg(files=None, mac_addrs=None):
return {'config': entries, 'version': 1}
+def read_initramfs_config():
+ """
+ Return v1 network config for initramfs-configured networking (or None)
+
+ This will consider each _INITRAMFS_CONFIG_SOURCES entry in turn, and return
+ v1 network configuration for the first one that is applicable. If none are
+ applicable, return None.
+ """
+ for src_cls in _INITRAMFS_CONFIG_SOURCES:
+ cfg_source = src_cls()
+
+ if not cfg_source.is_applicable():
+ continue
+
+ return cfg_source.render_config()
+ return None
+
+
def _decomp_gzip(blob, strict=True):
# decompress blob. raise exception if not compressed unless strict=False.
with io.BytesIO(blob) as iobuf:
@@ -167,23 +263,10 @@ def _b64dgz(b64str, gzipped="try"):
return _decomp_gzip(blob, strict=gzipped != "try")
-def _is_initramfs_netconfig(files, cmdline):
- if files:
- if 'ip=' in cmdline or 'ip6=' in cmdline:
- return True
- if os.path.exists(_OPEN_ISCSI_INTERFACE_FILE):
- # iBft can configure networking without ip=
- return True
- return False
-
-
-def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
+def read_kernel_cmdline_config(cmdline=None):
if cmdline is None:
cmdline = util.get_cmdline()
- if files is None:
- files = _get_klibc_net_cfg_files()
-
if 'network-config=' in cmdline:
data64 = None
for tok in cmdline.split():
@@ -192,16 +275,6 @@ def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
if data64:
return util.load_yaml(_b64dgz(data64))
- if not _is_initramfs_netconfig(files, cmdline):
- return None
-
- if mac_addrs is None:
- mac_addrs = {}
- for k in get_devicelist():
- mac_addr = read_sys_net_safe(k, 'address')
- if mac_addr:
- mac_addrs[k] = mac_addr
-
- return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
+ return None
# vi: ts=4 expandtab
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
index 0db991db..19d0199c 100644
--- a/cloudinit/net/dhcp.py
+++ b/cloudinit/net/dhcp.py
@@ -9,6 +9,8 @@ import logging
import os
import re
import signal
+import time
+from io import StringIO
from cloudinit.net import (
EphemeralIPv4Network, find_fallback_nic, get_devicelist,
@@ -16,7 +18,6 @@ from cloudinit.net import (
from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip
from cloudinit import temp_utils
from cloudinit import util
-from six import StringIO
LOG = logging.getLogger(__name__)
@@ -91,10 +92,17 @@ class EphemeralDHCPv4(object):
nmap = {'interface': 'interface', 'ip': 'fixed-address',
'prefix_or_mask': 'subnet-mask',
'broadcast': 'broadcast-address',
+ 'static_routes': [
+ 'rfc3442-classless-static-routes',
+ 'classless-static-routes'
+ ],
'router': 'routers'}
- kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()])
+ kwargs = self.extract_dhcp_options_mapping(nmap)
if not kwargs['broadcast']:
kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])
+ if kwargs['static_routes']:
+ kwargs['static_routes'] = (
+ parse_static_routes(kwargs['static_routes']))
if self.connectivity_url:
kwargs['connectivity_url'] = self.connectivity_url
ephipv4 = EphemeralIPv4Network(**kwargs)
@@ -102,6 +110,25 @@ class EphemeralDHCPv4(object):
self._ephipv4 = ephipv4
return self.lease
+ def extract_dhcp_options_mapping(self, nmap):
+ result = {}
+ for internal_reference, lease_option_names in nmap.items():
+ if isinstance(lease_option_names, list):
+ self.get_first_option_value(
+ internal_reference,
+ lease_option_names,
+ result
+ )
+ else:
+ result[internal_reference] = self.lease.get(lease_option_names)
+ return result
+
+ def get_first_option_value(self, internal_mapping,
+ lease_option_names, result):
+ for different_names in lease_option_names:
+ if not result.get(internal_mapping):
+ result[internal_mapping] = self.lease.get(different_names)
+
def maybe_perform_dhcp_discovery(nic=None):
"""Perform dhcp discovery if nic valid and dhclient command exists.
@@ -127,7 +154,9 @@ def maybe_perform_dhcp_discovery(nic=None):
if not dhclient_path:
LOG.debug('Skip dhclient configuration: No dhclient command found.')
return []
- with temp_utils.tempdir(prefix='cloud-init-dhcp-', needs_exe=True) as tdir:
+ with temp_utils.tempdir(rmtree_ignore_errors=True,
+ prefix='cloud-init-dhcp-',
+ needs_exe=True) as tdir:
# Use /var/tmp because /run/cloud-init/tmp is mounted noexec
return dhcp_discovery(dhclient_path, nic, tdir)
@@ -195,24 +224,39 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir):
'-pf', pid_file, interface, '-sf', '/bin/true']
util.subp(cmd, capture=True)
- # dhclient doesn't write a pid file until after it forks when it gets a
- # proper lease response. Since cleandir is a temp directory that gets
- # removed, we need to wait for that pidfile creation before the
- # cleandir is removed, otherwise we get FileNotFound errors.
+ # Wait for pid file and lease file to appear, and for the process
+ # named by the pid file to daemonize (have pid 1 as its parent). If we
+ # try to read the lease file before daemonization happens, we might try
+ # to read it before the dhclient has actually written it. We also have
+ # to wait until the dhclient has become a daemon so we can be sure to
+ # kill the correct process, thus freeing cleandir to be deleted back
+ # up the callstack.
missing = util.wait_for_files(
[pid_file, lease_file], maxwait=5, naplen=0.01)
if missing:
LOG.warning("dhclient did not produce expected files: %s",
', '.join(os.path.basename(f) for f in missing))
return []
- pid_content = util.load_file(pid_file).strip()
- try:
- pid = int(pid_content)
- except ValueError:
- LOG.debug(
- "pid file contains non-integer content '%s'", pid_content)
- else:
- os.kill(pid, signal.SIGKILL)
+
+ ppid = 'unknown'
+ for _ in range(0, 1000):
+ pid_content = util.load_file(pid_file).strip()
+ try:
+ pid = int(pid_content)
+ except ValueError:
+ pass
+ else:
+ ppid = util.get_proc_ppid(pid)
+ if ppid == 1:
+ LOG.debug('killing dhclient with pid=%s', pid)
+ os.kill(pid, signal.SIGKILL)
+ return parse_dhcp_lease_file(lease_file)
+ time.sleep(0.01)
+
+ LOG.error(
+ 'dhclient(pid=%s, parentpid=%s) failed to daemonize after %s seconds',
+ pid_content, ppid, 0.01 * 1000
+ )
return parse_dhcp_lease_file(lease_file)
@@ -254,4 +298,96 @@ def networkd_get_option_from_leases(keyname, leases_d=None):
return data[keyname]
return None
+
+def parse_static_routes(rfc3442):
+ """ parse rfc3442 format and return a list containing tuple of strings.
+
+ The tuple is composed of the network_address (including net length) and
+ gateway for a parsed static route. It can parse two formats of rfc3442,
+ one from dhcpcd and one from dhclient (isc).
+
+ @param rfc3442: string in rfc3442 format (isc or dhcpd)
+ @returns: list of tuple(str, str) for all valid parsed routes until the
+ first parsing error.
+
+ E.g.
+ sr=parse_static_routes("32,169,254,169,254,130,56,248,255,0,130,56,240,1")
+ sr=[
+ ("169.254.169.254/32", "130.56.248.255"), ("0.0.0.0/0", "130.56.240.1")
+ ]
+
+ sr2 = parse_static_routes("24.191.168.128 192.168.128.1,0 192.168.128.1")
+ sr2 = [
+ ("191.168.128.0/24", "192.168.128.1"), ("0.0.0.0/0", "192.168.128.1")
+ ]
+
+ Python version of isc-dhclient's hooks:
+ /etc/dhcp/dhclient-exit-hooks.d/rfc3442-classless-routes
+ """
+ # raw strings from dhcp lease may end in semi-colon
+ rfc3442 = rfc3442.rstrip(";")
+ tokens = [tok for tok in re.split(r"[, .]", rfc3442) if tok]
+ static_routes = []
+
+ def _trunc_error(cidr, required, remain):
+ msg = ("RFC3442 string malformed. Current route has CIDR of %s "
+ "and requires %s significant octets, but only %s remain. "
+ "Verify DHCP rfc3442-classless-static-routes value: %s"
+ % (cidr, required, remain, rfc3442))
+ LOG.error(msg)
+
+ current_idx = 0
+ for idx, tok in enumerate(tokens):
+ if idx < current_idx:
+ continue
+ net_length = int(tok)
+ if net_length in range(25, 33):
+ req_toks = 9
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(tokens[idx+1:idx+5])
+ gateway = ".".join(tokens[idx+5:idx+req_toks])
+ current_idx = idx + req_toks
+ elif net_length in range(17, 25):
+ req_toks = 8
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(tokens[idx+1:idx+4] + ["0"])
+ gateway = ".".join(tokens[idx+4:idx+req_toks])
+ current_idx = idx + req_toks
+ elif net_length in range(9, 17):
+ req_toks = 7
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(tokens[idx+1:idx+3] + ["0", "0"])
+ gateway = ".".join(tokens[idx+3:idx+req_toks])
+ current_idx = idx + req_toks
+ elif net_length in range(1, 9):
+ req_toks = 6
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = ".".join(tokens[idx+1:idx+2] + ["0", "0", "0"])
+ gateway = ".".join(tokens[idx+2:idx+req_toks])
+ current_idx = idx + req_toks
+ elif net_length == 0:
+ req_toks = 5
+ if len(tokens[idx:]) < req_toks:
+ _trunc_error(net_length, req_toks, len(tokens[idx:]))
+ return static_routes
+ net_address = "0.0.0.0"
+ gateway = ".".join(tokens[idx+1:idx+req_toks])
+ current_idx = idx + req_toks
+ else:
+ LOG.error('Parsed invalid net length "%s". Verify DHCP '
+ 'rfc3442-classless-static-routes value.', net_length)
+ return static_routes
+
+ static_routes.append(("%s/%s" % (net_address, net_length), gateway))
+
+ return static_routes
+
# vi: ts=4 expandtab
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index 64236320..2f714563 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -94,7 +94,7 @@ def _iface_add_attrs(iface, index, ipv4_subnet_mtu):
]
renames = {'mac_address': 'hwaddress'}
- if iface['type'] not in ['bond', 'bridge', 'vlan']:
+ if iface['type'] not in ['bond', 'bridge', 'infiniband', 'vlan']:
ignore_map.append('mac_address')
for key, value in iface.items():
@@ -366,8 +366,6 @@ class Renderer(renderer.Renderer):
down = indent + "pre-down route del"
or_true = " || true"
mapping = {
- 'network': '-net',
- 'netmask': 'netmask',
'gateway': 'gw',
'metric': 'metric',
}
@@ -379,13 +377,21 @@ class Renderer(renderer.Renderer):
default_gw = ' -A inet6 default'
route_line = ''
- for k in ['network', 'netmask', 'gateway', 'metric']:
- if default_gw and k in ['network', 'netmask']:
+ for k in ['network', 'gateway', 'metric']:
+ if default_gw and k == 'network':
continue
if k == 'gateway':
route_line += '%s %s %s' % (default_gw, mapping[k], route[k])
elif k in route:
- route_line += ' %s %s' % (mapping[k], route[k])
+ if k == 'network':
+ if ':' in route[k]:
+ route_line += ' -A inet6'
+ else:
+ route_line += ' -net'
+ if 'prefix' in route:
+ route_line += ' %s/%s' % (route[k], route['prefix'])
+ else:
+ route_line += ' %s %s' % (mapping[k], route[k])
content.append(up + route_line + or_true)
content.append(down + route_line + or_true)
return content
@@ -393,6 +399,7 @@ class Renderer(renderer.Renderer):
def _render_iface(self, iface, render_hwaddress=False):
sections = []
subnets = iface.get('subnets', {})
+ accept_ra = iface.pop('accept-ra', None)
if subnets:
for index, subnet in enumerate(subnets):
ipv4_subnet_mtu = None
@@ -405,8 +412,29 @@ class Renderer(renderer.Renderer):
else:
ipv4_subnet_mtu = subnet.get('mtu')
iface['inet'] = subnet_inet
- if subnet['type'].startswith('dhcp'):
+ if (subnet['type'] == 'dhcp4' or subnet['type'] == 'dhcp6' or
+ subnet['type'] == 'ipv6_dhcpv6-stateful'):
+ # Configure network settings using DHCP or DHCPv6
iface['mode'] = 'dhcp'
+ if accept_ra is not None:
+ # Accept router advertisements (0=off, 1=on)
+ iface['accept_ra'] = '1' if accept_ra else '0'
+ elif subnet['type'] == 'ipv6_dhcpv6-stateless':
+ # Configure network settings using SLAAC from RAs
+ iface['mode'] = 'auto'
+ # Use stateless DHCPv6 (0=off, 1=on)
+ iface['dhcp'] = '1'
+ elif subnet['type'] == 'ipv6_slaac':
+ # Configure network settings using SLAAC from RAs
+ iface['mode'] = 'auto'
+ # Use stateless DHCPv6 (0=off, 1=on)
+ iface['dhcp'] = '0'
+ elif subnet_is_ipv6(subnet):
+ # mode might be static6, eni uses 'static'
+ iface['mode'] = 'static'
+ if accept_ra is not None:
+ # Accept router advertisements (0=off, 1=on)
+ iface['accept_ra'] = '1' if accept_ra else '0'
# do not emit multiple 'auto $IFACE' lines as older (precise)
# ifupdown complains
@@ -461,9 +489,10 @@ class Renderer(renderer.Renderer):
order = {
'loopback': 0,
'physical': 1,
- 'bond': 2,
- 'bridge': 3,
- 'vlan': 4,
+ 'infiniband': 2,
+ 'bond': 3,
+ 'bridge': 4,
+ 'vlan': 5,
}
sections = []
diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
new file mode 100644
index 00000000..d6f61da3
--- /dev/null
+++ b/cloudinit/net/freebsd.py
@@ -0,0 +1,175 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import re
+
+from cloudinit import log as logging
+from cloudinit import net
+from cloudinit import util
+from cloudinit.distros import rhel_util
+from cloudinit.distros.parsers.resolv_conf import ResolvConf
+
+from . import renderer
+
+LOG = logging.getLogger(__name__)
+
+
+class Renderer(renderer.Renderer):
+ resolv_conf_fn = 'etc/resolv.conf'
+ rc_conf_fn = 'etc/rc.conf'
+
+ def __init__(self, config=None):
+ if not config:
+ config = {}
+ self.dhcp_interfaces = []
+ self._postcmds = config.get('postcmds', True)
+
+ def _update_rc_conf(self, settings, target=None):
+ fn = util.target_path(target, self.rc_conf_fn)
+ rhel_util.update_sysconfig_file(fn, settings)
+
+ def _write_ifconfig_entries(self, settings, target=None):
+ ifname_by_mac = net.get_interfaces_by_mac()
+ for interface in settings.iter_interfaces():
+ device_name = interface.get("name")
+ device_mac = interface.get("mac_address")
+ if device_name and re.match(r'^lo\d+$', device_name):
+ continue
+ if device_mac not in ifname_by_mac:
+ LOG.info('Cannot find any device with MAC %s', device_mac)
+ elif device_mac and device_name:
+ cur_name = ifname_by_mac[device_mac]
+ if cur_name != device_name:
+ LOG.info('netif service will rename interface %s to %s',
+ cur_name, device_name)
+ self._update_rc_conf(
+ {'ifconfig_%s_name' % cur_name: device_name},
+ target=target)
+ else:
+ device_name = ifname_by_mac[device_mac]
+
+ LOG.info('Configuring interface %s', device_name)
+ ifconfig = 'DHCP' # default
+
+ for subnet in interface.get("subnets", []):
+ if ifconfig != 'DHCP':
+ LOG.info('The FreeBSD provider only set the first subnet.')
+ break
+ if subnet.get('type') == 'static':
+ if not subnet.get('netmask'):
+ LOG.debug(
+ 'Skipping IP %s, because there is no netmask',
+ subnet.get('address'))
+ continue
+ LOG.debug('Configuring dev %s with %s / %s', device_name,
+ subnet.get('address'), subnet.get('netmask'))
+ # Configure an ipv4 address.
+ ifconfig = (
+ subnet.get('address') + ' netmask ' +
+ subnet.get('netmask'))
+
+ if ifconfig == 'DHCP':
+ self.dhcp_interfaces.append(device_name)
+ self._update_rc_conf(
+ {'ifconfig_' + device_name: ifconfig},
+ target=target)
+
+ def _write_route_entries(self, settings, target=None):
+ routes = list(settings.iter_routes())
+ for interface in settings.iter_interfaces():
+ subnets = interface.get("subnets", [])
+ for subnet in subnets:
+ if subnet.get('type') != 'static':
+ continue
+ gateway = subnet.get('gateway')
+ if gateway and len(gateway.split('.')) == 4:
+ routes.append({
+ 'network': '0.0.0.0',
+ 'netmask': '0.0.0.0',
+ 'gateway': gateway})
+ routes += subnet.get('routes', [])
+ route_cpt = 0
+ for route in routes:
+ network = route.get('network')
+ if not network:
+ LOG.debug('Skipping a bad route entry')
+ continue
+ netmask = route.get('netmask')
+ gateway = route.get('gateway')
+ route_cmd = "-route %s/%s %s" % (network, netmask, gateway)
+ if network == '0.0.0.0':
+ self._update_rc_conf(
+ {'defaultrouter': gateway}, target=target)
+ else:
+ self._update_rc_conf(
+ {'route_net%d' % route_cpt: route_cmd}, target=target)
+ route_cpt += 1
+
+ def _write_resolve_conf(self, settings, target=None):
+ nameservers = settings.dns_nameservers
+ searchdomains = settings.dns_searchdomains
+ for interface in settings.iter_interfaces():
+ for subnet in interface.get("subnets", []):
+ if 'dns_nameservers' in subnet:
+ nameservers.extend(subnet['dns_nameservers'])
+ if 'dns_search' in subnet:
+ searchdomains.extend(subnet['dns_search'])
+ # Try to read the /etc/resolv.conf or just start from scratch if that
+ # fails.
+ try:
+ resolvconf = ResolvConf(util.load_file(util.target_path(
+ target, self.resolv_conf_fn)))
+ resolvconf.parse()
+ except IOError:
+ util.logexc(LOG, "Failed to parse %s, use new empty file",
+ util.target_path(target, self.resolv_conf_fn))
+ resolvconf = ResolvConf('')
+ resolvconf.parse()
+
+ # Add some nameservers
+ for server in nameservers:
+ try:
+ resolvconf.add_nameserver(server)
+ except ValueError:
+ util.logexc(LOG, "Failed to add nameserver %s", server)
+
+ # And add any searchdomains.
+ for domain in searchdomains:
+ try:
+ resolvconf.add_search_domain(domain)
+ except ValueError:
+ util.logexc(LOG, "Failed to add search domain %s", domain)
+ util.write_file(
+ util.target_path(target, self.resolv_conf_fn),
+ str(resolvconf), 0o644)
+
+ def _write_network(self, settings, target=None):
+ self._write_ifconfig_entries(settings, target=target)
+ self._write_route_entries(settings, target=target)
+ self._write_resolve_conf(settings, target=target)
+
+ self.start_services(run=self._postcmds)
+
+ def render_network_state(self, network_state, templates=None, target=None):
+ self._write_network(network_state, target=target)
+
+ def start_services(self, run=False):
+ if not run:
+ LOG.debug("freebsd generate postcmd disabled")
+ return
+
+ util.subp(['service', 'netif', 'restart'], capture=True)
+ # On FreeBSD 10, the restart of routing and dhclient is likely to fail
+ # because
+ # - routing: it cannot remove the loopback route, but it will still set
+ # up the default route as expected.
+ # - dhclient: it cannot stop the dhclient started by the netif service.
+ # In both case, the situation is ok, and we can proceed.
+ util.subp(['service', 'routing', 'restart'], capture=True, rcs=[0, 1])
+ for dhcp_interface in self.dhcp_interfaces:
+ util.subp(['service', 'dhclient', 'restart', dhcp_interface],
+ rcs=[0, 1],
+ capture=True)
+
+
+def available(target=None):
+ return util.is_FreeBSD()
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index 21517fda..89855270 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -4,10 +4,11 @@ import copy
import os
from . import renderer
-from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2
+from .network_state import subnet_is_ipv6, NET_CONFIG_TO_V2, IPV6_DYNAMIC_TYPES
from cloudinit import log as logging
from cloudinit import util
+from cloudinit import safeyaml
from cloudinit.net import SYS_CLASS_NET, get_devicelist
KNOWN_SNAPD_CONFIG = b"""\
@@ -34,7 +35,7 @@ def _get_params_dict_by_match(config, match):
if key.startswith(match))
-def _extract_addresses(config, entry, ifname):
+def _extract_addresses(config, entry, ifname, features=None):
"""This method parse a cloudinit.net.network_state dictionary (config) and
maps netstate keys/values into a dictionary (entry) to represent
netplan yaml.
@@ -51,7 +52,8 @@ def _extract_addresses(config, entry, ifname):
'mtu': 1480,
'netmask': 64,
'type': 'static'}],
- 'type: physical'
+ 'type: physical',
+ 'accept-ra': 'true'
}
An entry dictionary looks like:
@@ -66,7 +68,7 @@ def _extract_addresses(config, entry, ifname):
'match': {'macaddress': '52:54:00:12:34:00'},
'mtu': 1501,
'address': ['192.168.1.2/24', '2001:4800:78ff:1b:be76:4eff:fe06:1000"],
- 'mtu6': 1480}
+ 'ipv6-mtu': 1480}
"""
@@ -79,6 +81,8 @@ def _extract_addresses(config, entry, ifname):
else:
return [obj, ]
+ if features is None:
+ features = []
addresses = []
routes = []
nameservers = []
@@ -92,7 +96,9 @@ def _extract_addresses(config, entry, ifname):
if sn_type == 'dhcp':
sn_type += '4'
entry.update({sn_type: True})
- elif sn_type in ['static']:
+ elif sn_type in IPV6_DYNAMIC_TYPES:
+ entry.update({'dhcp6': True})
+ elif sn_type in ['static', 'static6']:
addr = "%s" % subnet.get('address')
if 'prefix' in subnet:
addr += "/%d" % subnet.get('prefix')
@@ -108,8 +114,8 @@ def _extract_addresses(config, entry, ifname):
searchdomains += _listify(subnet.get('dns_search', []))
if 'mtu' in subnet:
mtukey = 'mtu'
- if subnet_is_ipv6(subnet):
- mtukey += '6'
+ if subnet_is_ipv6(subnet) and 'ipv6-mtu' in features:
+ mtukey = 'ipv6-mtu'
entry.update({mtukey: subnet.get('mtu')})
for route in subnet.get('routes', []):
to_net = "%s/%s" % (route.get('network'),
@@ -144,6 +150,8 @@ def _extract_addresses(config, entry, ifname):
ns = entry.get('nameservers', {})
ns.update({'search': searchdomains})
entry.update({'nameservers': ns})
+ if 'accept-ra' in config and config['accept-ra'] is not None:
+ entry.update({'accept-ra': util.is_true(config.get('accept-ra'))})
def _extract_bond_slaves_by_name(interfaces, entry, bond_master):
@@ -179,6 +187,7 @@ class Renderer(renderer.Renderer):
"""Renders network information in a /etc/netplan/network.yaml format."""
NETPLAN_GENERATE = ['netplan', 'generate']
+ NETPLAN_INFO = ['netplan', 'info']
def __init__(self, config=None):
if not config:
@@ -188,6 +197,22 @@ class Renderer(renderer.Renderer):
self.netplan_header = config.get('netplan_header', None)
self._postcmds = config.get('postcmds', False)
self.clean_default = config.get('clean_default', True)
+ self._features = config.get('features', None)
+
+ @property
+ def features(self):
+ if self._features is None:
+ try:
+ info_blob, _err = util.subp(self.NETPLAN_INFO, capture=True)
+ info = util.load_yaml(info_blob)
+ self._features = info['netplan.io']['features']
+ except util.ProcessExecutionError:
+ # if the info subcommand is not present then we don't have any
+ # new features
+ pass
+ except (TypeError, KeyError) as e:
+ LOG.debug('Failed to list features from netplan info: %s', e)
+ return self._features
def render_network_state(self, network_state, templates=None, target=None):
# check network state for version
@@ -235,9 +260,9 @@ class Renderer(renderer.Renderer):
# if content already in netplan format, pass it back
if network_state.version == 2:
LOG.debug('V2 to V2 passthrough')
- return util.yaml_dumps({'network': network_state.config},
- explicit_start=False,
- explicit_end=False)
+ return safeyaml.dumps({'network': network_state.config},
+ explicit_start=False,
+ explicit_end=False)
ethernets = {}
wifis = {}
@@ -271,7 +296,7 @@ class Renderer(renderer.Renderer):
else:
del eth['match']
del eth['set-name']
- _extract_addresses(ifcfg, eth, ifname)
+ _extract_addresses(ifcfg, eth, ifname, self.features)
ethernets.update({ifname: eth})
elif if_type == 'bond':
@@ -296,7 +321,7 @@ class Renderer(renderer.Renderer):
slave_interfaces = ifcfg.get('bond-slaves')
if slave_interfaces == 'none':
_extract_bond_slaves_by_name(interfaces, bond, ifname)
- _extract_addresses(ifcfg, bond, ifname)
+ _extract_addresses(ifcfg, bond, ifname, self.features)
bonds.update({ifname: bond})
elif if_type == 'bridge':
@@ -331,7 +356,7 @@ class Renderer(renderer.Renderer):
bridge.update({'parameters': br_config})
if ifcfg.get('mac_address'):
bridge['macaddress'] = ifcfg.get('mac_address').lower()
- _extract_addresses(ifcfg, bridge, ifname)
+ _extract_addresses(ifcfg, bridge, ifname, self.features)
bridges.update({ifname: bridge})
elif if_type == 'vlan':
@@ -343,7 +368,7 @@ class Renderer(renderer.Renderer):
macaddr = ifcfg.get('mac_address', None)
if macaddr is not None:
vlan['macaddress'] = macaddr.lower()
- _extract_addresses(ifcfg, vlan, ifname)
+ _extract_addresses(ifcfg, vlan, ifname, self.features)
vlans.update({ifname: vlan})
# inject global nameserver values under each all interface which
@@ -359,9 +384,10 @@ class Renderer(renderer.Renderer):
# workaround yaml dictionary key sorting when dumping
def _render_section(name, section):
if section:
- dump = util.yaml_dumps({name: section},
- explicit_start=False,
- explicit_end=False)
+ dump = safeyaml.dumps({name: section},
+ explicit_start=False,
+ explicit_end=False,
+ noalias=True)
txt = util.indent(dump, ' ' * 4)
return [txt]
return []
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index f76e508a..63d6e291 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -10,19 +10,23 @@ import logging
import socket
import struct
-import six
-
+from cloudinit import safeyaml
from cloudinit import util
LOG = logging.getLogger(__name__)
NETWORK_STATE_VERSION = 1
+IPV6_DYNAMIC_TYPES = ['dhcp6',
+ 'ipv6_slaac',
+ 'ipv6_dhcpv6-stateless',
+ 'ipv6_dhcpv6-stateful']
NETWORK_STATE_REQUIRED_KEYS = {
1: ['version', 'config', 'network_state'],
}
NETWORK_V2_KEY_FILTER = [
- 'addresses', 'dhcp4', 'dhcp6', 'gateway4', 'gateway6', 'interfaces',
- 'match', 'mtu', 'nameservers', 'renderer', 'set-name', 'wakeonlan'
+ 'addresses', 'dhcp4', 'dhcp4-overrides', 'dhcp6', 'dhcp6-overrides',
+ 'gateway4', 'gateway6', 'interfaces', 'match', 'mtu', 'nameservers',
+ 'renderer', 'set-name', 'wakeonlan', 'accept-ra'
]
NET_CONFIG_TO_V2 = {
@@ -67,7 +71,7 @@ def parse_net_config_data(net_config, skip_broken=True):
# pass the whole net-config as-is
config = net_config
- if version and config:
+ if version and config is not None:
nsi = NetworkStateInterpreter(version=version, config=config)
nsi.parse_config(skip_broken=skip_broken)
state = nsi.get_network_state()
@@ -148,6 +152,7 @@ class NetworkState(object):
self._network_state = copy.deepcopy(network_state)
self._version = version
self.use_ipv6 = network_state.get('use_ipv6', False)
+ self._has_default_route = None
@property
def config(self):
@@ -157,14 +162,6 @@ class NetworkState(object):
def version(self):
return self._version
- def iter_routes(self, filter_func=None):
- for route in self._network_state.get('routes', []):
- if filter_func is not None:
- if filter_func(route):
- yield route
- else:
- yield route
-
@property
def dns_nameservers(self):
try:
@@ -179,18 +176,49 @@ class NetworkState(object):
except KeyError:
return []
+ @property
+ def has_default_route(self):
+ if self._has_default_route is None:
+ self._has_default_route = self._maybe_has_default_route()
+ return self._has_default_route
+
def iter_interfaces(self, filter_func=None):
ifaces = self._network_state.get('interfaces', {})
- for iface in six.itervalues(ifaces):
+ for iface in ifaces.values():
if filter_func is None:
yield iface
else:
if filter_func(iface):
yield iface
+ def iter_routes(self, filter_func=None):
+ for route in self._network_state.get('routes', []):
+ if filter_func is not None:
+ if filter_func(route):
+ yield route
+ else:
+ yield route
+
+ def _maybe_has_default_route(self):
+ for route in self.iter_routes():
+ if self._is_default_route(route):
+ return True
+ for iface in self.iter_interfaces():
+ for subnet in iface.get('subnets', []):
+ for route in subnet.get('routes', []):
+ if self._is_default_route(route):
+ return True
+ return False
+
+ def _is_default_route(self, route):
+ default_nets = ('::', '0.0.0.0')
+ return (
+ route.get('prefix') == 0
+ and route.get('network') in default_nets
+ )
-@six.add_metaclass(CommandHandlerMeta)
-class NetworkStateInterpreter(object):
+
+class NetworkStateInterpreter(metaclass=CommandHandlerMeta):
initial_network_state = {
'interfaces': {},
@@ -228,7 +256,7 @@ class NetworkStateInterpreter(object):
'config': self._config,
'network_state': self._network_state,
}
- return util.yaml_dumps(state)
+ return safeyaml.dumps(state)
def load(self, state):
if 'version' not in state:
@@ -247,7 +275,7 @@ class NetworkStateInterpreter(object):
setattr(self, key, state[key])
def dump_network_state(self):
- return util.yaml_dumps(self._network_state)
+ return safeyaml.dumps(self._network_state)
def as_dict(self):
return {'version': self._version, 'config': self._config}
@@ -315,7 +343,8 @@ class NetworkStateInterpreter(object):
'name': 'eth0',
'subnets': [
{'type': 'dhcp4'}
- ]
+ ],
+ 'accept-ra': 'true'
}
'''
@@ -335,6 +364,9 @@ class NetworkStateInterpreter(object):
self.use_ipv6 = True
break
+ accept_ra = command.get('accept-ra', None)
+ if accept_ra is not None:
+ accept_ra = util.is_true(accept_ra)
iface.update({
'name': command.get('name'),
'type': command.get('type'),
@@ -345,6 +377,7 @@ class NetworkStateInterpreter(object):
'address': None,
'gateway': None,
'subnets': subnets,
+ 'accept-ra': accept_ra
})
self._network_state['interfaces'].update({command.get('name'): iface})
self.dump_network_state()
@@ -571,6 +604,7 @@ class NetworkStateInterpreter(object):
eno1:
match:
macaddress: 00:11:22:33:44:55
+ driver: hv_netsvc
wakeonlan: true
dhcp4: true
dhcp6: false
@@ -587,6 +621,7 @@ class NetworkStateInterpreter(object):
driver: ixgbe
set-name: lom1
dhcp6: true
+ accept-ra: true
switchports:
match:
name: enp2*
@@ -606,15 +641,18 @@ class NetworkStateInterpreter(object):
'type': 'physical',
'name': cfg.get('set-name', eth),
}
- mac_address = cfg.get('match', {}).get('macaddress', None)
+ match = cfg.get('match', {})
+ mac_address = match.get('macaddress', None)
if not mac_address:
LOG.debug('NetworkState Version2: missing "macaddress" info '
'in config entry: %s: %s', eth, str(cfg))
- phy_cmd.update({'mac_address': mac_address})
-
- for key in ['mtu', 'match', 'wakeonlan']:
+ phy_cmd['mac_address'] = mac_address
+ driver = match.get('driver', None)
+ if driver:
+ phy_cmd['params'] = {'driver': driver}
+ for key in ['mtu', 'match', 'wakeonlan', 'accept-ra']:
if key in cfg:
- phy_cmd.update({key: cfg.get(key)})
+ phy_cmd[key] = cfg[key]
subnets = self._v2_to_v1_ipcfg(cfg)
if len(subnets) > 0:
@@ -648,6 +686,8 @@ class NetworkStateInterpreter(object):
'vlan_id': cfg.get('id'),
'vlan_link': cfg.get('link'),
}
+ if 'mtu' in cfg:
+ vlan_cmd['mtu'] = cfg['mtu']
subnets = self._v2_to_v1_ipcfg(cfg)
if len(subnets) > 0:
vlan_cmd.update({'subnets': subnets})
@@ -682,6 +722,14 @@ class NetworkStateInterpreter(object):
item_params = dict((key, value) for (key, value) in
item_cfg.items() if key not in
NETWORK_V2_KEY_FILTER)
+ # we accept the fixed spelling, but write the old for compatability
+ # Xenial does not have an updated netplan which supports the
+ # correct spelling. LP: #1756701
+ params = item_params['parameters']
+ grat_value = params.pop('gratuitous-arp', None)
+ if grat_value:
+ params['gratuitious-arp'] = grat_value
+
v1_cmd = {
'type': cmd_type,
'name': item_name,
@@ -689,6 +737,8 @@ class NetworkStateInterpreter(object):
'params': dict((v2key_to_v1[k], v) for k, v in
item_params.get('parameters', {}).items())
}
+ if 'mtu' in item_cfg:
+ v1_cmd['mtu'] = item_cfg['mtu']
subnets = self._v2_to_v1_ipcfg(item_cfg)
if len(subnets) > 0:
v1_cmd.update({'subnets': subnets})
@@ -705,12 +755,20 @@ class NetworkStateInterpreter(object):
def _v2_to_v1_ipcfg(self, cfg):
"""Common ipconfig extraction from v2 to v1 subnets array."""
+ def _add_dhcp_overrides(overrides, subnet):
+ if 'route-metric' in overrides:
+ subnet['metric'] = overrides['route-metric']
+
subnets = []
- if 'dhcp4' in cfg:
- subnets.append({'type': 'dhcp4'})
- if 'dhcp6' in cfg:
+ if cfg.get('dhcp4'):
+ subnet = {'type': 'dhcp4'}
+ _add_dhcp_overrides(cfg.get('dhcp4-overrides', {}), subnet)
+ subnets.append(subnet)
+ if cfg.get('dhcp6'):
+ subnet = {'type': 'dhcp6'}
self.use_ipv6 = True
- subnets.append({'type': 'dhcp6'})
+ _add_dhcp_overrides(cfg.get('dhcp6-overrides', {}), subnet)
+ subnets.append(subnet)
gateway4 = None
gateway6 = None
@@ -877,9 +935,10 @@ def is_ipv6_addr(address):
def subnet_is_ipv6(subnet):
"""Common helper for checking network_state subnets for ipv6."""
- # 'static6' or 'dhcp6'
- if subnet['type'].endswith('6'):
- # This is a request for DHCPv6.
+ # 'static6', 'dhcp6', 'ipv6_dhcpv6-stateful', 'ipv6_dhcpv6-stateless' or
+ # 'ipv6_slaac'
+ if subnet['type'].endswith('6') or subnet['type'] in IPV6_DYNAMIC_TYPES:
+ # This is a request either static6 type or DHCPv6.
return True
elif subnet['type'] == 'static' and is_ipv6_addr(subnet.get('address')):
return True
@@ -908,7 +967,7 @@ def ipv4_mask_to_net_prefix(mask):
"""
if isinstance(mask, int):
return mask
- if isinstance(mask, six.string_types):
+ if isinstance(mask, str):
try:
return int(mask)
except ValueError:
@@ -935,7 +994,7 @@ def ipv6_mask_to_net_prefix(mask):
if isinstance(mask, int):
return mask
- if isinstance(mask, six.string_types):
+ if isinstance(mask, str):
try:
return int(mask)
except ValueError:
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
index 5f32e90f..2a61a7a8 100644
--- a/cloudinit/net/renderer.py
+++ b/cloudinit/net/renderer.py
@@ -6,7 +6,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
import abc
-import six
+import io
from .network_state import parse_net_config_data
from .udev import generate_udev_rule
@@ -34,7 +34,7 @@ class Renderer(object):
"""Given state, emit udev rules to map mac to ifname."""
# TODO(harlowja): this seems shared between eni renderer and
# this, so move it to a shared location.
- content = six.StringIO()
+ content = io.StringIO()
for iface in network_state.iter_interfaces(filter_by_physical):
# for physical interfaces write out a persist net udev rule
if 'name' in iface and iface.get('mac_address'):
diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py
index 5117b4a5..b98dbbe3 100644
--- a/cloudinit/net/renderers.py
+++ b/cloudinit/net/renderers.py
@@ -1,17 +1,19 @@
# This file is part of cloud-init. See LICENSE file for license information.
from . import eni
+from . import freebsd
from . import netplan
from . import RendererNotFoundError
from . import sysconfig
NAME_TO_RENDERER = {
"eni": eni,
+ "freebsd": freebsd,
"netplan": netplan,
"sysconfig": sysconfig,
}
-DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan"]
+DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd"]
def search(priority=None, target=None, first=False):
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 17293e1d..0a387377 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -1,20 +1,24 @@
# This file is part of cloud-init. See LICENSE file for license information.
+import copy
+import io
import os
import re
-import six
+from configobj import ConfigObj
-from cloudinit.distros.parsers import networkmanager_conf
-from cloudinit.distros.parsers import resolv_conf
from cloudinit import log as logging
from cloudinit import util
+from cloudinit.distros.parsers import networkmanager_conf
+from cloudinit.distros.parsers import resolv_conf
from . import renderer
from .network_state import (
- is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6)
+ is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES)
LOG = logging.getLogger(__name__)
+NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf"
+KNOWN_DISTROS = ['centos', 'fedora', 'rhel', 'suse']
def _make_header(sep='#'):
@@ -46,6 +50,24 @@ def _quote_value(value):
return value
+def enable_ifcfg_rh(path):
+ """Add ifcfg-rh to NetworkManager.cfg plugins if main section is present"""
+ config = ConfigObj(path)
+ if 'main' in config:
+ if 'plugins' in config['main']:
+ if 'ifcfg-rh' in config['main']['plugins']:
+ return
+ else:
+ config['main']['plugins'] = []
+
+ if isinstance(config['main']['plugins'], list):
+ config['main']['plugins'].append('ifcfg-rh')
+ else:
+ config['main']['plugins'] = [config['main']['plugins'], 'ifcfg-rh']
+ config.write()
+ LOG.debug('Enabled ifcfg-rh NetworkManager plugins')
+
+
class ConfigMap(object):
"""Sysconfig like dictionary object."""
@@ -64,6 +86,9 @@ class ConfigMap(object):
def __getitem__(self, key):
return self._conf[key]
+ def get(self, key):
+ return self._conf.get(key)
+
def __contains__(self, key):
return key in self._conf
@@ -74,7 +99,7 @@ class ConfigMap(object):
return len(self._conf)
def to_string(self):
- buf = six.StringIO()
+ buf = io.StringIO()
buf.write(_make_header())
if self._conf:
buf.write("\n")
@@ -82,11 +107,14 @@ class ConfigMap(object):
value = self._conf[key]
if isinstance(value, bool):
value = self._bool_map[value]
- if not isinstance(value, six.string_types):
+ if not isinstance(value, str):
value = str(value)
buf.write("%s=%s\n" % (key, _quote_value(value)))
return buf.getvalue()
+ def update(self, updates):
+ self._conf.update(updates)
+
class Route(ConfigMap):
"""Represents a route configuration."""
@@ -128,7 +156,7 @@ class Route(ConfigMap):
# only accept ipv4 and ipv6
if proto not in ['ipv4', 'ipv6']:
raise ValueError("Unknown protocol '%s'" % (str(proto)))
- buf = six.StringIO()
+ buf = io.StringIO()
buf.write(_make_header())
if self._conf:
buf.write("\n")
@@ -247,12 +275,29 @@ class Renderer(renderer.Renderer):
# s1-networkscripts-interfaces.html (or other docs for
# details about this)
- iface_defaults = tuple([
- ('ONBOOT', True),
- ('USERCTL', False),
- ('NM_CONTROLLED', False),
- ('BOOTPROTO', 'none'),
- ])
+ iface_defaults = {
+ 'rhel': {'ONBOOT': True, 'USERCTL': False, 'NM_CONTROLLED': False,
+ 'BOOTPROTO': 'none'},
+ 'suse': {'BOOTPROTO': 'static', 'STARTMODE': 'auto'},
+ }
+
+ cfg_key_maps = {
+ 'rhel': {
+ 'accept-ra': 'IPV6_FORCE_ACCEPT_RA',
+ 'bridge_stp': 'STP',
+ 'bridge_ageing': 'AGEING',
+ 'bridge_bridgeprio': 'PRIO',
+ 'mac_address': 'HWADDR',
+ 'mtu': 'MTU',
+ },
+ 'suse': {
+ 'bridge_stp': 'BRIDGE_STP',
+ 'bridge_ageing': 'BRIDGE_AGEINGTIME',
+ 'bridge_bridgeprio': 'BRIDGE_PRIORITY',
+ 'mac_address': 'LLADDR',
+ 'mtu': 'MTU',
+ },
+ }
# If these keys exist, then their values will be used to form
# a BONDING_OPTS grouping; otherwise no grouping will be set.
@@ -260,12 +305,18 @@ class Renderer(renderer.Renderer):
('bond_mode', "mode=%s"),
('bond_xmit_hash_policy', "xmit_hash_policy=%s"),
('bond_miimon', "miimon=%s"),
- ])
-
- bridge_opts_keys = tuple([
- ('bridge_stp', 'STP'),
- ('bridge_ageing', 'AGEING'),
- ('bridge_bridgeprio', 'PRIO'),
+ ('bond_min_links', "min_links=%s"),
+ ('bond_arp_interval', "arp_interval=%s"),
+ ('bond_arp_ip_target', "arp_ip_target=%s"),
+ ('bond_arp_validate', "arp_validate=%s"),
+ ('bond_ad_select', "ad_select=%s"),
+ ('bond_num_grat_arp', "num_grat_arp=%s"),
+ ('bond_downdelay', "downdelay=%s"),
+ ('bond_updelay', "updelay=%s"),
+ ('bond_lacp_rate', "lacp_rate=%s"),
+ ('bond_fail_over_mac', "fail_over_mac=%s"),
+ ('bond_primary', "primary=%s"),
+ ('bond_primary_reselect', "primary_reselect=%s"),
])
templates = {}
@@ -285,46 +336,101 @@ class Renderer(renderer.Renderer):
'iface_templates': config.get('iface_templates'),
'route_templates': config.get('route_templates'),
}
+ self.flavor = config.get('flavor', 'rhel')
@classmethod
- def _render_iface_shared(cls, iface, iface_cfg):
- for k, v in cls.iface_defaults:
- iface_cfg[k] = v
+ def _render_iface_shared(cls, iface, iface_cfg, flavor):
+ flavor_defaults = copy.deepcopy(cls.iface_defaults.get(flavor, {}))
+ iface_cfg.update(flavor_defaults)
- for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
+ for old_key in ('mac_address', 'mtu', 'accept-ra'):
old_value = iface.get(old_key)
if old_value is not None:
# only set HWADDR on physical interfaces
- if old_key == 'mac_address' and iface['type'] != 'physical':
+ if (old_key == 'mac_address' and
+ iface['type'] not in ['physical', 'infiniband']):
continue
- iface_cfg[new_key] = old_value
+ new_key = cls.cfg_key_maps[flavor].get(old_key)
+ if new_key:
+ iface_cfg[new_key] = old_value
@classmethod
- def _render_subnets(cls, iface_cfg, subnets):
+ def _render_subnets(cls, iface_cfg, subnets, has_default_route, flavor):
# setting base values
- iface_cfg['BOOTPROTO'] = 'none'
+ if flavor == 'suse':
+ iface_cfg['BOOTPROTO'] = 'static'
+ if 'BRIDGE' in iface_cfg:
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ iface_cfg.drop('BRIDGE')
+ else:
+ iface_cfg['BOOTPROTO'] = 'none'
# modifying base values according to subnets
for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
mtu_key = 'MTU'
subnet_type = subnet.get('type')
- if subnet_type == 'dhcp6':
- iface_cfg['IPV6INIT'] = True
- iface_cfg['DHCPV6C'] = True
+ if subnet_type == 'dhcp6' or subnet_type == 'ipv6_dhcpv6-stateful':
+ if flavor == 'suse':
+ # User wants dhcp for both protocols
+ if iface_cfg['BOOTPROTO'] == 'dhcp4':
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ else:
+ # Only IPv6 is DHCP, IPv4 may be static
+ iface_cfg['BOOTPROTO'] = 'dhcp6'
+ iface_cfg['DHCLIENT6_MODE'] = 'managed'
+ else:
+ iface_cfg['IPV6INIT'] = True
+ # Configure network settings using DHCPv6
+ iface_cfg['DHCPV6C'] = True
+ elif subnet_type == 'ipv6_dhcpv6-stateless':
+ if flavor == 'suse':
+ # User wants dhcp for both protocols
+ if iface_cfg['BOOTPROTO'] == 'dhcp4':
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ else:
+ # Only IPv6 is DHCP, IPv4 may be static
+ iface_cfg['BOOTPROTO'] = 'dhcp6'
+ iface_cfg['DHCLIENT6_MODE'] = 'info'
+ else:
+ iface_cfg['IPV6INIT'] = True
+ # Configure network settings using SLAAC from RAs and
+ # optional info from dhcp server using DHCPv6
+ iface_cfg['IPV6_AUTOCONF'] = True
+ iface_cfg['DHCPV6C'] = True
+ # Use Information-request to get only stateless
+ # configuration parameters (i.e., without address).
+ iface_cfg['DHCPV6C_OPTIONS'] = '-S'
+ elif subnet_type == 'ipv6_slaac':
+ if flavor == 'suse':
+ # User wants dhcp for both protocols
+ if iface_cfg['BOOTPROTO'] == 'dhcp4':
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ else:
+ # Only IPv6 is DHCP, IPv4 may be static
+ iface_cfg['BOOTPROTO'] = 'dhcp6'
+ iface_cfg['DHCLIENT6_MODE'] = 'info'
+ else:
+ iface_cfg['IPV6INIT'] = True
+ # Configure network settings using SLAAC from RAs
+ iface_cfg['IPV6_AUTOCONF'] = True
elif subnet_type in ['dhcp4', 'dhcp']:
+ bootproto_in = iface_cfg['BOOTPROTO']
iface_cfg['BOOTPROTO'] = 'dhcp'
- elif subnet_type == 'static':
+ if flavor == 'suse' and subnet_type == 'dhcp4':
+ # If dhcp6 is already specified the user wants dhcp
+ # for both protocols
+ if bootproto_in != 'dhcp6':
+ # Only IPv4 is DHCP, IPv6 may be static
+ iface_cfg['BOOTPROTO'] = 'dhcp4'
+ elif subnet_type in ['static', 'static6']:
+ # RH info
# grep BOOTPROTO sysconfig.txt -A2 | head -3
# BOOTPROTO=none|bootp|dhcp
# 'bootp' or 'dhcp' cause a DHCP client
# to run on the device. Any other
# value causes any static configuration
# in the file to be applied.
- # ==> the following should not be set to 'static'
- # but should remain 'none'
- # if iface_cfg['BOOTPROTO'] == 'none':
- # iface_cfg['BOOTPROTO'] = 'static'
- if subnet_is_ipv6(subnet):
+ if subnet_is_ipv6(subnet) and flavor != 'suse':
mtu_key = 'IPV6_MTU'
iface_cfg['IPV6INIT'] = True
if 'mtu' in subnet:
@@ -335,37 +441,70 @@ class Renderer(renderer.Renderer):
'Network config: ignoring %s device-level mtu:%s'
' because ipv4 subnet-level mtu:%s provided.',
iface_cfg.name, iface_cfg[mtu_key], subnet['mtu'])
- iface_cfg[mtu_key] = subnet['mtu']
+ if subnet_is_ipv6(subnet):
+ if flavor == 'suse':
+ # TODO(rjschwei) write mtu setting to
+ # /etc/sysctl.d/
+ pass
+ else:
+ iface_cfg[mtu_key] = subnet['mtu']
+ else:
+ iface_cfg[mtu_key] = subnet['mtu']
elif subnet_type == 'manual':
- # If the subnet has an MTU setting, then ONBOOT=True
- # to apply the setting
- iface_cfg['ONBOOT'] = mtu_key in iface_cfg
+ if flavor == 'suse':
+ LOG.debug('Unknown subnet type setting "%s"', subnet_type)
+ else:
+ # If the subnet has an MTU setting, then ONBOOT=True
+ # to apply the setting
+ iface_cfg['ONBOOT'] = mtu_key in iface_cfg
else:
raise ValueError("Unknown subnet type '%s' found"
" for interface '%s'" % (subnet_type,
iface_cfg.name))
if subnet.get('control') == 'manual':
- iface_cfg['ONBOOT'] = False
+ if flavor == 'suse':
+ iface_cfg['STARTMODE'] = 'manual'
+ else:
+ iface_cfg['ONBOOT'] = False
# set IPv4 and IPv6 static addresses
ipv4_index = -1
ipv6_index = -1
for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
subnet_type = subnet.get('type')
- if subnet_type == 'dhcp6':
+ # metric may apply to both dhcp and static config
+ if 'metric' in subnet:
+ if flavor != 'suse':
+ iface_cfg['METRIC'] = subnet['metric']
+ if subnet_type in ['dhcp', 'dhcp4']:
+ # On SUSE distros 'DHCLIENT_SET_DEFAULT_ROUTE' is a global
+ # setting in /etc/sysconfig/network/dhcp
+ if flavor != 'suse':
+ if has_default_route and iface_cfg['BOOTPROTO'] != 'none':
+ iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = False
continue
- elif subnet_type in ['dhcp4', 'dhcp']:
+ elif subnet_type in IPV6_DYNAMIC_TYPES:
continue
- elif subnet_type == 'static':
+ elif subnet_type in ['static', 'static6']:
if subnet_is_ipv6(subnet):
ipv6_index = ipv6_index + 1
ipv6_cidr = "%s/%s" % (subnet['address'], subnet['prefix'])
if ipv6_index == 0:
- iface_cfg['IPV6ADDR'] = ipv6_cidr
+ if flavor == 'suse':
+ iface_cfg['IPADDR6'] = ipv6_cidr
+ else:
+ iface_cfg['IPV6ADDR'] = ipv6_cidr
elif ipv6_index == 1:
- iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
+ if flavor == 'suse':
+ iface_cfg['IPADDR6_1'] = ipv6_cidr
+ else:
+ iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
else:
- iface_cfg['IPV6ADDR_SECONDARIES'] += " " + ipv6_cidr
+ if flavor == 'suse':
+ iface_cfg['IPADDR6_%d' % ipv6_index] = ipv6_cidr
+ else:
+ iface_cfg['IPV6ADDR_SECONDARIES'] += \
+ " " + ipv6_cidr
else:
ipv4_index = ipv4_index + 1
suff = "" if ipv4_index == 0 else str(ipv4_index)
@@ -373,20 +512,17 @@ class Renderer(renderer.Renderer):
iface_cfg['NETMASK' + suff] = \
net_prefix_to_ipv4_mask(subnet['prefix'])
- if 'gateway' in subnet:
+ if 'gateway' in subnet and flavor != 'suse':
iface_cfg['DEFROUTE'] = True
if is_ipv6_addr(subnet['gateway']):
iface_cfg['IPV6_DEFAULTGW'] = subnet['gateway']
else:
iface_cfg['GATEWAY'] = subnet['gateway']
- if 'metric' in subnet:
- iface_cfg['METRIC'] = subnet['metric']
-
- if 'dns_search' in subnet:
+ if 'dns_search' in subnet and flavor != 'suse':
iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search'])
- if 'dns_nameservers' in subnet:
+ if 'dns_nameservers' in subnet and flavor != 'suse':
if len(subnet['dns_nameservers']) > 3:
# per resolv.conf(5) MAXNS sets this to 3.
LOG.debug("%s has %d entries in dns_nameservers. "
@@ -396,12 +532,21 @@ class Renderer(renderer.Renderer):
iface_cfg['DNS' + str(i)] = k
@classmethod
- def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
+ def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets, flavor):
+ # TODO(rjschwei): route configuration on SUSE distro happens via
+ # ifroute-* files, see lp#1812117. SUSE currently carries a local
+ # patch in their package.
+ if flavor == 'suse':
+ return
for _, subnet in enumerate(subnets, start=len(iface_cfg.children)):
+ subnet_type = subnet.get('type')
for route in subnet.get('routes', []):
is_ipv6 = subnet.get('ipv6') or is_ipv6_addr(route['gateway'])
- if _is_default_route(route):
+ # Any dynamic configuration method, slaac, dhcpv6-stateful/
+ # stateless should get router information from router RA's.
+ if (_is_default_route(route) and subnet_type not in
+ IPV6_DYNAMIC_TYPES):
if (
(subnet.get('ipv4') and
route_cfg.has_set_default_ipv4) or
@@ -420,8 +565,10 @@ class Renderer(renderer.Renderer):
# TODO(harlowja): add validation that no other iface has
# also provided the default route?
iface_cfg['DEFROUTE'] = True
+ if iface_cfg['BOOTPROTO'] in ('dhcp', 'dhcp4'):
+ iface_cfg['DHCLIENT_SET_DEFAULT_ROUTE'] = True
if 'gateway' in route:
- if is_ipv6 or is_ipv6_addr(route['gateway']):
+ if is_ipv6:
iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
route_cfg.has_set_default_ipv6 = True
else:
@@ -462,7 +609,9 @@ class Renderer(renderer.Renderer):
iface_cfg['BONDING_OPTS'] = " ".join(bond_opts)
@classmethod
- def _render_physical_interfaces(cls, network_state, iface_contents):
+ def _render_physical_interfaces(
+ cls, network_state, iface_contents, flavor
+ ):
physical_filter = renderer.filter_by_physical
for iface in network_state.iter_interfaces(physical_filter):
iface_name = iface['name']
@@ -470,11 +619,16 @@ class Renderer(renderer.Renderer):
iface_cfg = iface_contents[iface_name]
route_cfg = iface_cfg.routes
- cls._render_subnets(iface_cfg, iface_subnets)
- cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+ cls._render_subnets(
+ iface_cfg, iface_subnets, network_state.has_default_route,
+ flavor
+ )
+ cls._render_subnet_routes(
+ iface_cfg, route_cfg, iface_subnets, flavor
+ )
@classmethod
- def _render_bond_interfaces(cls, network_state, iface_contents):
+ def _render_bond_interfaces(cls, network_state, iface_contents, flavor):
bond_filter = renderer.filter_by_type('bond')
slave_filter = renderer.filter_by_attr('bond-master')
for iface in network_state.iter_interfaces(bond_filter):
@@ -488,15 +642,24 @@ class Renderer(renderer.Renderer):
master_cfgs.extend(iface_cfg.children)
for master_cfg in master_cfgs:
master_cfg['BONDING_MASTER'] = True
- master_cfg.kind = 'bond'
+ if flavor != 'suse':
+ master_cfg.kind = 'bond'
if iface.get('mac_address'):
- iface_cfg['MACADDR'] = iface.get('mac_address')
+ if flavor == 'suse':
+ iface_cfg['LLADDR'] = iface.get('mac_address')
+ else:
+ iface_cfg['MACADDR'] = iface.get('mac_address')
iface_subnets = iface.get("subnets", [])
route_cfg = iface_cfg.routes
- cls._render_subnets(iface_cfg, iface_subnets)
- cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+ cls._render_subnets(
+ iface_cfg, iface_subnets, network_state.has_default_route,
+ flavor
+ )
+ cls._render_subnet_routes(
+ iface_cfg, route_cfg, iface_subnets, flavor
+ )
# iter_interfaces on network-state is not sorted to produce
# consistent numbers we need to sort.
@@ -506,29 +669,51 @@ class Renderer(renderer.Renderer):
if slave_iface['bond-master'] == iface_name])
for index, bond_slave in enumerate(bond_slaves):
- slavestr = 'BONDING_SLAVE%s' % index
+ if flavor == 'suse':
+ slavestr = 'BONDING_SLAVE_%s' % index
+ else:
+ slavestr = 'BONDING_SLAVE%s' % index
iface_cfg[slavestr] = bond_slave
slave_cfg = iface_contents[bond_slave]
- slave_cfg['MASTER'] = iface_name
- slave_cfg['SLAVE'] = True
+ if flavor == 'suse':
+ slave_cfg['BOOTPROTO'] = 'none'
+ slave_cfg['STARTMODE'] = 'hotplug'
+ else:
+ slave_cfg['MASTER'] = iface_name
+ slave_cfg['SLAVE'] = True
@classmethod
- def _render_vlan_interfaces(cls, network_state, iface_contents):
+ def _render_vlan_interfaces(cls, network_state, iface_contents, flavor):
vlan_filter = renderer.filter_by_type('vlan')
for iface in network_state.iter_interfaces(vlan_filter):
iface_name = iface['name']
iface_cfg = iface_contents[iface_name]
- iface_cfg['VLAN'] = True
- iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
+ if flavor == 'suse':
+ vlan_id = iface.get('vlan_id')
+ if vlan_id:
+ iface_cfg['VLAN_ID'] = vlan_id
+ iface_cfg['ETHERDEVICE'] = iface_name[:iface_name.rfind('.')]
+ else:
+ iface_cfg['VLAN'] = True
+ iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
iface_subnets = iface.get("subnets", [])
route_cfg = iface_cfg.routes
- cls._render_subnets(iface_cfg, iface_subnets)
- cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+ cls._render_subnets(
+ iface_cfg, iface_subnets, network_state.has_default_route,
+ flavor
+ )
+ cls._render_subnet_routes(
+ iface_cfg, route_cfg, iface_subnets, flavor
+ )
@staticmethod
def _render_dns(network_state, existing_dns_path=None):
+ # skip writing resolv.conf if network_state doesn't include any input.
+ if not any([len(network_state.dns_nameservers),
+ len(network_state.dns_searchdomains)]):
+ return None
content = resolv_conf.ResolvConf("")
if existing_dns_path and os.path.isfile(existing_dns_path):
content = resolv_conf.ResolvConf(util.load_file(existing_dns_path))
@@ -558,19 +743,39 @@ class Renderer(renderer.Renderer):
return out
@classmethod
- def _render_bridge_interfaces(cls, network_state, iface_contents):
+ def _render_bridge_interfaces(cls, network_state, iface_contents, flavor):
+ bridge_key_map = {
+ old_k: new_k for old_k, new_k in cls.cfg_key_maps[flavor].items()
+ if old_k.startswith('bridge')}
bridge_filter = renderer.filter_by_type('bridge')
+
for iface in network_state.iter_interfaces(bridge_filter):
iface_name = iface['name']
iface_cfg = iface_contents[iface_name]
- iface_cfg.kind = 'bridge'
- for old_key, new_key in cls.bridge_opts_keys:
+ if flavor != 'suse':
+ iface_cfg.kind = 'bridge'
+ for old_key, new_key in bridge_key_map.items():
if old_key in iface:
iface_cfg[new_key] = iface[old_key]
- if iface.get('mac_address'):
- iface_cfg['MACADDR'] = iface.get('mac_address')
+ if flavor == 'suse':
+ if 'BRIDGE_STP' in iface_cfg:
+ if iface_cfg.get('BRIDGE_STP'):
+ iface_cfg['BRIDGE_STP'] = 'on'
+ else:
+ iface_cfg['BRIDGE_STP'] = 'off'
+ if iface.get('mac_address'):
+ key = 'MACADDR'
+ if flavor == 'suse':
+ key = 'LLADDRESS'
+ iface_cfg[key] = iface.get('mac_address')
+
+ if flavor == 'suse':
+ if iface.get('bridge_ports', []):
+ iface_cfg['BRIDGE_PORTS'] = '%s' % " ".join(
+ iface.get('bridge_ports')
+ )
# Is this the right key to get all the connected interfaces?
for bridged_iface_name in iface.get('bridge_ports', []):
# Ensure all bridged interfaces are correctly tagged
@@ -579,15 +784,23 @@ class Renderer(renderer.Renderer):
bridged_cfgs = [bridged_cfg]
bridged_cfgs.extend(bridged_cfg.children)
for bridge_cfg in bridged_cfgs:
- bridge_cfg['BRIDGE'] = iface_name
+ bridge_value = iface_name
+ if flavor == 'suse':
+ bridge_value = 'yes'
+ bridge_cfg['BRIDGE'] = bridge_value
iface_subnets = iface.get("subnets", [])
route_cfg = iface_cfg.routes
- cls._render_subnets(iface_cfg, iface_subnets)
- cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+ cls._render_subnets(
+ iface_cfg, iface_subnets, network_state.has_default_route,
+ flavor
+ )
+ cls._render_subnet_routes(
+ iface_cfg, route_cfg, iface_subnets, flavor
+ )
@classmethod
- def _render_ib_interfaces(cls, network_state, iface_contents):
+ def _render_ib_interfaces(cls, network_state, iface_contents, flavor):
ib_filter = renderer.filter_by_type('infiniband')
for iface in network_state.iter_interfaces(ib_filter):
iface_name = iface['name']
@@ -595,11 +808,16 @@ class Renderer(renderer.Renderer):
iface_cfg.kind = 'infiniband'
iface_subnets = iface.get("subnets", [])
route_cfg = iface_cfg.routes
- cls._render_subnets(iface_cfg, iface_subnets)
- cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
+ cls._render_subnets(
+ iface_cfg, iface_subnets, network_state.has_default_route,
+ flavor
+ )
+ cls._render_subnet_routes(
+ iface_cfg, route_cfg, iface_subnets, flavor
+ )
@classmethod
- def _render_sysconfig(cls, base_sysconf_dir, network_state,
+ def _render_sysconfig(cls, base_sysconf_dir, network_state, flavor,
templates=None):
'''Given state, return /etc/sysconfig files + contents'''
if not templates:
@@ -610,13 +828,17 @@ class Renderer(renderer.Renderer):
continue
iface_name = iface['name']
iface_cfg = NetInterface(iface_name, base_sysconf_dir, templates)
- cls._render_iface_shared(iface, iface_cfg)
+ if flavor == 'suse':
+ iface_cfg.drop('DEVICE')
+ # If type detection fails it is considered a bug in SUSE
+ iface_cfg.drop('TYPE')
+ cls._render_iface_shared(iface, iface_cfg, flavor)
iface_contents[iface_name] = iface_cfg
- cls._render_physical_interfaces(network_state, iface_contents)
- cls._render_bond_interfaces(network_state, iface_contents)
- cls._render_vlan_interfaces(network_state, iface_contents)
- cls._render_bridge_interfaces(network_state, iface_contents)
- cls._render_ib_interfaces(network_state, iface_contents)
+ cls._render_physical_interfaces(network_state, iface_contents, flavor)
+ cls._render_bond_interfaces(network_state, iface_contents, flavor)
+ cls._render_vlan_interfaces(network_state, iface_contents, flavor)
+ cls._render_bridge_interfaces(network_state, iface_contents, flavor)
+ cls._render_ib_interfaces(network_state, iface_contents, flavor)
contents = {}
for iface_name, iface_cfg in iface_contents.items():
if iface_cfg or iface_cfg.children:
@@ -638,14 +860,15 @@ class Renderer(renderer.Renderer):
file_mode = 0o644
base_sysconf_dir = util.target_path(target, self.sysconf_dir)
for path, data in self._render_sysconfig(base_sysconf_dir,
- network_state,
+ network_state, self.flavor,
templates=templates).items():
util.write_file(path, data, file_mode)
if self.dns_path:
dns_path = util.target_path(target, self.dns_path)
resolv_content = self._render_dns(network_state,
existing_dns_path=dns_path)
- util.write_file(dns_path, resolv_content, file_mode)
+ if resolv_content:
+ util.write_file(dns_path, resolv_content, file_mode)
if self.networkmanager_conf_path:
nm_conf_path = util.target_path(target,
self.networkmanager_conf_path)
@@ -657,6 +880,8 @@ class Renderer(renderer.Renderer):
netrules_content = self._render_persistent_net(network_state)
netrules_path = util.target_path(target, self.netrules_path)
util.write_file(netrules_path, netrules_content, file_mode)
+ if available_nm(target=target):
+ enable_ifcfg_rh(util.target_path(target, path=NM_CFG_FILE))
sysconfig_path = util.target_path(target, templates.get('control'))
# Distros configuring /etc/sysconfig/network as a file e.g. Centos
@@ -671,6 +896,13 @@ class Renderer(renderer.Renderer):
def available(target=None):
+ sysconfig = available_sysconfig(target=target)
+ nm = available_nm(target=target)
+ return (util.system_info()['variant'] in KNOWN_DISTROS
+ and any([nm, sysconfig]))
+
+
+def available_sysconfig(target=None):
expected = ['ifup', 'ifdown']
search = ['/sbin', '/usr/sbin']
for p in expected:
@@ -679,10 +911,16 @@ def available(target=None):
expected_paths = [
'etc/sysconfig/network-scripts/network-functions',
- 'etc/sysconfig/network-scripts/ifdown-eth']
+ 'etc/sysconfig/config']
for p in expected_paths:
- if not os.path.isfile(util.target_path(target, p)):
- return False
+ if os.path.isfile(util.target_path(target, p)):
+ return True
+ return False
+
+
+def available_nm(target=None):
+ if not os.path.isfile(util.target_path(target, path=NM_CFG_FILE)):
+ return False
return True
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
index cd3e7328..c3fa1e04 100644
--- a/cloudinit/net/tests/test_dhcp.py
+++ b/cloudinit/net/tests/test_dhcp.py
@@ -8,7 +8,8 @@ from textwrap import dedent
import cloudinit.net as net
from cloudinit.net.dhcp import (
InvalidDHCPLeaseFileError, maybe_perform_dhcp_discovery,
- parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases)
+ parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases,
+ parse_static_routes)
from cloudinit.util import ensure_file, write_file
from cloudinit.tests.helpers import (
CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call)
@@ -64,6 +65,188 @@ class TestParseDHCPLeasesFile(CiTestCase):
self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))
+class TestDHCPRFC3442(CiTestCase):
+
+ def test_parse_lease_finds_rfc3442_classless_static_routes(self):
+ """parse_dhcp_lease_file returns rfc3442-classless-static-routes."""
+ lease_file = self.tmp_path('leases')
+ content = dedent("""
+ lease {
+ interface "wlp3s0";
+ fixed-address 192.168.2.74;
+ option subnet-mask 255.255.255.0;
+ option routers 192.168.2.1;
+ option rfc3442-classless-static-routes 0,130,56,240,1;
+ renew 4 2017/07/27 18:02:30;
+ expire 5 2017/07/28 07:08:15;
+ }
+ """)
+ expected = [
+ {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
+ 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
+ 'rfc3442-classless-static-routes': '0,130,56,240,1',
+ 'renew': '4 2017/07/27 18:02:30',
+ 'expire': '5 2017/07/28 07:08:15'}]
+ write_file(lease_file, content)
+ self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))
+
+ def test_parse_lease_finds_classless_static_routes(self):
+ """
+ parse_dhcp_lease_file returns classless-static-routes
+ for Centos lease format.
+ """
+ lease_file = self.tmp_path('leases')
+ content = dedent("""
+ lease {
+ interface "wlp3s0";
+ fixed-address 192.168.2.74;
+ option subnet-mask 255.255.255.0;
+ option routers 192.168.2.1;
+ option classless-static-routes 0 130.56.240.1;
+ renew 4 2017/07/27 18:02:30;
+ expire 5 2017/07/28 07:08:15;
+ }
+ """)
+ expected = [
+ {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
+ 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
+ 'classless-static-routes': '0 130.56.240.1',
+ 'renew': '4 2017/07/27 18:02:30',
+ 'expire': '5 2017/07/28 07:08:15'}]
+ write_file(lease_file, content)
+ self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file))
+
+ @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
+ @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
+ def test_obtain_lease_parses_static_routes(self, m_maybe, m_ipv4):
+ """EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network"""
+ lease = [
+ {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
+ 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
+ 'rfc3442-classless-static-routes': '0,130,56,240,1',
+ 'renew': '4 2017/07/27 18:02:30',
+ 'expire': '5 2017/07/28 07:08:15'}]
+ m_maybe.return_value = lease
+ eph = net.dhcp.EphemeralDHCPv4()
+ eph.obtain_lease()
+ expected_kwargs = {
+ 'interface': 'wlp3s0',
+ 'ip': '192.168.2.74',
+ 'prefix_or_mask': '255.255.255.0',
+ 'broadcast': '192.168.2.255',
+ 'static_routes': [('0.0.0.0/0', '130.56.240.1')],
+ 'router': '192.168.2.1'}
+ m_ipv4.assert_called_with(**expected_kwargs)
+
+ @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
+ @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
+ def test_obtain_centos_lease_parses_static_routes(self, m_maybe, m_ipv4):
+ """
+ EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network
+ for Centos Lease format
+ """
+ lease = [
+ {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74',
+ 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1',
+ 'classless-static-routes': '0 130.56.240.1',
+ 'renew': '4 2017/07/27 18:02:30',
+ 'expire': '5 2017/07/28 07:08:15'}]
+ m_maybe.return_value = lease
+ eph = net.dhcp.EphemeralDHCPv4()
+ eph.obtain_lease()
+ expected_kwargs = {
+ 'interface': 'wlp3s0',
+ 'ip': '192.168.2.74',
+ 'prefix_or_mask': '255.255.255.0',
+ 'broadcast': '192.168.2.255',
+ 'static_routes': [('0.0.0.0/0', '130.56.240.1')],
+ 'router': '192.168.2.1'}
+ m_ipv4.assert_called_with(**expected_kwargs)
+
+
+class TestDHCPParseStaticRoutes(CiTestCase):
+
+ with_logs = True
+
+ def parse_static_routes_empty_string(self):
+ self.assertEqual([], parse_static_routes(""))
+
+ def test_parse_static_routes_invalid_input_returns_empty_list(self):
+ rfc3442 = "32,169,254,169,254,130,56,248"
+ self.assertEqual([], parse_static_routes(rfc3442))
+
+ def test_parse_static_routes_bogus_width_returns_empty_list(self):
+ rfc3442 = "33,169,254,169,254,130,56,248"
+ self.assertEqual([], parse_static_routes(rfc3442))
+
+ def test_parse_static_routes_single_ip(self):
+ rfc3442 = "32,169,254,169,254,130,56,248,255"
+ self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
+ parse_static_routes(rfc3442))
+
+ def test_parse_static_routes_single_ip_handles_trailing_semicolon(self):
+ rfc3442 = "32,169,254,169,254,130,56,248,255;"
+ self.assertEqual([('169.254.169.254/32', '130.56.248.255')],
+ parse_static_routes(rfc3442))
+
+ def test_parse_static_routes_default_route(self):
+ rfc3442 = "0,130,56,240,1"
+ self.assertEqual([('0.0.0.0/0', '130.56.240.1')],
+ parse_static_routes(rfc3442))
+
+ def test_parse_static_routes_class_c_b_a(self):
+ class_c = "24,192,168,74,192,168,0,4"
+ class_b = "16,172,16,172,16,0,4"
+ class_a = "8,10,10,0,0,4"
+ rfc3442 = ",".join([class_c, class_b, class_a])
+ self.assertEqual(sorted([
+ ("192.168.74.0/24", "192.168.0.4"),
+ ("172.16.0.0/16", "172.16.0.4"),
+ ("10.0.0.0/8", "10.0.0.4")
+ ]), sorted(parse_static_routes(rfc3442)))
+
+ def test_parse_static_routes_logs_error_truncated(self):
+ bad_rfc3442 = {
+ "class_c": "24,169,254,169,10",
+ "class_b": "16,172,16,10",
+ "class_a": "8,10,10",
+ "gateway": "0,0",
+ "netlen": "33,0",
+ }
+ for rfc3442 in bad_rfc3442.values():
+ self.assertEqual([], parse_static_routes(rfc3442))
+
+ logs = self.logs.getvalue()
+ self.assertEqual(len(bad_rfc3442.keys()), len(logs.splitlines()))
+
+ def test_parse_static_routes_returns_valid_routes_until_parse_err(self):
+ class_c = "24,192,168,74,192,168,0,4"
+ class_b = "16,172,16,172,16,0,4"
+ class_a_error = "8,10,10,0,0"
+ rfc3442 = ",".join([class_c, class_b, class_a_error])
+ self.assertEqual(sorted([
+ ("192.168.74.0/24", "192.168.0.4"),
+ ("172.16.0.0/16", "172.16.0.4"),
+ ]), sorted(parse_static_routes(rfc3442)))
+
+ logs = self.logs.getvalue()
+ self.assertIn(rfc3442, logs.splitlines()[0])
+
+ def test_redhat_format(self):
+ redhat_format = "24.191.168.128 192.168.128.1,0 192.168.128.1"
+ self.assertEqual(sorted([
+ ("191.168.128.0/24", "192.168.128.1"),
+ ("0.0.0.0/0", "192.168.128.1")
+ ]), sorted(parse_static_routes(redhat_format)))
+
+ def test_redhat_format_with_a_space_too_much_after_comma(self):
+ redhat_format = "24.191.168.128 192.168.128.1, 0 192.168.128.1"
+ self.assertEqual(sorted([
+ ("191.168.128.0/24", "192.168.128.1"),
+ ("0.0.0.0/0", "192.168.128.1")
+ ]), sorted(parse_static_routes(redhat_format)))
+
+
class TestDHCPDiscoveryClean(CiTestCase):
with_logs = True
@@ -117,6 +300,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
self.assertEqual('eth9', call[0][1])
self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2])
+ @mock.patch('time.sleep', mock.MagicMock())
@mock.patch('cloudinit.net.dhcp.os.kill')
@mock.patch('cloudinit.net.dhcp.util.subp')
def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp,
@@ -145,16 +329,20 @@ class TestDHCPDiscoveryClean(CiTestCase):
'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],
dhcp_discovery(dhclient_script, 'eth9', tmpdir))
self.assertIn(
- "pid file contains non-integer content ''", self.logs.getvalue())
+ "dhclient(pid=, parentpid=unknown) failed "
+ "to daemonize after 10.0 seconds",
+ self.logs.getvalue())
m_kill.assert_not_called()
+ @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid')
@mock.patch('cloudinit.net.dhcp.os.kill')
@mock.patch('cloudinit.net.dhcp.util.wait_for_files')
@mock.patch('cloudinit.net.dhcp.util.subp')
def test_dhcp_discovery_run_in_sandbox_waits_on_lease_and_pid(self,
m_subp,
m_wait,
- m_kill):
+ m_kill,
+ m_getppid):
"""dhcp_discovery waits for the presence of pidfile and dhcp.leases."""
tmpdir = self.tmp_dir()
dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
@@ -164,6 +352,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
pidfile = self.tmp_path('dhclient.pid', tmpdir)
leasefile = self.tmp_path('dhcp.leases', tmpdir)
m_wait.return_value = [pidfile] # Return the missing pidfile wait for
+ m_getppid.return_value = 1 # Indicate that dhclient has daemonized
self.assertEqual([], dhcp_discovery(dhclient_script, 'eth9', tmpdir))
self.assertEqual(
mock.call([pidfile, leasefile], maxwait=5, naplen=0.01),
@@ -173,9 +362,10 @@ class TestDHCPDiscoveryClean(CiTestCase):
self.logs.getvalue())
m_kill.assert_not_called()
+ @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid')
@mock.patch('cloudinit.net.dhcp.os.kill')
@mock.patch('cloudinit.net.dhcp.util.subp')
- def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill):
+ def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill, m_getppid):
"""dhcp_discovery brings up the interface and runs dhclient.
It also returns the parsed dhcp.leases file generated in the sandbox.
@@ -197,6 +387,7 @@ class TestDHCPDiscoveryClean(CiTestCase):
pid_file = os.path.join(tmpdir, 'dhclient.pid')
my_pid = 1
write_file(pid_file, "%d\n" % my_pid)
+ m_getppid.return_value = 1 # Indicate that dhclient has daemonized
self.assertItemsEqual(
[{'interface': 'eth9', 'fixed-address': '192.168.2.74',
@@ -355,3 +546,5 @@ class TestEphemeralDhcpNoNetworkSetup(HttprettyTestCase):
self.assertEqual(fake_lease, lease)
# Ensure that dhcp discovery occurs
m_dhcp.called_once_with()
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index f55c31e8..5081a337 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -3,15 +3,15 @@
import copy
import errno
import httpretty
-import mock
import os
import requests
import textwrap
-import yaml
+from unittest import mock
import cloudinit.net as net
from cloudinit.util import ensure_file, write_file, ProcessExecutionError
from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase
+from cloudinit import safeyaml as yaml
class TestSysDevPath(CiTestCase):
@@ -157,6 +157,41 @@ class TestReadSysNet(CiTestCase):
ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
self.assertTrue(net.is_bond('eth0'))
+ def test_get_master(self):
+ """get_master returns the path when /sys/net/devname/master exists."""
+ self.assertIsNone(net.get_master('enP1s1'))
+ master_path = os.path.join(self.sysdir, 'enP1s1', 'master')
+ ensure_file(master_path)
+ self.assertEqual(master_path, net.get_master('enP1s1'))
+
+ def test_master_is_bridge_or_bond(self):
+ bridge_mac = 'aa:bb:cc:aa:bb:cc'
+ bond_mac = 'cc:bb:aa:cc:bb:aa'
+
+ # No master => False
+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), bridge_mac)
+ write_file(os.path.join(self.sysdir, 'eth2', 'address'), bond_mac)
+
+ self.assertFalse(net.master_is_bridge_or_bond('eth1'))
+ self.assertFalse(net.master_is_bridge_or_bond('eth2'))
+
+ # masters without bridge/bonding => False
+ write_file(os.path.join(self.sysdir, 'br0', 'address'), bridge_mac)
+ write_file(os.path.join(self.sysdir, 'bond0', 'address'), bond_mac)
+
+ os.symlink('../br0', os.path.join(self.sysdir, 'eth1', 'master'))
+ os.symlink('../bond0', os.path.join(self.sysdir, 'eth2', 'master'))
+
+ self.assertFalse(net.master_is_bridge_or_bond('eth1'))
+ self.assertFalse(net.master_is_bridge_or_bond('eth2'))
+
+ # masters with bridge/bonding => True
+ write_file(os.path.join(self.sysdir, 'br0', 'bridge'), '')
+ write_file(os.path.join(self.sysdir, 'bond0', 'bonding'), '')
+
+ self.assertTrue(net.master_is_bridge_or_bond('eth1'))
+ self.assertTrue(net.master_is_bridge_or_bond('eth2'))
+
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'))
@@ -204,6 +239,10 @@ class TestGenerateFallbackConfig(CiTestCase):
self.add_patch('cloudinit.net.util.is_container', 'm_is_container',
return_value=False)
self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')
+ self.add_patch('cloudinit.net.is_netfailover', 'm_netfail',
+ return_value=False)
+ self.add_patch('cloudinit.net.is_netfail_master', 'm_netfail_master',
+ return_value=False)
def test_generate_fallback_finds_connected_eth_with_mac(self):
"""generate_fallback_config finds any connected device with a mac."""
@@ -212,9 +251,9 @@ class TestGenerateFallbackConfig(CiTestCase):
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}
+ 'ethernets': {'eth1': {'match': {'macaddress': mac},
+ 'dhcp4': True, 'set-name': 'eth1'}},
+ 'version': 2}
self.assertEqual(expected, net.generate_fallback_config())
def test_generate_fallback_finds_dormant_eth_with_mac(self):
@@ -223,9 +262,9 @@ class TestGenerateFallbackConfig(CiTestCase):
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}
+ 'ethernets': {'eth0': {'match': {'macaddress': mac}, 'dhcp4': True,
+ 'set-name': 'eth0'}},
+ 'version': 2}
self.assertEqual(expected, net.generate_fallback_config())
def test_generate_fallback_finds_eth_by_operstate(self):
@@ -233,9 +272,10 @@ class TestGenerateFallbackConfig(CiTestCase):
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}
+ 'ethernets': {
+ 'eth0': {'dhcp4': True, 'match': {'macaddress': mac},
+ 'set-name': 'eth0'}},
+ 'version': 2}
valid_operstates = ['dormant', 'down', 'lowerlayerdown', 'unknown']
for state in valid_operstates:
write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
@@ -267,6 +307,61 @@ class TestGenerateFallbackConfig(CiTestCase):
ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
self.assertIsNone(net.generate_fallback_config())
+ def test_generate_fallback_config_skips_netfail_devs(self):
+ """gen_fallback_config ignores netfail primary,sby no mac on master."""
+ mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
+ for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
+ write_file(os.path.join(self.sysdir, iface, 'carrier'), '1')
+ write_file(
+ os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
+ write_file(
+ os.path.join(self.sysdir, iface, 'address'), mac)
+
+ def is_netfail(iface, _driver=None):
+ # ens3 is the master
+ if iface == 'ens3':
+ return False
+ return True
+ self.m_netfail.side_effect = is_netfail
+
+ def is_netfail_master(iface, _driver=None):
+ # ens3 is the master
+ if iface == 'ens3':
+ return True
+ return False
+ self.m_netfail_master.side_effect = is_netfail_master
+ expected = {
+ 'ethernets': {
+ 'ens3': {'dhcp4': True, 'match': {'name': 'ens3'},
+ 'set-name': 'ens3'}},
+ 'version': 2}
+ result = net.generate_fallback_config()
+ self.assertEqual(expected, result)
+
+
+class TestNetFindFallBackNic(CiTestCase):
+
+ with_logs = True
+
+ def setUp(self):
+ super(TestNetFindFallBackNic, 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_first_connected_eth_with_mac(self):
+ """find_fallback_nic 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)
+ self.assertEqual('eth1', net.find_fallback_nic())
+
class TestGetDeviceList(CiTestCase):
@@ -364,6 +459,57 @@ class TestGetInterfaceMAC(CiTestCase):
expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
self.assertEqual(expected, net.get_interfaces())
+ def test_get_interfaces_by_mac_skips_master_devs(self):
+ """Ignore interfaces with a master device which would have dup mac."""
+ mac1 = mac2 = 'aa:bb:cc:aa:bb:cc'
+ write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac1)
+ write_file(os.path.join(self.sysdir, 'eth1', 'master'), "blah")
+ write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
+ write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac2)
+ expected = [('eth2', mac2, None, None)]
+ self.assertEqual(expected, net.get_interfaces())
+
+ @mock.patch('cloudinit.net.is_netfailover')
+ def test_get_interfaces_by_mac_skips_netfailvoer(self, m_netfail):
+ """Ignore interfaces if netfailover primary or standby."""
+ mac = 'aa:bb:cc:aa:bb:cc' # netfailover devs share the same mac
+ for iface in ['ens3', 'ens3sby', 'enP0s1f3']:
+ write_file(
+ os.path.join(self.sysdir, iface, 'addr_assign_type'), '0')
+ write_file(
+ os.path.join(self.sysdir, iface, 'address'), mac)
+
+ def is_netfail(iface, _driver=None):
+ # ens3 is the master
+ if iface == 'ens3':
+ return False
+ else:
+ return True
+ m_netfail.side_effect = is_netfail
+ expected = [('ens3', mac, None, None)]
+ self.assertEqual(expected, net.get_interfaces())
+
+ def test_get_interfaces_does_not_skip_phys_members_of_bridges_and_bonds(
+ self
+ ):
+ bridge_mac = 'aa:bb:cc:aa:bb:cc'
+ bond_mac = 'cc:bb:aa:cc:bb:aa'
+ write_file(os.path.join(self.sysdir, 'br0', 'address'), bridge_mac)
+ write_file(os.path.join(self.sysdir, 'br0', 'bridge'), '')
+
+ write_file(os.path.join(self.sysdir, 'bond0', 'address'), bond_mac)
+ write_file(os.path.join(self.sysdir, 'bond0', 'bonding'), '')
+
+ write_file(os.path.join(self.sysdir, 'eth1', 'address'), bridge_mac)
+ os.symlink('../br0', os.path.join(self.sysdir, 'eth1', 'master'))
+
+ write_file(os.path.join(self.sysdir, 'eth2', 'address'), bond_mac)
+ os.symlink('../bond0', os.path.join(self.sysdir, 'eth2', 'master'))
+
+ interface_names = [interface[0] for interface in net.get_interfaces()]
+ self.assertEqual(['eth1', 'eth2'], sorted(interface_names))
+
class TestInterfaceHasOwnMAC(CiTestCase):
@@ -549,6 +695,45 @@ class TestEphemeralIPV4Network(CiTestCase):
self.assertEqual(expected_setup_calls, m_subp.call_args_list)
m_subp.assert_has_calls(expected_teardown_calls)
+ def test_ephemeral_ipv4_network_with_rfc3442_static_routes(self, m_subp):
+ params = {
+ 'interface': 'eth0', 'ip': '192.168.2.2',
+ 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
+ 'static_routes': [('169.254.169.254/32', '192.168.2.1'),
+ ('0.0.0.0/0', '192.168.2.1')],
+ 'router': '192.168.2.1'}
+ 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', '-4', 'route', 'add', '169.254.169.254/32',
+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
+ mock.call(
+ ['ip', '-4', 'route', 'add', '0.0.0.0/0',
+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True)]
+ expected_teardown_calls = [
+ mock.call(
+ ['ip', '-4', 'route', 'del', '0.0.0.0/0',
+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
+ mock.call(
+ ['ip', '-4', 'route', 'del', '169.254.169.254/32',
+ 'via', '192.168.2.1', 'dev', 'eth0'], capture=True),
+ 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)
+ ]
+ with net.EphemeralIPv4Network(**params):
+ self.assertEqual(expected_setup_calls, m_subp.call_args_list)
+ m_subp.assert_has_calls(expected_setup_calls + expected_teardown_calls)
+
class TestApplyNetworkCfgNames(CiTestCase):
V1_CONFIG = textwrap.dedent("""\
@@ -669,3 +854,447 @@ class TestHasURLConnectivity(HttprettyTestCase):
httpretty.register_uri(httpretty.GET, self.url, body={}, status=404)
self.assertFalse(
net.has_url_connectivity(self.url), 'Expected False on url fail')
+
+
+def _mk_v1_phys(mac, name, driver, device_id):
+ v1_cfg = {'type': 'physical', 'name': name, 'mac_address': mac}
+ params = {}
+ if driver:
+ params.update({'driver': driver})
+ if device_id:
+ params.update({'device_id': device_id})
+
+ if params:
+ v1_cfg.update({'params': params})
+
+ return v1_cfg
+
+
+def _mk_v2_phys(mac, name, driver=None, device_id=None):
+ v2_cfg = {'set-name': name, 'match': {'macaddress': mac}}
+ if driver:
+ v2_cfg['match'].update({'driver': driver})
+ if device_id:
+ v2_cfg['match'].update({'device_id': device_id})
+
+ return v2_cfg
+
+
+class TestExtractPhysdevs(CiTestCase):
+
+ def setUp(self):
+ super(TestExtractPhysdevs, self).setUp()
+ self.add_patch('cloudinit.net.device_driver', 'm_driver')
+ self.add_patch('cloudinit.net.device_devid', 'm_devid')
+
+ def test_extract_physdevs_looks_up_driver_v1(self):
+ driver = 'virtio'
+ self.m_driver.return_value = driver
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', None, '0x1000'],
+ ]
+ netcfg = {
+ 'version': 1,
+ 'config': [_mk_v1_phys(*args) for args in physdevs],
+ }
+ # insert the driver value for verification
+ physdevs[0][2] = driver
+ self.assertEqual(sorted(physdevs),
+ sorted(net.extract_physdevs(netcfg)))
+ self.m_driver.assert_called_with('eth0')
+
+ def test_extract_physdevs_looks_up_driver_v2(self):
+ driver = 'virtio'
+ self.m_driver.return_value = driver
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', None, '0x1000'],
+ ]
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {args[1]: _mk_v2_phys(*args) for args in physdevs},
+ }
+ # insert the driver value for verification
+ physdevs[0][2] = driver
+ self.assertEqual(sorted(physdevs),
+ sorted(net.extract_physdevs(netcfg)))
+ self.m_driver.assert_called_with('eth0')
+
+ def test_extract_physdevs_looks_up_devid_v1(self):
+ devid = '0x1000'
+ self.m_devid.return_value = devid
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', None],
+ ]
+ netcfg = {
+ 'version': 1,
+ 'config': [_mk_v1_phys(*args) for args in physdevs],
+ }
+ # insert the driver value for verification
+ physdevs[0][3] = devid
+ self.assertEqual(sorted(physdevs),
+ sorted(net.extract_physdevs(netcfg)))
+ self.m_devid.assert_called_with('eth0')
+
+ def test_extract_physdevs_looks_up_devid_v2(self):
+ devid = '0x1000'
+ self.m_devid.return_value = devid
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', None],
+ ]
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {args[1]: _mk_v2_phys(*args) for args in physdevs},
+ }
+ # insert the driver value for verification
+ physdevs[0][3] = devid
+ self.assertEqual(sorted(physdevs),
+ sorted(net.extract_physdevs(netcfg)))
+ self.m_devid.assert_called_with('eth0')
+
+ def test_get_v1_type_physical(self):
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
+ ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
+ ['09:87:65:43:21:10', 'ens0p1', 'mlx4_core', '0:0:1000'],
+ ]
+ netcfg = {
+ 'version': 1,
+ 'config': [_mk_v1_phys(*args) for args in physdevs],
+ }
+ self.assertEqual(sorted(physdevs),
+ sorted(net.extract_physdevs(netcfg)))
+
+ def test_get_v2_type_physical(self):
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
+ ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
+ ['09:87:65:43:21:10', 'ens0p1', 'mlx4_core', '0:0:1000'],
+ ]
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {args[1]: _mk_v2_phys(*args) for args in physdevs},
+ }
+ self.assertEqual(sorted(physdevs),
+ sorted(net.extract_physdevs(netcfg)))
+
+ def test_get_v2_type_physical_skips_if_no_set_name(self):
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {
+ 'ens3': {
+ 'match': {'macaddress': '00:11:22:33:44:55'},
+ }
+ }
+ }
+ self.assertEqual([], net.extract_physdevs(netcfg))
+
+ def test_runtime_error_on_unknown_netcfg_version(self):
+ with self.assertRaises(RuntimeError):
+ net.extract_physdevs({'version': 3, 'awesome_config': []})
+
+
+class TestWaitForPhysdevs(CiTestCase):
+
+ with_logs = True
+
+ def setUp(self):
+ super(TestWaitForPhysdevs, self).setUp()
+ self.add_patch('cloudinit.net.get_interfaces_by_mac',
+ 'm_get_iface_mac')
+ self.add_patch('cloudinit.util.udevadm_settle', 'm_udev_settle')
+
+ def test_wait_for_physdevs_skips_settle_if_all_present(self):
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
+ ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
+ ]
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {args[1]: _mk_v2_phys(*args)
+ for args in physdevs},
+ }
+ self.m_get_iface_mac.side_effect = iter([
+ {'aa:bb:cc:dd:ee:ff': 'eth0',
+ '00:11:22:33:44:55': 'ens3'},
+ ])
+ net.wait_for_physdevs(netcfg)
+ self.assertEqual(0, self.m_udev_settle.call_count)
+
+ def test_wait_for_physdevs_calls_udev_settle_on_missing(self):
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
+ ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
+ ]
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {args[1]: _mk_v2_phys(*args)
+ for args in physdevs},
+ }
+ self.m_get_iface_mac.side_effect = iter([
+ {'aa:bb:cc:dd:ee:ff': 'eth0'}, # first call ens3 is missing
+ {'aa:bb:cc:dd:ee:ff': 'eth0',
+ '00:11:22:33:44:55': 'ens3'}, # second call has both
+ ])
+ net.wait_for_physdevs(netcfg)
+ self.m_udev_settle.assert_called_with(exists=net.sys_dev_path('ens3'))
+
+ def test_wait_for_physdevs_raise_runtime_error_if_missing_and_strict(self):
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
+ ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
+ ]
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {args[1]: _mk_v2_phys(*args)
+ for args in physdevs},
+ }
+ self.m_get_iface_mac.return_value = {}
+ with self.assertRaises(RuntimeError):
+ net.wait_for_physdevs(netcfg)
+
+ self.assertEqual(5 * len(physdevs), self.m_udev_settle.call_count)
+
+ def test_wait_for_physdevs_no_raise_if_not_strict(self):
+ physdevs = [
+ ['aa:bb:cc:dd:ee:ff', 'eth0', 'virtio', '0x1000'],
+ ['00:11:22:33:44:55', 'ens3', 'e1000', '0x1643'],
+ ]
+ netcfg = {
+ 'version': 2,
+ 'ethernets': {args[1]: _mk_v2_phys(*args)
+ for args in physdevs},
+ }
+ self.m_get_iface_mac.return_value = {}
+ net.wait_for_physdevs(netcfg, strict=False)
+ self.assertEqual(5 * len(physdevs), self.m_udev_settle.call_count)
+
+
+class TestNetFailOver(CiTestCase):
+
+ with_logs = True
+
+ def setUp(self):
+ super(TestNetFailOver, self).setUp()
+ self.add_patch('cloudinit.net.util', 'm_util')
+ self.add_patch('cloudinit.net.read_sys_net', 'm_read_sys_net')
+ self.add_patch('cloudinit.net.device_driver', 'm_device_driver')
+
+ def test_get_dev_features(self):
+ devname = self.random_string()
+ features = self.random_string()
+ self.m_read_sys_net.return_value = features
+
+ self.assertEqual(features, net.get_dev_features(devname))
+ self.assertEqual(1, self.m_read_sys_net.call_count)
+ self.assertEqual(mock.call(devname, 'device/features'),
+ self.m_read_sys_net.call_args_list[0])
+
+ def test_get_dev_features_none_returns_empty_string(self):
+ devname = self.random_string()
+ self.m_read_sys_net.side_effect = Exception('error')
+ self.assertEqual('', net.get_dev_features(devname))
+ self.assertEqual(1, self.m_read_sys_net.call_count)
+ self.assertEqual(mock.call(devname, 'device/features'),
+ self.m_read_sys_net.call_args_list[0])
+
+ @mock.patch('cloudinit.net.get_dev_features')
+ def test_has_netfail_standby_feature(self, m_dev_features):
+ devname = self.random_string()
+ standby_features = ('0' * 62) + '1' + '0'
+ m_dev_features.return_value = standby_features
+ self.assertTrue(net.has_netfail_standby_feature(devname))
+
+ @mock.patch('cloudinit.net.get_dev_features')
+ def test_has_netfail_standby_feature_short_is_false(self, m_dev_features):
+ devname = self.random_string()
+ standby_features = self.random_string()
+ m_dev_features.return_value = standby_features
+ self.assertFalse(net.has_netfail_standby_feature(devname))
+
+ @mock.patch('cloudinit.net.get_dev_features')
+ def test_has_netfail_standby_feature_not_present_is_false(self,
+ m_dev_features):
+ devname = self.random_string()
+ standby_features = '0' * 64
+ m_dev_features.return_value = standby_features
+ self.assertFalse(net.has_netfail_standby_feature(devname))
+
+ @mock.patch('cloudinit.net.get_dev_features')
+ def test_has_netfail_standby_feature_no_features_is_false(self,
+ m_dev_features):
+ devname = self.random_string()
+ standby_features = None
+ m_dev_features.return_value = standby_features
+ self.assertFalse(net.has_netfail_standby_feature(devname))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_master(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ m_exists.return_value = False # no master sysfs attr
+ m_standby.return_value = True # has standby feature flag
+ self.assertTrue(net.is_netfail_master(devname, driver))
+
+ @mock.patch('cloudinit.net.sys_dev_path')
+ def test_is_netfail_master_checks_master_attr(self, m_sysdev):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ m_sysdev.return_value = self.random_string()
+ self.assertFalse(net.is_netfail_master(devname, driver))
+ self.assertEqual(1, m_sysdev.call_count)
+ self.assertEqual(mock.call(devname, path='master'),
+ m_sysdev.call_args_list[0])
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_master_wrong_driver(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = self.random_string()
+ self.assertFalse(net.is_netfail_master(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_master_has_master_attr(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ m_exists.return_value = True # has master sysfs attr
+ self.assertFalse(net.is_netfail_master(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_master_no_standby_feat(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ m_exists.return_value = False # no master sysfs attr
+ m_standby.return_value = False # no standby feature flag
+ self.assertFalse(net.is_netfail_master(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ @mock.patch('cloudinit.net.sys_dev_path')
+ def test_is_netfail_primary(self, m_sysdev, m_exists, m_standby):
+ devname = self.random_string()
+ driver = self.random_string() # device not virtio_net
+ master_devname = self.random_string()
+ m_sysdev.return_value = "%s/%s" % (self.random_string(),
+ master_devname)
+ m_exists.return_value = True # has master sysfs attr
+ self.m_device_driver.return_value = 'virtio_net' # master virtio_net
+ m_standby.return_value = True # has standby feature flag
+ self.assertTrue(net.is_netfail_primary(devname, driver))
+ self.assertEqual(1, self.m_device_driver.call_count)
+ self.assertEqual(mock.call(master_devname),
+ self.m_device_driver.call_args_list[0])
+ self.assertEqual(1, m_standby.call_count)
+ self.assertEqual(mock.call(master_devname),
+ m_standby.call_args_list[0])
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ @mock.patch('cloudinit.net.sys_dev_path')
+ def test_is_netfail_primary_wrong_driver(self, m_sysdev, m_exists,
+ m_standby):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ self.assertFalse(net.is_netfail_primary(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ @mock.patch('cloudinit.net.sys_dev_path')
+ def test_is_netfail_primary_no_master(self, m_sysdev, m_exists, m_standby):
+ devname = self.random_string()
+ driver = self.random_string() # device not virtio_net
+ m_exists.return_value = False # no master sysfs attr
+ self.assertFalse(net.is_netfail_primary(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ @mock.patch('cloudinit.net.sys_dev_path')
+ def test_is_netfail_primary_bad_master(self, m_sysdev, m_exists,
+ m_standby):
+ devname = self.random_string()
+ driver = self.random_string() # device not virtio_net
+ master_devname = self.random_string()
+ m_sysdev.return_value = "%s/%s" % (self.random_string(),
+ master_devname)
+ m_exists.return_value = True # has master sysfs attr
+ self.m_device_driver.return_value = 'XXXX' # master not virtio_net
+ self.assertFalse(net.is_netfail_primary(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ @mock.patch('cloudinit.net.sys_dev_path')
+ def test_is_netfail_primary_no_standby(self, m_sysdev, m_exists,
+ m_standby):
+ devname = self.random_string()
+ driver = self.random_string() # device not virtio_net
+ master_devname = self.random_string()
+ m_sysdev.return_value = "%s/%s" % (self.random_string(),
+ master_devname)
+ m_exists.return_value = True # has master sysfs attr
+ self.m_device_driver.return_value = 'virtio_net' # master virtio_net
+ m_standby.return_value = False # master has no standby feature flag
+ self.assertFalse(net.is_netfail_primary(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_standby(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ m_exists.return_value = True # has master sysfs attr
+ m_standby.return_value = True # has standby feature flag
+ self.assertTrue(net.is_netfail_standby(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_standby_wrong_driver(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = self.random_string()
+ self.assertFalse(net.is_netfail_standby(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_standby_no_master(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ m_exists.return_value = False # has master sysfs attr
+ self.assertFalse(net.is_netfail_standby(devname, driver))
+
+ @mock.patch('cloudinit.net.has_netfail_standby_feature')
+ @mock.patch('cloudinit.net.os.path.exists')
+ def test_is_netfail_standby_no_standby_feature(self, m_exists, m_standby):
+ devname = self.random_string()
+ driver = 'virtio_net'
+ m_exists.return_value = True # has master sysfs attr
+ m_standby.return_value = False # has standby feature flag
+ self.assertFalse(net.is_netfail_standby(devname, driver))
+
+ @mock.patch('cloudinit.net.is_netfail_standby')
+ @mock.patch('cloudinit.net.is_netfail_primary')
+ def test_is_netfailover_primary(self, m_primary, m_standby):
+ devname = self.random_string()
+ driver = self.random_string()
+ m_primary.return_value = True
+ m_standby.return_value = False
+ self.assertTrue(net.is_netfailover(devname, driver))
+
+ @mock.patch('cloudinit.net.is_netfail_standby')
+ @mock.patch('cloudinit.net.is_netfail_primary')
+ def test_is_netfailover_standby(self, m_primary, m_standby):
+ devname = self.random_string()
+ driver = self.random_string()
+ m_primary.return_value = False
+ m_standby.return_value = True
+ self.assertTrue(net.is_netfailover(devname, driver))
+
+ @mock.patch('cloudinit.net.is_netfail_standby')
+ @mock.patch('cloudinit.net.is_netfail_primary')
+ def test_is_netfailover_returns_false(self, m_primary, m_standby):
+ devname = self.random_string()
+ driver = self.random_string()
+ m_primary.return_value = False
+ m_standby.return_value = False
+ self.assertFalse(net.is_netfailover(devname, driver))
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/tests/test_network_state.py b/cloudinit/net/tests/test_network_state.py
new file mode 100644
index 00000000..55880852
--- /dev/null
+++ b/cloudinit/net/tests/test_network_state.py
@@ -0,0 +1,48 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from unittest import mock
+
+from cloudinit.net import network_state
+from cloudinit.tests.helpers import CiTestCase
+
+netstate_path = 'cloudinit.net.network_state'
+
+
+class TestNetworkStateParseConfig(CiTestCase):
+
+ def setUp(self):
+ super(TestNetworkStateParseConfig, self).setUp()
+ nsi_path = netstate_path + '.NetworkStateInterpreter'
+ self.add_patch(nsi_path, 'm_nsi')
+
+ def test_missing_version_returns_none(self):
+ ncfg = {}
+ self.assertEqual(None, network_state.parse_net_config_data(ncfg))
+
+ def test_unknown_versions_returns_none(self):
+ ncfg = {'version': 13.2}
+ self.assertEqual(None, network_state.parse_net_config_data(ncfg))
+
+ def test_version_2_passes_self_as_config(self):
+ ncfg = {'version': 2, 'otherconfig': {}, 'somemore': [1, 2, 3]}
+ network_state.parse_net_config_data(ncfg)
+ self.assertEqual([mock.call(version=2, config=ncfg)],
+ self.m_nsi.call_args_list)
+
+ def test_valid_config_gets_network_state(self):
+ ncfg = {'version': 2, 'otherconfig': {}, 'somemore': [1, 2, 3]}
+ result = network_state.parse_net_config_data(ncfg)
+ self.assertNotEqual(None, result)
+
+ def test_empty_v1_config_gets_network_state(self):
+ ncfg = {'version': 1, 'config': []}
+ result = network_state.parse_net_config_data(ncfg)
+ self.assertNotEqual(None, result)
+
+ def test_empty_v2_config_gets_network_state(self):
+ ncfg = {'version': 2}
+ result = network_state.parse_net_config_data(ncfg)
+ self.assertNotEqual(None, result)
+
+
+# vi: ts=4 expandtab