diff options
author | zdc <zdc@users.noreply.github.com> | 2022-03-26 15:41:59 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-26 15:41:59 +0200 |
commit | aa60d48c2711cdcd9f88a4e5c77379adb0408231 (patch) | |
tree | 349631a02467dae0158f6f663cc8aa8537974a97 /cloudinit/net | |
parent | 5c4b3943343a85fbe517e5ec1fc670b3a8566b4b (diff) | |
parent | 31448cccedd8f841fb3ac7d0f2e3cdefe08a53ba (diff) | |
download | vyos-cloud-init-aa60d48c2711cdcd9f88a4e5c77379adb0408231.tar.gz vyos-cloud-init-aa60d48c2711cdcd9f88a4e5c77379adb0408231.zip |
Merge pull request #51 from zdc/T2117-sagitta-22.1
T2117: Cloud-init updated to 22.1
Diffstat (limited to 'cloudinit/net')
-rw-r--r-- | cloudinit/net/__init__.py | 689 | ||||
-rw-r--r-- | cloudinit/net/activators.py | 290 | ||||
-rw-r--r-- | cloudinit/net/bsd.py | 125 | ||||
-rwxr-xr-x | cloudinit/net/cmdline.py | 101 | ||||
-rw-r--r-- | cloudinit/net/dhcp.py | 208 | ||||
-rw-r--r-- | cloudinit/net/eni.py | 452 | ||||
-rw-r--r-- | cloudinit/net/freebsd.py | 48 | ||||
-rw-r--r-- | cloudinit/net/netbsd.py | 27 | ||||
-rw-r--r-- | cloudinit/net/netplan.py | 317 | ||||
-rw-r--r-- | cloudinit/net/network_state.py | 880 | ||||
-rw-r--r-- | cloudinit/net/networkd.py | 280 | ||||
-rw-r--r-- | cloudinit/net/openbsd.py | 44 | ||||
-rw-r--r-- | cloudinit/net/renderer.py | 37 | ||||
-rw-r--r-- | cloudinit/net/renderers.py | 51 | ||||
-rw-r--r-- | cloudinit/net/sysconfig.py | 903 | ||||
-rw-r--r-- | cloudinit/net/tests/__init__.py | 0 | ||||
-rw-r--r-- | cloudinit/net/tests/test_dhcp.py | 634 | ||||
-rw-r--r-- | cloudinit/net/tests/test_init.py | 1270 | ||||
-rw-r--r-- | cloudinit/net/tests/test_network_state.py | 58 | ||||
-rw-r--r-- | cloudinit/net/udev.py | 23 |
20 files changed, 2845 insertions, 3592 deletions
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index de65e7af..3270e1f7 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -6,30 +6,46 @@ # This file is part of cloud-init. See LICENSE file for license information. import errno +import functools import ipaddress import logging import os import re +from typing import Any, Dict -from cloudinit import subp -from cloudinit import util -from cloudinit.net.network_state import mask_to_net_prefix +from cloudinit import subp, util +from cloudinit.net.network_state import ipv4_mask_to_net_prefix from cloudinit.url_helper import UrlError, readurl LOG = logging.getLogger(__name__) SYS_CLASS_NET = "/sys/class/net/" -DEFAULT_PRIMARY_INTERFACE = 'eth0' - - -def natural_sort_key(s, _nsre=re.compile('([0-9]+)')): +DEFAULT_PRIMARY_INTERFACE = "eth0" +OVS_INTERNAL_INTERFACE_LOOKUP_CMD = [ + "ovs-vsctl", + "--format", + "csv", + "--no-headings", + "--timeout", + "10", + "--columns", + "name", + "find", + "interface", + "type=internal", +] + + +def natural_sort_key(s, _nsre=re.compile("([0-9]+)")): """Sorting for Humans: natural sort order. Can be use as the key to sort functions. This will sort ['eth0', 'ens3', 'ens10', 'ens12', 'ens8', 'ens0'] as ['ens0', 'ens3', 'ens8', 'ens10', 'ens12', 'eth0'] instead of the simple python way which will produce ['ens0', 'ens10', 'ens12', 'ens3', 'ens8', 'eth0'].""" - return [int(text) if text.isdigit() else text.lower() - for text in re.split(_nsre, s)] + return [ + int(text) if text.isdigit() else text.lower() + for text in re.split(_nsre, s) + ] def get_sys_class_path(): @@ -41,14 +57,19 @@ def sys_dev_path(devname, path=""): return get_sys_class_path() + devname + "/" + path -def read_sys_net(devname, path, translate=None, - on_enoent=None, on_keyerror=None, - on_einval=None): +def read_sys_net( + devname, + path, + translate=None, + on_enoent=None, + on_keyerror=None, + on_einval=None, +): dev_path = sys_dev_path(devname, path) try: contents = util.load_file(dev_path) except (OSError, IOError) as e: - e_errno = getattr(e, 'errno', None) + e_errno = getattr(e, "errno", None) if e_errno in (errno.ENOENT, errno.ENOTDIR): if on_enoent is not None: return on_enoent(e) @@ -65,19 +86,26 @@ def read_sys_net(devname, path, translate=None, if on_keyerror is not None: return on_keyerror(e) else: - LOG.debug("Found unexpected (not translatable) value" - " '%s' in '%s", contents, dev_path) + LOG.debug( + "Found unexpected (not translatable) value '%s' in '%s", + contents, + dev_path, + ) raise def read_sys_net_safe(iface, field, translate=None): def on_excp_false(e): return False - return read_sys_net(iface, field, - on_keyerror=on_excp_false, - on_enoent=on_excp_false, - on_einval=on_excp_false, - translate=translate) + + return read_sys_net( + iface, + field, + on_keyerror=on_excp_false, + on_enoent=on_excp_false, + on_einval=on_excp_false, + translate=translate, + ) def read_sys_net_int(iface, field): @@ -94,7 +122,7 @@ def is_up(devname): # The linux kernel says to consider devices in 'unknown' # operstate as up for the purposes of network configuration. See # Documentation/networking/operstates.txt in the kernel source. - translate = {'up': True, 'unknown': True, 'down': False} + translate = {"up": True, "unknown": True, "down": False} return read_sys_net_safe(devname, "operstate", translate=translate) @@ -121,7 +149,7 @@ def master_is_bridge_or_bond(devname): 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)) + return os.path.exists(bonding_path) or os.path.exists(bridge_path) def master_is_openvswitch(devname): @@ -133,24 +161,71 @@ def master_is_openvswitch(devname): return os.path.exists(ovs_path) +@functools.lru_cache(maxsize=None) +def openvswitch_is_installed() -> bool: + """Return a bool indicating if Open vSwitch is installed in the system.""" + ret = bool(subp.which("ovs-vsctl")) + if not ret: + LOG.debug( + "ovs-vsctl not in PATH; not detecting Open vSwitch interfaces" + ) + return ret + + +@functools.lru_cache(maxsize=None) +def get_ovs_internal_interfaces() -> list: + """Return a list of the names of OVS internal interfaces on the system. + + These will all be strings, and are used to exclude OVS-specific interface + from cloud-init's network configuration handling. + """ + try: + out, _err = subp.subp(OVS_INTERNAL_INTERFACE_LOOKUP_CMD) + except subp.ProcessExecutionError as exc: + if "database connection failed" in exc.stderr: + LOG.info( + "Open vSwitch is not yet up; no interfaces will be detected as" + " OVS-internal" + ) + return [] + raise + else: + return out.splitlines() + + +def is_openvswitch_internal_interface(devname: str) -> bool: + """Returns True if this is an OVS internal interface. + + If OVS is not installed or not yet running, this will return False. + """ + if not openvswitch_is_installed(): + return False + ovs_bridges = get_ovs_internal_interfaces() + if devname in ovs_bridges: + LOG.debug("Detected %s as an OVS interface", devname) + return True + return False + + 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. + """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): + 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 = '' + """Returns a str from reading /sys/class/net/<devname>/device/features.""" + features = "" try: - features = read_sys_net(devname, 'device/features') + features = read_sys_net(devname, "device/features") except Exception: pass return features @@ -170,13 +245,13 @@ def has_netfail_standby_feature(devname): def is_netfail_master(devname, driver=None): - """ A device is a "netfail master" device if: + """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 + - 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. + Return True if all of the above is True. """ if get_master(devname) is not None: return False @@ -194,17 +269,17 @@ def is_netfail_master(devname, driver=None): def is_netfail_primary(devname, driver=None): - """ A device is a "netfail primary" device if: + """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 + - 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. + 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') + master_sysfs_path = sys_dev_path(devname, path="master") if not os.path.exists(master_sysfs_path): return False @@ -227,13 +302,13 @@ def is_netfail_primary(devname, driver=None): def is_netfail_standby(devname, driver=None): - """ A device is a "netfail standby" device if: + """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 + - 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. + Return True if all of the above is True. """ if get_master(devname) is None: return False @@ -253,21 +328,21 @@ def is_netfail_standby(devname, driver=None): def is_renamed(devname): """ /* interface name assignment types (sysfs name_assign_type attribute) */ - #define NET_NAME_UNKNOWN 0 /* unknown origin (not exposed to user) */ - #define NET_NAME_ENUM 1 /* enumerated by kernel */ - #define NET_NAME_PREDICTABLE 2 /* predictably named by the kernel */ - #define NET_NAME_USER 3 /* provided by user-space */ - #define NET_NAME_RENAMED 4 /* renamed by user-space */ + #define NET_NAME_UNKNOWN 0 /* unknown origin (not exposed to user) */ + #define NET_NAME_ENUM 1 /* enumerated by kernel */ + #define NET_NAME_PREDICTABLE 2 /* predictably named by the kernel */ + #define NET_NAME_USER 3 /* provided by user-space */ + #define NET_NAME_RENAMED 4 /* renamed by user-space */ """ - name_assign_type = read_sys_net_safe(devname, 'name_assign_type') - if name_assign_type and name_assign_type in ['3', '4']: + name_assign_type = read_sys_net_safe(devname, "name_assign_type") + if name_assign_type and name_assign_type in ["3", "4"]: return True return False def is_vlan(devname): uevent = str(read_sys_net_safe(devname, "uevent")) - return 'DEVTYPE=vlan' in uevent.splitlines() + return "DEVTYPE=vlan" in uevent.splitlines() def device_driver(devname): @@ -291,7 +366,7 @@ def device_devid(devname): def get_devicelist(): - if util.is_FreeBSD(): + if util.is_FreeBSD() or util.is_DragonFlyBSD(): return list(get_interfaces_by_mac().values()) try: @@ -311,12 +386,12 @@ class ParserError(Exception): def is_disabled_cfg(cfg): if not cfg or not isinstance(cfg, dict): return False - return cfg.get('config') == "disabled" + return cfg.get("config") == "disabled" def find_fallback_nic(blacklist_drivers=None): """Return the name of the 'fallback' network device.""" - if util.is_FreeBSD(): + if util.is_FreeBSD() or util.is_DragonFlyBSD(): return find_fallback_nic_on_freebsd(blacklist_drivers) elif util.is_NetBSD() or util.is_OpenBSD(): return find_fallback_nic_on_netbsd_or_openbsd(blacklist_drivers) @@ -325,9 +400,9 @@ def find_fallback_nic(blacklist_drivers=None): def find_fallback_nic_on_netbsd_or_openbsd(blacklist_drivers=None): - values = list(sorted( - get_interfaces_by_mac().values(), - key=natural_sort_key)) + values = list( + sorted(get_interfaces_by_mac().values(), key=natural_sort_key) + ) if values: return values[0] @@ -341,7 +416,7 @@ def find_fallback_nic_on_freebsd(blacklist_drivers=None): we'll use the first interface from ``ifconfig -l -u ether`` """ - stdout, _stderr = subp.subp(['ifconfig', '-l', '-u', 'ether']) + stdout, _stderr = subp.subp(["ifconfig", "-l", "-u", "ether"]) values = stdout.split() if values: return values[0] @@ -358,22 +433,31 @@ def find_fallback_nic_on_linux(blacklist_drivers=None): if not blacklist_drivers: blacklist_drivers = [] - if 'net.ifnames=0' in util.get_cmdline(): - LOG.debug('Stable ifnames disabled by net.ifnames=0 in /proc/cmdline') + if "net.ifnames=0" in util.get_cmdline(): + LOG.debug("Stable ifnames disabled by net.ifnames=0 in /proc/cmdline") else: - unstable = [device for device in get_devicelist() - if device != 'lo' and not is_renamed(device)] + unstable = [ + device + for device in get_devicelist() + if device != "lo" and not is_renamed(device) + ] if len(unstable): - LOG.debug('Found unstable nic names: %s; calling udevadm settle', - unstable) - msg = 'Waiting for udev events to settle' + LOG.debug( + "Found unstable nic names: %s; calling udevadm settle", + unstable, + ) + msg = "Waiting for udev events to settle" util.log_time(LOG.debug, msg, func=util.udevadm_settle) # get list of interfaces that could have connections - invalid_interfaces = set(['lo']) - potential_interfaces = set([device for device in get_devicelist() - if device_driver(device) not in - blacklist_drivers]) + invalid_interfaces = set(["lo"]) + potential_interfaces = set( + [ + device + for device in get_devicelist() + if device_driver(device) not in blacklist_drivers + ] + ) potential_interfaces = potential_interfaces.difference(invalid_interfaces) # sort into interfaces with carrier, interfaces which could have carrier, # and ignore interfaces that are definitely disconnected @@ -391,19 +475,19 @@ def find_fallback_nic_on_linux(blacklist_drivers=None): if is_netfailover(interface): # ignore netfailover primary/standby interfaces continue - carrier = read_sys_net_int(interface, 'carrier') + carrier = read_sys_net_int(interface, "carrier") if carrier: connected.append(interface) continue # check if nic is dormant or down, as this may make a nick appear to # not have a carrier even though it could acquire one when brought # online by dhclient - dormant = read_sys_net_int(interface, 'dormant') + dormant = read_sys_net_int(interface, "dormant") if dormant: possibly_connected.append(interface) continue - operstate = read_sys_net_safe(interface, 'operstate') - if operstate in ['dormant', 'down', 'lowerlayerdown', 'unknown']: + operstate = read_sys_net_safe(interface, "operstate") + if operstate in ["dormant", "down", "lowerlayerdown", "unknown"]: possibly_connected.append(interface) continue @@ -423,7 +507,7 @@ def find_fallback_nic_on_linux(blacklist_drivers=None): # pick the first that has a mac-address for name in names: - if read_sys_net_safe(name, 'address'): + if read_sys_net_safe(name, "address"): return name return None @@ -440,32 +524,32 @@ def generate_fallback_config(blacklist_drivers=None, config_driver=None): # netfail cannot use mac for matching, they have duplicate macs if is_netfail_master(target_name): - match = {'name': 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} + "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} + cfg["match"]["driver"] = driver + nconf = {"ethernets": {target_name: cfg}, "version": 2} return nconf def extract_physdevs(netcfg): - def _version_1(netcfg): physdevs = [] - for ent in netcfg.get('config', {}): - if ent.get('type') != 'physical': + for ent in netcfg.get("config", {}): + if ent.get("type") != "physical": continue - mac = ent.get('mac_address') + mac = ent.get("mac_address") if not mac: continue - name = ent.get('name') - driver = ent.get('params', {}).get('driver') - device_id = ent.get('params', {}).get('device_id') + name = ent.get("name") + driver = ent.get("params", {}).get("driver") + device_id = ent.get("params", {}).get("device_id") if not driver: driver = device_driver(name) if not device_id: @@ -475,17 +559,17 @@ def extract_physdevs(netcfg): def _version_2(netcfg): physdevs = [] - for ent in netcfg.get('ethernets', {}).values(): + for ent in netcfg.get("ethernets", {}).values(): # only rename if configured to do so - name = ent.get('set-name') + name = ent.get("set-name") if not name: continue # cloud-init requires macaddress for renaming - mac = ent.get('match', {}).get('macaddress') + mac = ent.get("match", {}).get("macaddress") if not mac: continue - driver = ent.get('match', {}).get('driver') - device_id = ent.get('match', {}).get('device_id') + driver = ent.get("match", {}).get("driver") + device_id = ent.get("match", {}).get("device_id") if not driver: driver = device_driver(name) if not device_id: @@ -493,13 +577,13 @@ def extract_physdevs(netcfg): physdevs.append([mac, name, driver, device_id]) return physdevs - version = netcfg.get('version') + 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) + raise RuntimeError("Unknown network config version: %s" % version) def apply_network_config_names(netcfg, strict_present=True, strict_busy=True): @@ -516,7 +600,7 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True): _rename_interfaces(extract_physdevs(netcfg)) except RuntimeError as e: raise RuntimeError( - 'Failed to apply network config names: %s' % e + "Failed to apply network config names: %s" % e ) from e @@ -558,33 +642,37 @@ def _get_current_rename_info(check_downable=True): cur_info = {} for (name, mac, driver, device_id) in get_interfaces(): cur_info[name] = { - 'downable': None, - 'device_id': device_id, - 'driver': driver, - 'mac': mac.lower(), - 'name': name, - 'up': is_up(name), + "downable": None, + "device_id": device_id, + "driver": driver, + "mac": mac.lower(), + "name": name, + "up": is_up(name), } if check_downable: nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]") - ipv6, _err = subp.subp(['ip', '-6', 'addr', 'show', 'permanent', - 'scope', 'global'], capture=True) - ipv4, _err = subp.subp(['ip', '-4', 'addr', 'show'], capture=True) + ipv6, _err = subp.subp( + ["ip", "-6", "addr", "show", "permanent", "scope", "global"], + capture=True, + ) + ipv4, _err = subp.subp(["ip", "-4", "addr", "show"], capture=True) nics_with_addresses = set() for bytes_out in (ipv6, ipv4): nics_with_addresses.update(nmatch.findall(bytes_out)) for d in cur_info.values(): - d['downable'] = (d['up'] is False or - d['name'] not in nics_with_addresses) + d["downable"] = ( + d["up"] is False or d["name"] not in nics_with_addresses + ) return cur_info -def _rename_interfaces(renames, strict_present=True, strict_busy=True, - current_info=None): +def _rename_interfaces( + renames, strict_present=True, strict_busy=True, current_info=None +): if not len(renames): LOG.debug("no interfaces to rename") @@ -596,14 +684,15 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, cur_info = {} for name, data in current_info.items(): cur = data.copy() - if cur.get('mac'): - cur['mac'] = cur['mac'].lower() - cur['name'] = name + if cur.get("mac"): + cur["mac"] = cur["mac"].lower() + cur["name"] = name cur_info[name] = cur + LOG.debug("Detected interfaces %s", cur_info) + def update_byname(bymac): - return dict((data['name'], data) - for data in cur_info.values()) + return dict((data["name"], data) for data in cur_info.values()) def rename(cur, new): subp.subp(["ip", "link", "set", cur, "name", new], capture=True) @@ -624,25 +713,31 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, def entry_match(data, mac, driver, device_id): """match if set and in data""" if mac and driver and device_id: - return (data['mac'] == mac and - data['driver'] == driver and - data['device_id'] == device_id) + return ( + data["mac"] == mac + and data["driver"] == driver + and data["device_id"] == device_id + ) elif mac and driver: - return (data['mac'] == mac and - data['driver'] == driver) + return data["mac"] == mac and data["driver"] == driver elif mac: - return (data['mac'] == mac) + return data["mac"] == mac return False def find_entry(mac, driver, device_id): - match = [data for data in cur_info.values() - if entry_match(data, mac, driver, device_id)] + match = [ + data + for data in cur_info.values() + if entry_match(data, mac, driver, device_id) + ] if len(match): if len(match) > 1: - msg = ('Failed to match a single device. Matched devices "%s"' - ' with search values "(mac:%s driver:%s device_id:%s)"' - % (match, mac, driver, device_id)) + msg = ( + 'Failed to match a single device. Matched devices "%s"' + ' with search values "(mac:%s driver:%s device_id:%s)"' + % (match, mac, driver, device_id) + ) raise ValueError(msg) return match[0] @@ -657,10 +752,11 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, if strict_present: errors.append( "[nic not present] Cannot rename mac=%s to %s" - ", not available." % (mac, new_name)) + ", not available." % (mac, new_name) + ) continue - cur_name = cur.get('name') + cur_name = cur.get("name") if cur_name == new_name: # nothing to do continue @@ -669,24 +765,25 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, if strict_present: errors.append( "[nic not present] Cannot rename mac=%s to %s" - ", not available." % (mac, new_name)) + ", not available." % (mac, new_name) + ) continue - if cur['up']: + if cur["up"]: msg = "[busy] Error renaming mac=%s from %s to %s" - if not cur['downable']: + if not cur["downable"]: if strict_busy: errors.append(msg % (mac, cur_name, new_name)) continue - cur['up'] = False + cur["up"] = False cur_ops.append(("down", mac, new_name, (cur_name,))) ups.append(("up", mac, new_name, (new_name,))) if new_name in cur_byname: target = cur_byname[new_name] - if target['up']: + if target["up"]: msg = "[busy-target] Error renaming mac=%s from %s to %s." - if not target['downable']: + if not target["downable"]: if strict_busy: errors.append(msg % (mac, cur_name, new_name)) continue @@ -699,17 +796,17 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, tmp_name = tmpname_fmt % tmpi cur_ops.append(("rename", mac, new_name, (new_name, tmp_name))) - target['name'] = tmp_name + target["name"] = tmp_name cur_byname = update_byname(cur_info) - if target['up']: + if target["up"]: ups.append(("up", mac, new_name, (tmp_name,))) - cur_ops.append(("rename", mac, new_name, (cur['name'], new_name))) - cur['name'] = new_name + cur_ops.append(("rename", mac, new_name, (cur["name"], new_name))) + cur["name"] = new_name cur_byname = update_byname(cur_info) ops += cur_ops - opmap = {'rename': rename, 'down': down, 'up': up} + opmap = {"rename": rename, "down": down, "up": up} if len(ops) + len(ups) == 0: if len(errors): @@ -724,11 +821,12 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, opmap.get(op)(*params) except Exception as e: errors.append( - "[unknown] Error performing %s%s for %s, %s: %s" % - (op, params, mac, new_name, e)) + "[unknown] Error performing %s%s for %s, %s: %s" + % (op, params, mac, new_name, e) + ) if len(errors): - raise Exception('\n'.join(errors)) + raise Exception("\n".join(errors)) def get_interface_mac(ifname): @@ -747,7 +845,7 @@ def get_ib_interface_hwaddr(ifname, ethernet_format): representation of the address will be returned. """ # Type 32 is Infiniband. - if read_sys_net_safe(ifname, 'type') == '32': + if read_sys_net_safe(ifname, "type") == "32": mac = get_interface_mac(ifname) if mac and ethernet_format: # Use bytes 13-15 and 18-20 of the hardware address. @@ -756,28 +854,32 @@ def get_ib_interface_hwaddr(ifname, ethernet_format): def get_interfaces_by_mac(blacklist_drivers=None) -> dict: - if util.is_FreeBSD(): + if util.is_FreeBSD() or util.is_DragonFlyBSD(): return get_interfaces_by_mac_on_freebsd( - blacklist_drivers=blacklist_drivers) + blacklist_drivers=blacklist_drivers + ) elif util.is_NetBSD(): return get_interfaces_by_mac_on_netbsd( - blacklist_drivers=blacklist_drivers) + blacklist_drivers=blacklist_drivers + ) elif util.is_OpenBSD(): return get_interfaces_by_mac_on_openbsd( - blacklist_drivers=blacklist_drivers) + blacklist_drivers=blacklist_drivers + ) else: return get_interfaces_by_mac_on_linux( - blacklist_drivers=blacklist_drivers) + blacklist_drivers=blacklist_drivers + ) def get_interfaces_by_mac_on_freebsd(blacklist_drivers=None) -> dict(): - (out, _) = subp.subp(['ifconfig', '-a', 'ether']) + (out, _) = subp.subp(["ifconfig", "-a", "ether"]) # flatten each interface block in a single line def flatten(out): - curr_block = '' - for line in out.split('\n'): - if line.startswith('\t'): + curr_block = "" + for line in out.split("\n"): + if line.startswith("\t"): curr_block += line else: if curr_block: @@ -789,10 +891,11 @@ def get_interfaces_by_mac_on_freebsd(blacklist_drivers=None) -> dict(): 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) + r"^(?P<ifname>\S*): .*ether\s(?P<mac>[\da-f:]{17}).*", block + ) if m: - yield (m.group('mac'), m.group('ifname')) + yield (m.group("mac"), m.group("ifname")) + results = {mac: ifname for mac, ifname in find_mac(flatten(out))} return results @@ -803,13 +906,13 @@ def get_interfaces_by_mac_on_netbsd(blacklist_drivers=None) -> dict(): r"(?P<ifname>\w+).*address:\s" r"(?P<mac>([\da-f]{2}[:-]){5}([\da-f]{2})).*" ) - (out, _) = subp.subp(['ifconfig', '-a']) - if_lines = re.sub(r'\n\s+', ' ', out).splitlines() + (out, _) = subp.subp(["ifconfig", "-a"]) + if_lines = re.sub(r"\n\s+", " ", out).splitlines() for line in if_lines: m = re.match(re_field_match, line) if m: fields = m.groupdict() - ret[fields['mac']] = fields['ifname'] + ret[fields["mac"]] = fields["ifname"] return ret @@ -817,14 +920,15 @@ def get_interfaces_by_mac_on_openbsd(blacklist_drivers=None) -> dict(): ret = {} re_field_match = ( r"(?P<ifname>\w+).*lladdr\s" - r"(?P<mac>([\da-f]{2}[:-]){5}([\da-f]{2})).*") - (out, _) = subp.subp(['ifconfig', '-a']) - if_lines = re.sub(r'\n\s+', ' ', out).splitlines() + r"(?P<mac>([\da-f]{2}[:-]){5}([\da-f]{2})).*" + ) + (out, _) = subp.subp(["ifconfig", "-a"]) + if_lines = re.sub(r"\n\s+", " ", out).splitlines() for line in if_lines: m = re.match(re_field_match, line) if m: fields = m.groupdict() - ret[fields['mac']] = fields['ifname'] + ret[fields["mac"]] = fields["ifname"] return ret @@ -834,11 +938,13 @@ def get_interfaces_by_mac_on_linux(blacklist_drivers=None) -> dict: Bridges and any devices that have a 'stolen' mac are excluded.""" ret = {} for name, mac, _driver, _devid in get_interfaces( - blacklist_drivers=blacklist_drivers): + blacklist_drivers=blacklist_drivers + ): if mac in ret: raise RuntimeError( - "duplicate mac found! both '%s' and '%s' have mac '%s'" % - (name, ret[mac], mac)) + "duplicate mac found! both '%s' and '%s' have mac '%s'" + % (name, ret[mac], mac) + ) ret[mac] = name # Try to get an Infiniband hardware address (in 6 byte Ethernet format) # for the interface. @@ -846,8 +952,9 @@ def get_interfaces_by_mac_on_linux(blacklist_drivers=None) -> dict: if ib_mac: if ib_mac in ret: raise RuntimeError( - "duplicate mac found! both '%s' and '%s' have mac '%s'" % - (name, ret[ib_mac], ib_mac)) + "duplicate mac found! both '%s' and '%s' have mac '%s'" + % (name, ret[ib_mac], ib_mac) + ) ret[ib_mac] = name return ret @@ -861,7 +968,7 @@ def get_interfaces(blacklist_drivers=None) -> list: blacklist_drivers = [] devs = get_devicelist() # 16 somewhat arbitrarily chosen. Normally a mac is 6 '00:' tokens. - zero_mac = ':'.join(('00',) * 16) + zero_mac = ":".join(("00",) * 16) for name in devs: if not interface_has_own_mac(name): continue @@ -872,8 +979,9 @@ def get_interfaces(blacklist_drivers=None) -> list: if is_bond(name): continue if get_master(name) is not None: - if (not master_is_bridge_or_bond(name) and - not master_is_openvswitch(name)): + if not master_is_bridge_or_bond( + name + ) and not master_is_openvswitch(name): continue if is_netfailover(name): continue @@ -882,7 +990,9 @@ def get_interfaces(blacklist_drivers=None) -> list: if not mac: continue # skip nics that have no mac (00:00....) - if name != 'lo' and mac == zero_mac[:len(mac)]: + if name != "lo" and mac == zero_mac[: len(mac)]: + continue + if is_openvswitch_internal_interface(name): continue # skip nics that have drivers blacklisted driver = device_driver(name) @@ -901,24 +1011,43 @@ def get_ib_hwaddrs_by_interface(): if ib_mac: if ib_mac in ret: raise RuntimeError( - "duplicate mac found! both '%s' and '%s' have mac '%s'" % - (name, ret[ib_mac], ib_mac)) + "duplicate mac found! both '%s' and '%s' have mac '%s'" + % (name, ret[ib_mac], ib_mac) + ) ret[name] = ib_mac return ret -def has_url_connectivity(url): - """Return true when the instance has access to the provided URL +def has_url_connectivity(url_data: Dict[str, Any]) -> bool: + """Return true when the instance has access to the provided URL. Logs a warning if url is not the expected format. + + url_data is a dictionary of kwargs to send to readurl. E.g.: + + has_url_connectivity({ + "url": "http://example.invalid", + "headers": {"some": "header"}, + "timeout": 10 + }) """ - if not any([url.startswith('http://'), url.startswith('https://')]): + if "url" not in url_data: + LOG.warning( + "Ignoring connectivity check. No 'url' to check in %s", url_data + ) + return False + url = url_data["url"] + if not any([url.startswith("http://"), url.startswith("https://")]): LOG.warning( "Ignoring connectivity check. Expected URL beginning with http*://" - " received '%s'", url) + " received '%s'", + url, + ) return False + if "timeout" not in url_data: + url_data["timeout"] = 5 try: - readurl(url, timeout=5) + readurl(**url_data) except UrlError: return False return True @@ -961,14 +1090,22 @@ class EphemeralIPv4Network(object): No operations are performed if the provided interface already has the specified configuration. - This can be verified with the connectivity_url. + This can be verified with the connectivity_url_data. If unconnected, bring up the interface with valid ip, prefix and broadcast. If router is provided setup a default route for that interface. Upon context exit, clean up the interface leaving no configuration behind. """ - def __init__(self, interface, ip, prefix_or_mask, broadcast, router=None, - connectivity_url=None, static_routes=None): + def __init__( + self, + interface, + ip, + prefix_or_mask, + broadcast, + router=None, + connectivity_url_data: Dict[str, Any] = None, + static_routes=None, + ): """Setup context manager and validate call signature. @param interface: Name of the network interface to bring up. @@ -977,22 +1114,25 @@ class EphemeralIPv4Network(object): prefix. @param broadcast: Broadcast address for the IPv4 network. @param router: Optionally the default gateway IP. - @param connectivity_url: Optionally, a URL to verify if a usable + @param connectivity_url_data: 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( - 'Cannot init network on {0} with {1}/{2} and bcast {3}'.format( - interface, ip, prefix_or_mask, broadcast)) + "Cannot init network on {0} with {1}/{2} and bcast {3}".format( + interface, ip, prefix_or_mask, broadcast + ) + ) try: - self.prefix = mask_to_net_prefix(prefix_or_mask) + self.prefix = ipv4_mask_to_net_prefix(prefix_or_mask) except ValueError as e: raise ValueError( - 'Cannot setup network: {0}'.format(e) + "Cannot setup network, invalid prefix or " + "netmask: {0}".format(e) ) from e - self.connectivity_url = connectivity_url + self.connectivity_url_data = connectivity_url_data self.interface = interface self.ip = ip self.broadcast = broadcast @@ -1002,11 +1142,13 @@ class EphemeralIPv4Network(object): def __enter__(self): """Perform ephemeral network setup if interface is not connected.""" - if self.connectivity_url: - if has_url_connectivity(self.connectivity_url): + if self.connectivity_url_data: + if has_url_connectivity(self.connectivity_url_data): LOG.debug( - 'Skip ephemeral network setup, instance has connectivity' - ' to %s', self.connectivity_url) + "Skip ephemeral network setup, instance has connectivity" + " to %s", + self.connectivity_url_data["url"], + ) return self._bringup_device() @@ -1035,74 +1177,169 @@ class EphemeralIPv4Network(object): def _delete_address(self, address, prefix): """Perform the ip command to remove the specified address.""" subp.subp( - ['ip', '-family', 'inet', 'addr', 'del', - '%s/%s' % (address, prefix), 'dev', self.interface], - capture=True) + [ + "ip", + "-family", + "inet", + "addr", + "del", + "%s/%s" % (address, prefix), + "dev", + self.interface, + ], + capture=True, + ) def _bringup_device(self): """Perform the ip comands to fully setup the device.""" - cidr = '{0}/{1}'.format(self.ip, self.prefix) + cidr = "{0}/{1}".format(self.ip, self.prefix) LOG.debug( - 'Attempting setup of ephemeral network on %s with %s brd %s', - self.interface, cidr, self.broadcast) + "Attempting setup of ephemeral network on %s with %s brd %s", + self.interface, + cidr, + self.broadcast, + ) try: subp.subp( - ['ip', '-family', 'inet', 'addr', 'add', cidr, 'broadcast', - self.broadcast, 'dev', self.interface], - capture=True, update_env={'LANG': 'C'}) + [ + "ip", + "-family", + "inet", + "addr", + "add", + cidr, + "broadcast", + self.broadcast, + "dev", + self.interface, + ], + capture=True, + update_env={"LANG": "C"}, + ) except subp.ProcessExecutionError as e: if "File exists" not in e.stderr: raise LOG.debug( - 'Skip ephemeral network setup, %s already has address %s', - self.interface, self.ip) + "Skip ephemeral network setup, %s already has address %s", + self.interface, + self.ip, + ) else: # Address creation success, bring up device and queue cleanup subp.subp( - ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface, - 'up'], capture=True) + [ + "ip", + "-family", + "inet", + "link", + "set", + "dev", + self.interface, + "up", + ], + capture=True, + ) self.cleanup_cmds.append( - ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface, - 'down']) + [ + "ip", + "-family", + "inet", + "link", + "set", + "dev", + self.interface, + "down", + ] + ) self.cleanup_cmds.append( - ['ip', '-family', 'inet', 'addr', 'del', cidr, 'dev', - self.interface]) + [ + "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] + if gateway != "0.0.0.0": + via_arg = ["via", gateway] subp.subp( - ['ip', '-4', 'route', 'add', net_address] + via_arg + - ['dev', self.interface], capture=True) + ["ip", "-4", "route", "append", 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]) + 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 - out, _ = subp.subp(['ip', 'route', 'show', '0.0.0.0/0'], capture=True) - if 'default' in out: + out, _ = subp.subp(["ip", "route", "show", "0.0.0.0/0"], capture=True) + if "default" in out: LOG.debug( - 'Skip ephemeral route setup. %s already has default route: %s', - self.interface, out.strip()) + "Skip ephemeral route setup. %s already has default route: %s", + self.interface, + out.strip(), + ) return subp.subp( - ['ip', '-4', 'route', 'add', self.router, 'dev', self.interface, - 'src', self.ip], capture=True) + [ + "ip", + "-4", + "route", + "add", + self.router, + "dev", + self.interface, + "src", + self.ip, + ], + capture=True, + ) self.cleanup_cmds.insert( 0, - ['ip', '-4', 'route', 'del', self.router, 'dev', self.interface, - 'src', self.ip]) + [ + "ip", + "-4", + "route", + "del", + self.router, + "dev", + self.interface, + "src", + self.ip, + ], + ) subp.subp( - ['ip', '-4', 'route', 'add', 'default', 'via', self.router, - 'dev', self.interface], capture=True) + [ + "ip", + "-4", + "route", + "add", + "default", + "via", + self.router, + "dev", + self.interface, + ], + capture=True, + ) self.cleanup_cmds.insert( - 0, ['ip', '-4', 'route', 'del', 'default', 'dev', self.interface]) + 0, ["ip", "-4", "route", "del", "default", "dev", self.interface] + ) class RendererNotFoundError(RuntimeError): diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py new file mode 100644 index 00000000..e80c26df --- /dev/null +++ b/cloudinit/net/activators.py @@ -0,0 +1,290 @@ +# This file is part of cloud-init. See LICENSE file for license information. +import logging +import os +from abc import ABC, abstractmethod +from typing import Iterable, List, Type + +from cloudinit import subp, util +from cloudinit.net.eni import available as eni_available +from cloudinit.net.netplan import available as netplan_available +from cloudinit.net.network_state import NetworkState +from cloudinit.net.networkd import available as networkd_available +from cloudinit.net.sysconfig import NM_CFG_FILE + +LOG = logging.getLogger(__name__) + + +class NoActivatorException(Exception): + pass + + +def _alter_interface(cmd, device_name) -> bool: + LOG.debug("Attempting command %s for device %s", cmd, device_name) + try: + (_out, err) = subp.subp(cmd) + if len(err): + LOG.warning("Running %s resulted in stderr output: %s", cmd, err) + return True + except subp.ProcessExecutionError: + util.logexc(LOG, "Running interface command %s failed", cmd) + return False + + +class NetworkActivator(ABC): + @staticmethod + @abstractmethod + def available() -> bool: + """Return True if activator is available, otherwise return False.""" + raise NotImplementedError() + + @staticmethod + @abstractmethod + def bring_up_interface(device_name: str) -> bool: + """Bring up interface. + + Return True is successful, otherwise return False + """ + raise NotImplementedError() + + @staticmethod + @abstractmethod + def bring_down_interface(device_name: str) -> bool: + """Bring down interface. + + Return True is successful, otherwise return False + """ + raise NotImplementedError() + + @classmethod + def bring_up_interfaces(cls, device_names: Iterable[str]) -> bool: + """Bring up specified list of interfaces. + + Return True is successful, otherwise return False + """ + return all(cls.bring_up_interface(device) for device in device_names) + + @classmethod + def bring_up_all_interfaces(cls, network_state: NetworkState) -> bool: + """Bring up all interfaces. + + Return True is successful, otherwise return False + """ + return cls.bring_up_interfaces( + [i["name"] for i in network_state.iter_interfaces()] + ) + + @classmethod + def bring_down_interfaces(cls, device_names: Iterable[str]) -> bool: + """Bring down specified list of interfaces. + + Return True is successful, otherwise return False + """ + return all(cls.bring_down_interface(device) for device in device_names) + + @classmethod + def bring_down_all_interfaces(cls, network_state: NetworkState) -> bool: + """Bring down all interfaces. + + Return True is successful, otherwise return False + """ + return cls.bring_down_interfaces( + [i["name"] for i in network_state.iter_interfaces()] + ) + + +class IfUpDownActivator(NetworkActivator): + # Note that we're not overriding bring_up_interfaces to pass something + # like ifup --all because it isn't supported everywhere. + # E.g., NetworkManager has a ifupdown plugin that requires the name + # of a specific connection. + @staticmethod + def available(target=None) -> bool: + """Return true if ifupdown can be used on this system.""" + return eni_available(target=target) + + @staticmethod + def bring_up_interface(device_name: str) -> bool: + """Bring up interface using ifup. + + Return True is successful, otherwise return False + """ + cmd = ["ifup", device_name] + return _alter_interface(cmd, device_name) + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + """Bring up interface using ifup. + + Return True is successful, otherwise return False + """ + cmd = ["ifdown", device_name] + return _alter_interface(cmd, device_name) + + +class NetworkManagerActivator(NetworkActivator): + @staticmethod + def available(target=None) -> bool: + """Return true if network manager can be used on this system.""" + config_present = os.path.isfile( + subp.target_path(target, path=NM_CFG_FILE) + ) + nmcli_present = subp.which("nmcli", target=target) + return config_present and bool(nmcli_present) + + @staticmethod + def bring_up_interface(device_name: str) -> bool: + """Bring up interface using nmcli. + + Return True is successful, otherwise return False + """ + cmd = ["nmcli", "connection", "up", "ifname", device_name] + return _alter_interface(cmd, device_name) + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + """Bring down interface using nmcli. + + Return True is successful, otherwise return False + """ + cmd = ["nmcli", "connection", "down", device_name] + return _alter_interface(cmd, device_name) + + +class NetplanActivator(NetworkActivator): + NETPLAN_CMD = ["netplan", "apply"] + + @staticmethod + def available(target=None) -> bool: + """Return true if netplan can be used on this system.""" + return netplan_available(target=target) + + @staticmethod + def bring_up_interface(device_name: str) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + LOG.debug( + "Calling 'netplan apply' rather than " + "altering individual interfaces" + ) + return _alter_interface(NetplanActivator.NETPLAN_CMD, "all") + + @staticmethod + def bring_up_interfaces(device_names: Iterable[str]) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + LOG.debug( + "Calling 'netplan apply' rather than " + "altering individual interfaces" + ) + return _alter_interface(NetplanActivator.NETPLAN_CMD, "all") + + @staticmethod + def bring_up_all_interfaces(network_state: NetworkState) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + return _alter_interface(NetplanActivator.NETPLAN_CMD, "all") + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + LOG.debug( + "Calling 'netplan apply' rather than " + "altering individual interfaces" + ) + return _alter_interface(NetplanActivator.NETPLAN_CMD, "all") + + @staticmethod + def bring_down_interfaces(device_names: Iterable[str]) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + LOG.debug( + "Calling 'netplan apply' rather than " + "altering individual interfaces" + ) + return _alter_interface(NetplanActivator.NETPLAN_CMD, "all") + + @staticmethod + def bring_down_all_interfaces(network_state: NetworkState) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + return _alter_interface(NetplanActivator.NETPLAN_CMD, "all") + + +class NetworkdActivator(NetworkActivator): + @staticmethod + def available(target=None) -> bool: + """Return true if ifupdown can be used on this system.""" + return networkd_available(target=target) + + @staticmethod + def bring_up_interface(device_name: str) -> bool: + """Return True is successful, otherwise return False""" + cmd = ["ip", "link", "set", "up", device_name] + return _alter_interface(cmd, device_name) + + @staticmethod + def bring_up_all_interfaces(network_state: NetworkState) -> bool: + """Return True is successful, otherwise return False""" + cmd = ["systemctl", "restart", "systemd-networkd", "systemd-resolved"] + return _alter_interface(cmd, "all") + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + """Return True is successful, otherwise return False""" + cmd = ["ip", "link", "set", "down", device_name] + return _alter_interface(cmd, device_name) + + +# This section is mostly copied and pasted from renderers.py. An abstract +# version to encompass both seems overkill at this point +DEFAULT_PRIORITY = [ + IfUpDownActivator, + NetworkManagerActivator, + NetplanActivator, + NetworkdActivator, +] + + +def search_activator( + priority=None, target=None +) -> List[Type[NetworkActivator]]: + if priority is None: + priority = DEFAULT_PRIORITY + + unknown = [i for i in priority if i not in DEFAULT_PRIORITY] + if unknown: + raise ValueError( + "Unknown activators provided in priority list: %s" % unknown + ) + + return [activator for activator in priority if activator.available(target)] + + +def select_activator(priority=None, target=None) -> Type[NetworkActivator]: + found = search_activator(priority, target) + if not found: + if priority is None: + priority = DEFAULT_PRIORITY + tmsg = "" + if target and target != "/": + tmsg = " in target=%s" % target + raise NoActivatorException( + "No available network activators found%s. Searched " + "through list: %s" % (tmsg, priority) + ) + selected = found[0] + LOG.debug("Using selected activator: %s", selected) + return selected diff --git a/cloudinit/net/bsd.py b/cloudinit/net/bsd.py index e34e0454..ff5c7413 100644 --- a/cloudinit/net/bsd.py +++ b/cloudinit/net/bsd.py @@ -3,11 +3,9 @@ import re from cloudinit import log as logging -from cloudinit import net -from cloudinit import util -from cloudinit import subp -from cloudinit.distros.parsers.resolv_conf import ResolvConf +from cloudinit import net, subp, util from cloudinit.distros import bsd_utils +from cloudinit.distros.parsers.resolv_conf import ResolvConf from . import renderer @@ -15,8 +13,8 @@ LOG = logging.getLogger(__name__) class BSDRenderer(renderer.Renderer): - resolv_conf_fn = 'etc/resolv.conf' - rc_conf_fn = 'etc/rc.conf' + resolv_conf_fn = "etc/resolv.conf" + rc_conf_fn = "etc/rc.conf" def get_rc_config_value(self, key): fn = subp.target_path(self.target, self.rc_conf_fn) @@ -31,115 +29,136 @@ class BSDRenderer(renderer.Renderer): config = {} self.target = None self.interface_configurations = {} - self._postcmds = config.get('postcmds', True) + self._postcmds = config.get("postcmds", True) - def _ifconfig_entries(self, settings, target=None): + def _ifconfig_entries(self, settings): 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): + 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) + 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) + LOG.info( + "netif service will rename interface %s to %s", + cur_name, + device_name, + ) try: self.rename_interface(cur_name, device_name) except NotImplementedError: - LOG.error(( - 'Interface renaming is ' - 'not supported on this OS')) + LOG.error( + "Interface renaming is not supported on this OS" + ) device_name = cur_name else: device_name = ifname_by_mac[device_mac] - LOG.info('Configuring interface %s', device_name) + LOG.info("Configuring interface %s", device_name) - self.interface_configurations[device_name] = 'DHCP' + self.interface_configurations[device_name] = "DHCP" for subnet in interface.get("subnets", []): - if subnet.get('type') == 'static': - if not subnet.get('netmask'): + if subnet.get("type") == "static": + if not subnet.get("netmask"): LOG.debug( - 'Skipping IP %s, because there is no netmask', - subnet.get('address') + "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')) + LOG.debug( + "Configuring dev %s with %s / %s", + device_name, + subnet.get("address"), + subnet.get("netmask"), + ) self.interface_configurations[device_name] = { - 'address': subnet.get('address'), - 'netmask': subnet.get('netmask'), + "address": subnet.get("address"), + "netmask": subnet.get("netmask"), + "mtu": subnet.get("mtu") or interface.get("mtu"), } - def _route_entries(self, settings, target=None): + def _route_entries(self, settings): routes = list(settings.iter_routes()) for interface in settings.iter_interfaces(): subnets = interface.get("subnets", []) for subnet in subnets: - if subnet.get('type') != 'static': + 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', []) + 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", []) for route in routes: - network = route.get('network') + network = route.get("network") if not network: - LOG.debug('Skipping a bad route entry') + LOG.debug("Skipping a bad route entry") continue - netmask = route.get('netmask') - gateway = route.get('gateway') + netmask = route.get("netmask") + gateway = route.get("gateway") self.set_route(network, netmask, gateway) - def _resolve_conf(self, settings, target=None): + def _resolve_conf(self, settings): 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']) + 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(subp.target_path( - target, self.resolv_conf_fn))) + resolvconf = ResolvConf( + util.load_file( + subp.target_path(self.target, self.resolv_conf_fn) + ) + ) resolvconf.parse() except IOError: - util.logexc(LOG, "Failed to parse %s, use new empty file", - subp.target_path(target, self.resolv_conf_fn)) - resolvconf = ResolvConf('') + util.logexc( + LOG, + "Failed to parse %s, use new empty file", + subp.target_path(self.target, self.resolv_conf_fn), + ) + resolvconf = ResolvConf("") resolvconf.parse() # Add some nameservers - for server in nameservers: + for server in set(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: + for domain in set(searchdomains): try: resolvconf.add_search_domain(domain) except ValueError: util.logexc(LOG, "Failed to add search domain %s", domain) util.write_file( - subp.target_path(target, self.resolv_conf_fn), - str(resolvconf), 0o644) + subp.target_path(self.target, self.resolv_conf_fn), + str(resolvconf), + 0o644, + ) def render_network_state(self, network_state, templates=None, target=None): + if target: + self.target = target self._ifconfig_entries(settings=network_state) self._route_entries(settings=network_state) self._resolve_conf(settings=network_state) @@ -149,7 +168,7 @@ class BSDRenderer(renderer.Renderer): def dhcp_interfaces(self): ic = self.interface_configurations.items - return [k for k, v in ic() if v == 'DHCP'] + return [k for k, v in ic() if v == "DHCP"] def start_services(self, run=False): raise NotImplementedError() diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py index cc8dc17b..eab86d9f 100755 --- a/cloudinit/net/cmdline.py +++ b/cloudinit/net/cmdline.py @@ -12,11 +12,11 @@ import gzip import io import logging import os +import shlex from cloudinit import util -from . import get_devicelist -from . import read_sys_net_safe +from . import get_devicelist, read_sys_net_safe _OPEN_ISCSI_INTERFACE_FILE = "/run/initramfs/open-iscsi.interface" @@ -57,7 +57,7 @@ class KlibcNetworkConfigSource(InitramfsNetworkConfigSource): if self._mac_addrs is None: self._mac_addrs = {} for k in get_devicelist(): - mac_addr = read_sys_net_safe(k, 'address') + mac_addr = read_sys_net_safe(k, "address") if mac_addr: self._mac_addrs[k] = mac_addr @@ -72,8 +72,9 @@ class KlibcNetworkConfigSource(InitramfsNetworkConfigSource): (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 + for item in shlex.split(self._cmdline): + if item.startswith("ip=") or item.startswith("ip6="): + return True if os.path.exists(_OPEN_ISCSI_INTERFACE_FILE): # iBft can configure networking without ip= return True @@ -81,7 +82,8 @@ class KlibcNetworkConfigSource(InitramfsNetworkConfigSource): def render_config(self) -> dict: return config_from_klibc_net_cfg( - files=self._files, mac_addrs=self._mac_addrs, + files=self._files, + mac_addrs=self._mac_addrs, ) @@ -111,78 +113,78 @@ def _klibc_to_config_entry(content, mac_addrs=None): data = util.load_shell_content(content) try: - name = data['DEVICE'] if 'DEVICE' in data else data['DEVICE6'] + name = data["DEVICE"] if "DEVICE" in data else data["DEVICE6"] except KeyError as e: raise ValueError("no 'DEVICE' or 'DEVICE6' entry in data") from e # ipconfig on precise does not write PROTO # IPv6 config gives us IPV6PROTO, not PROTO. - proto = data.get('PROTO', data.get('IPV6PROTO')) + proto = data.get("PROTO", data.get("IPV6PROTO")) if not proto: - if data.get('filename'): - proto = 'dhcp' + if data.get("filename"): + proto = "dhcp" else: - proto = 'none' + proto = "none" - if proto not in ('none', 'dhcp', 'dhcp6'): + if proto not in ("none", "dhcp", "dhcp6"): raise ValueError("Unexpected value for PROTO: %s" % proto) iface = { - 'type': 'physical', - 'name': name, - 'subnets': [], + "type": "physical", + "name": name, + "subnets": [], } if name in mac_addrs: - iface['mac_address'] = mac_addrs[name] + iface["mac_address"] = mac_addrs[name] # Handle both IPv4 and IPv6 values - for pre in ('IPV4', 'IPV6'): + for pre in ("IPV4", "IPV6"): # if no IPV4ADDR or IPV6ADDR, then go on. if pre + "ADDR" not in data: continue # PROTO for ipv4, IPV6PROTO for ipv6 - cur_proto = data.get(pre + 'PROTO', proto) + 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'} + if cur_proto == "none": + cur_proto = "static" + subnet = {"type": cur_proto, "control": "manual"} # only populate address for static types. While the rendered config # may have an address for dhcp, that is not really expected. - if cur_proto == 'static': - subnet['address'] = data[pre + 'ADDR'] + if cur_proto == "static": + subnet["address"] = data[pre + "ADDR"] # these fields go right on the subnet - for key in ('NETMASK', 'BROADCAST', 'GATEWAY'): + for key in ("NETMASK", "BROADCAST", "GATEWAY"): if pre + key in data: subnet[key.lower()] = data[pre + key] dns = [] # handle IPV4DNS0 or IPV6DNS0 - for nskey in ('DNS0', 'DNS1'): + for nskey in ("DNS0", "DNS1"): ns = data.get(pre + nskey) # verify it has something other than 0.0.0.0 (or ipv6) if ns and len(ns.strip(":.0")): dns.append(data[pre + nskey]) if dns: - subnet['dns_nameservers'] = dns + subnet["dns_nameservers"] = dns # add search to both ipv4 and ipv6, as it has no namespace - search = data.get('DOMAINSEARCH') + search = data.get("DOMAINSEARCH") if search: - if ',' in search: - subnet['dns_search'] = search.split(",") + if "," in search: + subnet["dns_search"] = search.split(",") else: - subnet['dns_search'] = search.split() + subnet["dns_search"] = search.split() - iface['subnets'].append(subnet) + iface["subnets"].append(subnet) return name, iface def _get_klibc_net_cfg_files(): - return glob.glob('/run/net-*.conf') + glob.glob('/run/net6-*.conf') + return glob.glob("/run/net-*.conf") + glob.glob("/run/net6-*.conf") def config_from_klibc_net_cfg(files=None, mac_addrs=None): @@ -192,24 +194,28 @@ def config_from_klibc_net_cfg(files=None, mac_addrs=None): entries = [] names = {} for cfg_file in files: - name, entry = _klibc_to_config_entry(util.load_file(cfg_file), - mac_addrs=mac_addrs) + name, entry = _klibc_to_config_entry( + util.load_file(cfg_file), mac_addrs=mac_addrs + ) if name in names: - prev = names[name]['entry'] - if prev.get('mac_address') != entry.get('mac_address'): + prev = names[name]["entry"] + if prev.get("mac_address") != entry.get("mac_address"): raise ValueError( "device '{name}' was defined multiple times ({files})" " but had differing mac addresses: {old} -> {new}.".format( - name=name, files=' '.join(names[name]['files']), - old=prev.get('mac_address'), - new=entry.get('mac_address'))) - prev['subnets'].extend(entry['subnets']) - names[name]['files'].append(cfg_file) + name=name, + files=" ".join(names[name]["files"]), + old=prev.get("mac_address"), + new=entry.get("mac_address"), + ) + ) + prev["subnets"].extend(entry["subnets"]) + names[name]["files"].append(cfg_file) else: - names[name] = {'files': [cfg_file], 'entry': entry} + names[name] = {"files": [cfg_file], "entry": entry} entries.append(entry) - return {'config': entries, 'version': 1} + return {"config": entries, "version": 1} def read_initramfs_config(): @@ -255,8 +261,10 @@ def _b64dgz(data): except (TypeError, ValueError): logging.error( "Expected base64 encoded kernel commandline parameter" - " network-config. Ignoring network-config=%s.", data) - return '' + " network-config. Ignoring network-config=%s.", + data, + ) + return "" return _decomp_gzip(blob) @@ -265,7 +273,7 @@ def read_kernel_cmdline_config(cmdline=None): if cmdline is None: cmdline = util.get_cmdline() - if 'network-config=' in cmdline: + if "network-config=" in cmdline: data64 = None for tok in cmdline.split(): if tok.startswith("network-config="): @@ -277,4 +285,5 @@ def read_kernel_cmdline_config(cmdline=None): return None + # vi: ts=4 expandtab diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py index 4394c68b..f9af18cf 100644 --- a/cloudinit/net/dhcp.py +++ b/cloudinit/net/dhcp.py @@ -4,25 +4,28 @@ # # This file is part of cloud-init. See LICENSE file for license information. -import configobj import logging import os import re import signal import time from io import StringIO +from typing import Any, Dict + +import configobj +from cloudinit import subp, temp_utils, util from cloudinit.net import ( - EphemeralIPv4Network, find_fallback_nic, get_devicelist, - has_url_connectivity) + EphemeralIPv4Network, + find_fallback_nic, + get_devicelist, + has_url_connectivity, +) from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip -from cloudinit import temp_utils -from cloudinit import subp -from cloudinit import util LOG = logging.getLogger(__name__) -NETWORKD_LEASES_DIR = '/run/systemd/netif/leases' +NETWORKD_LEASES_DIR = "/run/systemd/netif/leases" class InvalidDHCPLeaseFileError(Exception): @@ -38,21 +41,28 @@ class NoDHCPLeaseError(Exception): class EphemeralDHCPv4(object): - def __init__(self, iface=None, connectivity_url=None, dhcp_log_func=None): + def __init__( + self, + iface=None, + connectivity_url_data: Dict[str, Any] = None, + dhcp_log_func=None, + ): self.iface = iface self._ephipv4 = None self.lease = None self.dhcp_log_func = dhcp_log_func - self.connectivity_url = connectivity_url + self.connectivity_url_data = connectivity_url_data def __enter__(self): """Setup sandboxed dhcp context, unless connectivity_url can already be reached.""" - if self.connectivity_url: - if has_url_connectivity(self.connectivity_url): + if self.connectivity_url_data: + if has_url_connectivity(self.connectivity_url_data): LOG.debug( - 'Skip ephemeral DHCP setup, instance has connectivity' - ' to %s', self.connectivity_url) + "Skip ephemeral DHCP setup, instance has connectivity" + " to %s", + self.connectivity_url_data, + ) return return self.obtain_lease() @@ -81,31 +91,39 @@ class EphemeralDHCPv4(object): return self.lease try: leases = maybe_perform_dhcp_discovery( - self.iface, self.dhcp_log_func) + self.iface, self.dhcp_log_func + ) except InvalidDHCPLeaseFileError as e: raise NoDHCPLeaseError() from e if not leases: raise NoDHCPLeaseError() self.lease = leases[-1] - LOG.debug("Received dhcp lease on %s for %s/%s", - self.lease['interface'], self.lease['fixed-address'], - self.lease['subnet-mask']) - 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'} + LOG.debug( + "Received dhcp lease on %s for %s/%s", + self.lease["interface"], + self.lease["fixed-address"], + self.lease["subnet-mask"], + ) + 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 = 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 + 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_data: + kwargs["connectivity_url_data"] = self.connectivity_url_data ephipv4 = EphemeralIPv4Network(**kwargs) ephipv4.__enter__() self._ephipv4 = ephipv4 @@ -116,16 +134,15 @@ class EphemeralDHCPv4(object): 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 + 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): + 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) @@ -147,19 +164,20 @@ def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None): if nic is None: nic = find_fallback_nic() if nic is None: - LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.') + LOG.debug("Skip dhcp_discovery: Unable to find fallback nic.") return [] elif nic not in get_devicelist(): LOG.debug( - 'Skip dhcp_discovery: nic %s not found in get_devicelist.', nic) + "Skip dhcp_discovery: nic %s not found in get_devicelist.", nic + ) return [] - dhclient_path = subp.which('dhclient') + dhclient_path = subp.which("dhclient") if not dhclient_path: - LOG.debug('Skip dhclient configuration: No dhclient command found.') + LOG.debug("Skip dhclient configuration: No dhclient command found.") return [] - with temp_utils.tempdir(rmtree_ignore_errors=True, - 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, dhcp_log_func) @@ -173,25 +191,28 @@ def parse_dhcp_lease_file(lease_file): @raises: InvalidDHCPLeaseFileError on empty of unparseable leasefile content. """ - lease_regex = re.compile(r"lease {(?P<lease>[^}]*)}\n") + lease_regex = re.compile(r"lease {(?P<lease>.*?)}\n", re.DOTALL) dhcp_leases = [] lease_content = util.load_file(lease_file) if len(lease_content) == 0: raise InvalidDHCPLeaseFileError( - 'Cannot parse empty dhcp lease file {0}'.format(lease_file)) + "Cannot parse empty dhcp lease file {0}".format(lease_file) + ) for lease in lease_regex.findall(lease_content): lease_options = [] - for line in lease.split(';'): + for line in lease.split(";"): # Strip newlines, double-quotes and option prefix - line = line.strip().replace('"', '').replace('option ', '') + line = line.strip().replace('"', "").replace("option ", "") if not line: continue - lease_options.append(line.split(' ', 1)) + lease_options.append(line.split(" ", 1)) dhcp_leases.append(dict(lease_options)) if not dhcp_leases: raise InvalidDHCPLeaseFileError( - 'Cannot parse dhcp lease file {0}. No leases found'.format( - lease_file)) + "Cannot parse dhcp lease file {0}. No leases found".format( + lease_file + ) + ) return dhcp_leases @@ -208,17 +229,17 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): @return: A list of dicts of representing the dhcp leases parsed from the dhcp.leases file or empty list. """ - LOG.debug('Performing a dhcp discovery on %s', interface) + LOG.debug("Performing a dhcp discovery on %s", interface) # XXX We copy dhclient out of /sbin/dhclient to avoid dealing with strict # app armor profiles which disallow running dhclient -sf <our-script-file>. # We want to avoid running /sbin/dhclient-script because of side-effects in # /etc/resolv.conf any any other vendor specific scripts in # /etc/dhcp/dhclient*hooks.d. - sandbox_dhclient_cmd = os.path.join(cleandir, 'dhclient') + sandbox_dhclient_cmd = os.path.join(cleandir, "dhclient") util.copy(dhclient_cmd_path, sandbox_dhclient_cmd) - pid_file = os.path.join(cleandir, 'dhclient.pid') - lease_file = os.path.join(cleandir, 'dhcp.leases') + pid_file = os.path.join(cleandir, "dhclient.pid") + lease_file = os.path.join(cleandir, "dhcp.leases") # In some cases files in /var/tmp may not be executable, launching dhclient # from there will certainly raise 'Permission denied' error. Try launching @@ -230,9 +251,19 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): # Generally dhclient relies on dhclient-script PREINIT action to bring the # link up before attempting discovery. Since we are using -sf /bin/true, # we need to do that "link up" ourselves first. - subp.subp(['ip', 'link', 'set', 'dev', interface, 'up'], capture=True) - cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file, - '-pf', pid_file, interface, '-sf', '/bin/true'] + subp.subp(["ip", "link", "set", "dev", interface, "up"], capture=True) + cmd = [ + sandbox_dhclient_cmd, + "-1", + "-v", + "-lf", + lease_file, + "-pf", + pid_file, + interface, + "-sf", + "/bin/true", + ] out, err = subp.subp(cmd, capture=True) # Wait for pid file and lease file to appear, and for the process @@ -243,13 +274,16 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): # 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) + [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)) + LOG.warning( + "dhclient did not produce expected files: %s", + ", ".join(os.path.basename(f) for f in missing), + ) return [] - ppid = 'unknown' + ppid = "unknown" daemonized = False for _ in range(0, 1000): pid_content = util.load_file(pid_file).strip() @@ -260,7 +294,7 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): else: ppid = util.get_proc_ppid(pid) if ppid == 1: - LOG.debug('killing dhclient with pid=%s', pid) + LOG.debug("killing dhclient with pid=%s", pid) os.kill(pid, signal.SIGKILL) daemonized = True break @@ -268,8 +302,11 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir, dhcp_log_func=None): if not daemonized: LOG.error( - 'dhclient(pid=%s, parentpid=%s) failed to daemonize after %s ' - 'seconds', pid_content, ppid, 0.01 * 1000 + "dhclient(pid=%s, parentpid=%s) failed to daemonize after %s " + "seconds", + pid_content, + ppid, + 0.01 * 1000, ) if dhcp_log_func is not None: dhcp_log_func(out, err) @@ -301,7 +338,8 @@ def networkd_load_leases(leases_d=None): return ret for lfile in os.listdir(leases_d): ret[lfile] = networkd_parse_lease( - util.load_file(os.path.join(leases_d, lfile))) + util.load_file(os.path.join(leases_d, lfile)) + ) return ret @@ -316,7 +354,7 @@ def networkd_get_option_from_leases(keyname, leases_d=None): def parse_static_routes(rfc3442): - """ parse rfc3442 format and return a list containing tuple of strings. + """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, @@ -346,10 +384,12 @@ def parse_static_routes(rfc3442): 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)) + 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 @@ -362,32 +402,32 @@ def parse_static_routes(rfc3442): 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]) + 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]) + 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]) + 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]) + 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 @@ -395,15 +435,19 @@ def parse_static_routes(rfc3442): _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]) + 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) + 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 0074691b..99e3fbb0 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -5,32 +5,58 @@ import glob import os import re -from . import ParserError - -from . import renderer -from .network_state import subnet_is_ipv6 - from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util +from cloudinit import subp, util +from . import ParserError, renderer +from .network_state import subnet_is_ipv6 LOG = logging.getLogger(__name__) NET_CONFIG_COMMANDS = [ - "pre-up", "up", "post-up", "down", "pre-down", "post-down", + "pre-up", + "up", + "post-up", + "down", + "pre-down", + "post-down", ] NET_CONFIG_BRIDGE_OPTIONS = [ - "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit", - "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp", + "bridge_ageing", + "bridge_bridgeprio", + "bridge_fd", + "bridge_gcinit", + "bridge_hello", + "bridge_maxage", + "bridge_maxwait", + "bridge_stp", ] NET_CONFIG_OPTIONS = [ - "address", "netmask", "broadcast", "network", "metric", "gateway", - "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime", - "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame", - "netnum", "endpoint", "local", "ttl", + "address", + "netmask", + "broadcast", + "network", + "metric", + "gateway", + "pointtopoint", + "media", + "mtu", + "hostname", + "leasehours", + "leasetime", + "vendor", + "client", + "bootfile", + "server", + "hwaddr", + "provider", + "frame", + "netnum", + "endpoint", + "local", + "ttl", ] @@ -38,27 +64,27 @@ NET_CONFIG_OPTIONS = [ def _iface_add_subnet(iface, subnet): content = [] valid_map = [ - 'address', - 'netmask', - 'broadcast', - 'metric', - 'gateway', - 'pointopoint', - 'mtu', - 'scope', - 'dns_search', - 'dns_nameservers', + "address", + "netmask", + "broadcast", + "metric", + "gateway", + "pointopoint", + "mtu", + "scope", + "dns_search", + "dns_nameservers", ] for key, value in subnet.items(): - if key == 'netmask': + if key == "netmask": continue - if key == 'address': - value = "%s/%s" % (subnet['address'], subnet['prefix']) + if key == "address": + value = "%s/%s" % (subnet["address"], subnet["prefix"]) if value and key in valid_map: if type(value) == list: value = " ".join(value) - if '_' in key: - key = key.replace('_', '-') + if "_" in key: + key = key.replace("_", "-") content.append(" {0} {1}".format(key, value)) return sorted(content) @@ -75,41 +101,44 @@ def _iface_add_attrs(iface, index, ipv4_subnet_mtu): return [] content = [] ignore_map = [ - 'control', - 'device_id', - 'driver', - 'index', - 'inet', - 'mode', - 'name', - 'subnets', - 'type', + "control", + "device_id", + "driver", + "index", + "inet", + "mode", + "name", + "subnets", + "type", ] # The following parameters require repetitive entries of the key for # each of the values multiline_keys = [ - 'bridge_pathcost', - 'bridge_portprio', - 'bridge_waitport', + "bridge_pathcost", + "bridge_portprio", + "bridge_waitport", ] - renames = {'mac_address': 'hwaddress'} - if iface['type'] not in ['bond', 'bridge', 'infiniband', 'vlan']: - ignore_map.append('mac_address') + renames = {"mac_address": "hwaddress"} + if iface["type"] not in ["bond", "bridge", "infiniband", "vlan"]: + ignore_map.append("mac_address") for key, value in iface.items(): # convert bool to string for eni if type(value) == bool: - value = 'on' if iface[key] else 'off' + value = "on" if iface[key] else "off" if not value or key in ignore_map: continue - if key == 'mtu' and ipv4_subnet_mtu: + if key == "mtu" and ipv4_subnet_mtu: if value != ipv4_subnet_mtu: LOG.warning( "Network config: ignoring %s device-level mtu:%s because" " ipv4 subnet-level mtu:%s provided.", - iface['name'], value, ipv4_subnet_mtu) + iface["name"], + value, + ipv4_subnet_mtu, + ) continue if key in multiline_keys: for v in value: @@ -123,9 +152,9 @@ def _iface_add_attrs(iface, index, ipv4_subnet_mtu): def _iface_start_entry(iface, index, render_hwaddress=False): - fullname = iface['name'] + fullname = iface["name"] - control = iface['control'] + control = iface["control"] if control == "auto": cverb = "auto" elif control in ("hotplug",): @@ -134,12 +163,13 @@ def _iface_start_entry(iface, index, render_hwaddress=False): cverb = "# control-" + control subst = iface.copy() - subst.update({'fullname': fullname, 'cverb': cverb}) + subst.update({"fullname": fullname, "cverb": cverb}) lines = [ "{cverb} {fullname}".format(**subst), - "iface {fullname} {inet} {mode}".format(**subst)] - if render_hwaddress and iface.get('mac_address'): + "iface {fullname} {inet} {mode}".format(**subst), + ] + if render_hwaddress and iface.get("mac_address"): lines.append(" hwaddress {mac_address}".format(**subst)) return lines @@ -159,9 +189,9 @@ def _parse_deb_config_data(ifaces, contents, src_dir, src_path): currif = None for line in contents.splitlines(): line = line.strip() - if line.startswith('#'): + if line.startswith("#"): continue - split = line.split(' ') + split = line.split(" ") option = split[0] if option == "source-directory": parsed_src_dir = split[1] @@ -172,16 +202,18 @@ def _parse_deb_config_data(ifaces, contents, src_dir, src_path): dir_contents = [ os.path.join(expanded_path, path) for path in dir_contents - if (os.path.isfile(os.path.join(expanded_path, path)) and - re.match("^[a-zA-Z0-9_-]+$", path) is not None) + if ( + os.path.isfile(os.path.join(expanded_path, path)) + and re.match("^[a-zA-Z0-9_-]+$", path) is not None + ) ] for entry in dir_contents: with open(entry, "r") as fp: src_data = fp.read().strip() abs_entry = os.path.abspath(entry) _parse_deb_config_data( - ifaces, src_data, - os.path.dirname(abs_entry), abs_entry) + ifaces, src_data, os.path.dirname(abs_entry), abs_entry + ) elif option == "source": new_src_path = split[1] if not new_src_path.startswith("/"): @@ -191,8 +223,8 @@ def _parse_deb_config_data(ifaces, contents, src_dir, src_path): src_data = fp.read().strip() abs_path = os.path.abspath(expanded_path) _parse_deb_config_data( - ifaces, src_data, - os.path.dirname(abs_path), abs_path) + ifaces, src_data, os.path.dirname(abs_path), abs_path + ) elif option == "auto": for iface in split[1:]: if iface not in ifaces: @@ -200,7 +232,7 @@ def _parse_deb_config_data(ifaces, contents, src_dir, src_path): # Include the source path this interface was found in. "_source_path": src_path } - ifaces[iface]['auto'] = True + ifaces[iface]["auto"] = True elif option == "iface": iface, family, method = split[1:4] if iface not in ifaces: @@ -208,71 +240,72 @@ def _parse_deb_config_data(ifaces, contents, src_dir, src_path): # Include the source path this interface was found in. "_source_path": src_path } - elif 'family' in ifaces[iface]: + elif "family" in ifaces[iface]: raise ParserError( "Interface %s can only be defined once. " - "Re-defined in '%s'." % (iface, src_path)) - ifaces[iface]['family'] = family - ifaces[iface]['method'] = method + "Re-defined in '%s'." % (iface, src_path) + ) + ifaces[iface]["family"] = family + ifaces[iface]["method"] = method currif = iface elif option == "hwaddress": if split[1] == "ether": val = split[2] else: val = split[1] - ifaces[currif]['hwaddress'] = val + ifaces[currif]["hwaddress"] = val elif option in NET_CONFIG_OPTIONS: ifaces[currif][option] = split[1] elif option in NET_CONFIG_COMMANDS: if option not in ifaces[currif]: ifaces[currif][option] = [] - ifaces[currif][option].append(' '.join(split[1:])) - elif option.startswith('dns-'): - if 'dns' not in ifaces[currif]: - ifaces[currif]['dns'] = {} - if option == 'dns-search': - ifaces[currif]['dns']['search'] = [] + ifaces[currif][option].append(" ".join(split[1:])) + elif option.startswith("dns-"): + if "dns" not in ifaces[currif]: + ifaces[currif]["dns"] = {} + if option == "dns-search": + ifaces[currif]["dns"]["search"] = [] for domain in split[1:]: - ifaces[currif]['dns']['search'].append(domain) - elif option == 'dns-nameservers': - ifaces[currif]['dns']['nameservers'] = [] + ifaces[currif]["dns"]["search"].append(domain) + elif option == "dns-nameservers": + ifaces[currif]["dns"]["nameservers"] = [] for server in split[1:]: - ifaces[currif]['dns']['nameservers'].append(server) - elif option.startswith('bridge_'): - if 'bridge' not in ifaces[currif]: - ifaces[currif]['bridge'] = {} + ifaces[currif]["dns"]["nameservers"].append(server) + elif option.startswith("bridge_"): + if "bridge" not in ifaces[currif]: + ifaces[currif]["bridge"] = {} if option in NET_CONFIG_BRIDGE_OPTIONS: - bridge_option = option.replace('bridge_', '', 1) - ifaces[currif]['bridge'][bridge_option] = split[1] + bridge_option = option.replace("bridge_", "", 1) + ifaces[currif]["bridge"][bridge_option] = split[1] elif option == "bridge_ports": - ifaces[currif]['bridge']['ports'] = [] + ifaces[currif]["bridge"]["ports"] = [] for iface in split[1:]: - ifaces[currif]['bridge']['ports'].append(iface) + ifaces[currif]["bridge"]["ports"].append(iface) elif option == "bridge_hw": # doc is confusing and thus some may put literal 'MAC' # bridge_hw MAC <address> # but correct is: # bridge_hw <address> if split[1].lower() == "mac": - ifaces[currif]['bridge']['mac'] = split[2] + ifaces[currif]["bridge"]["mac"] = split[2] else: - ifaces[currif]['bridge']['mac'] = split[1] + ifaces[currif]["bridge"]["mac"] = split[1] elif option == "bridge_pathcost": - if 'pathcost' not in ifaces[currif]['bridge']: - ifaces[currif]['bridge']['pathcost'] = {} - ifaces[currif]['bridge']['pathcost'][split[1]] = split[2] + if "pathcost" not in ifaces[currif]["bridge"]: + ifaces[currif]["bridge"]["pathcost"] = {} + ifaces[currif]["bridge"]["pathcost"][split[1]] = split[2] elif option == "bridge_portprio": - if 'portprio' not in ifaces[currif]['bridge']: - ifaces[currif]['bridge']['portprio'] = {} - ifaces[currif]['bridge']['portprio'][split[1]] = split[2] - elif option.startswith('bond-'): - if 'bond' not in ifaces[currif]: - ifaces[currif]['bond'] = {} - bond_option = option.replace('bond-', '', 1) - ifaces[currif]['bond'][bond_option] = split[1] + if "portprio" not in ifaces[currif]["bridge"]: + ifaces[currif]["bridge"]["portprio"] = {} + ifaces[currif]["bridge"]["portprio"][split[1]] = split[2] + elif option.startswith("bond-"): + if "bond" not in ifaces[currif]: + ifaces[currif]["bond"] = {} + bond_option = option.replace("bond-", "", 1) + ifaces[currif]["bond"][bond_option] = split[1] for iface in ifaces.keys(): - if 'auto' not in ifaces[iface]: - ifaces[iface]['auto'] = False + if "auto" not in ifaces[iface]: + ifaces[iface]["auto"] = False def parse_deb_config(path): @@ -282,8 +315,8 @@ def parse_deb_config(path): contents = fp.read().strip() abs_path = os.path.abspath(path) _parse_deb_config_data( - ifaces, contents, - os.path.dirname(abs_path), abs_path) + ifaces, contents, os.path.dirname(abs_path), abs_path + ) return ifaces @@ -308,32 +341,31 @@ def _ifaces_to_net_config_data(ifaces): dtype = "loopback" else: dtype = "physical" - devs[devname] = {'type': dtype, 'name': devname, 'subnets': []} + devs[devname] = {"type": dtype, "name": devname, "subnets": []} # this isnt strictly correct, but some might specify # hwaddress on a nic for matching / declaring name. - if 'hwaddress' in data: - devs[devname]['mac_address'] = data['hwaddress'] - subnet = {'_orig_eni_name': name, 'type': data['method']} - if data.get('auto'): - subnet['control'] = 'auto' + if "hwaddress" in data: + devs[devname]["mac_address"] = data["hwaddress"] + subnet = {"_orig_eni_name": name, "type": data["method"]} + if data.get("auto"): + subnet["control"] = "auto" else: - subnet['control'] = 'manual' + subnet["control"] = "manual" - if data.get('method') == 'static': - subnet['address'] = data['address'] + if data.get("method") == "static": + subnet["address"] = data["address"] - for copy_key in ('netmask', 'gateway', 'broadcast'): + for copy_key in ("netmask", "gateway", "broadcast"): if copy_key in data: subnet[copy_key] = data[copy_key] - if 'dns' in data: - for n in ('nameservers', 'search'): - if n in data['dns'] and data['dns'][n]: - subnet['dns_' + n] = data['dns'][n] - devs[devname]['subnets'].append(subnet) + if "dns" in data: + for n in ("nameservers", "search"): + if n in data["dns"] and data["dns"][n]: + subnet["dns_" + n] = data["dns"][n] + devs[devname]["subnets"].append(subnet) - return {'version': 1, - 'config': [devs[d] for d in sorted(devs)]} + return {"version": 1, "config": [devs[d] for d in sorted(devs)]} class Renderer(renderer.Renderer): @@ -342,10 +374,11 @@ class Renderer(renderer.Renderer): def __init__(self, config=None): if not config: config = {} - self.eni_path = config.get('eni_path', 'etc/network/interfaces') - self.eni_header = config.get('eni_header', None) + self.eni_path = config.get("eni_path", "etc/network/interfaces") + self.eni_header = config.get("eni_header", None) self.netrules_path = config.get( - 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules') + "netrules_path", "etc/udev/rules.d/70-persistent-net.rules" + ) def _render_route(self, route, indent=""): """When rendering routes for an iface, in some cases applying a route @@ -367,151 +400,166 @@ class Renderer(renderer.Renderer): down = indent + "pre-down route del" or_true = " || true" mapping = { - 'gateway': 'gw', - 'metric': 'metric', + "gateway": "gw", + "metric": "metric", } - default_gw = '' - if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0': - default_gw = ' default' - elif route['network'] == '::' and route['prefix'] == 0: - default_gw = ' -A inet6 default' + default_gw = "" + if route["network"] == "0.0.0.0" and route["netmask"] == "0.0.0.0": + default_gw = " default" + elif route["network"] == "::" and route["prefix"] == 0: + default_gw = " -A inet6 default" - route_line = '' - for k in ['network', 'gateway', 'metric']: - if default_gw and k == 'network': + route_line = "" + 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]) + if k == "gateway": + route_line += "%s %s %s" % (default_gw, mapping[k], route[k]) elif k in route: - if k == 'network': - if ':' in route[k]: - route_line += ' -A inet6' + if k == "network": + if ":" in route[k]: + route_line += " -A inet6" + elif route.get("prefix") == 32: + route_line += " -host" else: - route_line += ' -net' - if 'prefix' in route: - route_line += ' %s/%s' % (route[k], route['prefix']) + route_line += " -net" + if "prefix" in route: + route_line += " %s/%s" % (route[k], route["prefix"]) else: - route_line += ' %s %s' % (mapping[k], route[k]) + route_line += " %s %s" % (mapping[k], route[k]) content.append(up + route_line + or_true) content.append(down + route_line + or_true) return content def _render_iface(self, iface, render_hwaddress=False): sections = [] - subnets = iface.get('subnets', {}) - accept_ra = iface.pop('accept-ra', None) - ethernet_wol = iface.pop('wakeonlan', None) + subnets = iface.get("subnets", {}) + accept_ra = iface.pop("accept-ra", None) + ethernet_wol = iface.pop("wakeonlan", None) if ethernet_wol: # Specify WOL setting 'g' for using "Magic Packet" - iface['ethernet-wol'] = 'g' + iface["ethernet-wol"] = "g" if subnets: for index, subnet in enumerate(subnets): ipv4_subnet_mtu = None - iface['index'] = index - iface['mode'] = subnet['type'] - iface['control'] = subnet.get('control', 'auto') - subnet_inet = 'inet' + iface["index"] = index + iface["mode"] = subnet["type"] + iface["control"] = subnet.get("control", "auto") + subnet_inet = "inet" if subnet_is_ipv6(subnet): - subnet_inet += '6' + subnet_inet += "6" else: - ipv4_subnet_mtu = subnet.get('mtu') - iface['inet'] = subnet_inet - if (subnet['type'] == 'dhcp4' or subnet['type'] == 'dhcp6' or - subnet['type'] == 'ipv6_dhcpv6-stateful'): + ipv4_subnet_mtu = subnet.get("mtu") + iface["inet"] = subnet_inet + if ( + subnet["type"] == "dhcp4" + or subnet["type"] == "dhcp6" + or subnet["type"] == "ipv6_dhcpv6-stateful" + ): # Configure network settings using DHCP or DHCPv6 - iface['mode'] = 'dhcp' + 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': + 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' + iface["mode"] = "auto" # Use stateless DHCPv6 (0=off, 1=on) - iface['dhcp'] = '1' - elif subnet['type'] == 'ipv6_slaac': + iface["dhcp"] = "1" + elif subnet["type"] == "ipv6_slaac": # Configure network settings using SLAAC from RAs - iface['mode'] = 'auto' + iface["mode"] = "auto" # Use stateless DHCPv6 (0=off, 1=on) - iface['dhcp'] = '0' + iface["dhcp"] = "0" elif subnet_is_ipv6(subnet): # mode might be static6, eni uses 'static' - iface['mode'] = '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' + iface["accept_ra"] = "1" if accept_ra else "0" # do not emit multiple 'auto $IFACE' lines as older (precise) # ifupdown complains - if True in ["auto %s" % (iface['name']) in line - for line in sections]: - iface['control'] = 'alias' + if True in [ + "auto %s" % (iface["name"]) in line for line in sections + ]: + iface["control"] = "alias" lines = list( _iface_start_entry( - iface, index, render_hwaddress=render_hwaddress) + - _iface_add_subnet(iface, subnet) + - _iface_add_attrs(iface, index, ipv4_subnet_mtu) + iface, index, render_hwaddress=render_hwaddress + ) + + _iface_add_subnet(iface, subnet) + + _iface_add_attrs(iface, index, ipv4_subnet_mtu) ) - for route in subnet.get('routes', []): + for route in subnet.get("routes", []): lines.extend(self._render_route(route, indent=" ")) sections.append(lines) else: # ifenslave docs say to auto the slave devices lines = [] - if 'bond-master' in iface or 'bond-slaves' in iface: + if "bond-master" in iface or "bond-slaves" in iface: lines.append("auto {name}".format(**iface)) lines.append("iface {name} {inet} {mode}".format(**iface)) lines.extend( - _iface_add_attrs(iface, index=0, ipv4_subnet_mtu=None)) + _iface_add_attrs(iface, index=0, ipv4_subnet_mtu=None) + ) sections.append(lines) return sections def _render_interfaces(self, network_state, render_hwaddress=False): - '''Given state, emit etc/network/interfaces content.''' + """Given state, emit etc/network/interfaces content.""" # handle 'lo' specifically as we need to insert the global dns entries # there (as that is the only interface that will be always up). - lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet', - 'subnets': [{'type': 'loopback', 'control': 'auto'}]} + lo = { + "name": "lo", + "type": "physical", + "inet": "inet", + "subnets": [{"type": "loopback", "control": "auto"}], + } for iface in network_state.iter_interfaces(): - if iface.get('name') == "lo": + if iface.get("name") == "lo": lo = copy.deepcopy(iface) nameservers = network_state.dns_nameservers if nameservers: - lo['subnets'][0]["dns_nameservers"] = (" ".join(nameservers)) + lo["subnets"][0]["dns_nameservers"] = " ".join(nameservers) searchdomains = network_state.dns_searchdomains if searchdomains: - lo['subnets'][0]["dns_search"] = (" ".join(searchdomains)) + lo["subnets"][0]["dns_search"] = " ".join(searchdomains) # Apply a sort order to ensure that we write out the physical # interfaces first; this is critical for bonding order = { - 'loopback': 0, - 'physical': 1, - 'infiniband': 2, - 'bond': 3, - 'bridge': 4, - 'vlan': 5, + "loopback": 0, + "physical": 1, + "infiniband": 2, + "bond": 3, + "bridge": 4, + "vlan": 5, } sections = [] sections.extend(self._render_iface(lo)) - for iface in sorted(network_state.iter_interfaces(), - key=lambda k: (order[k['type']], k['name'])): + for iface in sorted( + network_state.iter_interfaces(), + key=lambda k: (order[k["type"]], k["name"]), + ): - if iface.get('name') == "lo": + if iface.get("name") == "lo": continue sections.extend( - self._render_iface(iface, render_hwaddress=render_hwaddress)) + self._render_iface(iface, render_hwaddress=render_hwaddress) + ) for route in network_state.iter_routes(): sections.append(self._render_route(route)) - return '\n\n'.join(['\n'.join(s) for s in sections]) + "\n" + return "\n\n".join(["\n".join(s) for s in sections]) + "\n" def render_network_state(self, network_state, templates=None, target=None): fpeni = subp.target_path(target, self.eni_path) @@ -522,34 +570,38 @@ class Renderer(renderer.Renderer): if self.netrules_path: netrules = subp.target_path(target, self.netrules_path) util.ensure_dir(os.path.dirname(netrules)) - util.write_file(netrules, - self._render_persistent_net(network_state)) + util.write_file( + netrules, self._render_persistent_net(network_state) + ) def network_state_to_eni(network_state, header=None, render_hwaddress=False): # render the provided network state, return a string of equivalent eni - eni_path = 'etc/network/interfaces' - renderer = Renderer(config={ - 'eni_path': eni_path, - 'eni_header': header, - 'netrules_path': None, - }) + eni_path = "etc/network/interfaces" + renderer = Renderer( + config={ + "eni_path": eni_path, + "eni_header": header, + "netrules_path": None, + } + ) if not header: header = "" if not header.endswith("\n"): header += "\n" contents = renderer._render_interfaces( - network_state, render_hwaddress=render_hwaddress) + network_state, render_hwaddress=render_hwaddress + ) return header + contents def available(target=None): - expected = ['ifquery', 'ifup', 'ifdown'] - search = ['/sbin', '/usr/sbin'] + expected = ["ifquery", "ifup", "ifdown"] + search = ["/sbin", "/usr/sbin"] for p in expected: if not subp.which(p, search=search, target=target): return False - eni = subp.target_path(target, 'etc/network/interfaces') + eni = subp.target_path(target, "etc/network/interfaces") if not os.path.isfile(eni): return False diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py index 0285dfec..ec42b60c 100644 --- a/cloudinit/net/freebsd.py +++ b/cloudinit/net/freebsd.py @@ -1,59 +1,69 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import log as logging import cloudinit.net.bsd -from cloudinit import subp -from cloudinit import util +from cloudinit import log as logging +from cloudinit import subp, util LOG = logging.getLogger(__name__) class Renderer(cloudinit.net.bsd.BSDRenderer): - def __init__(self, config=None): self._route_cpt = 0 super(Renderer, self).__init__() def rename_interface(self, cur_name, device_name): - self.set_rc_config_value('ifconfig_%s_name' % cur_name, device_name) + self.set_rc_config_value("ifconfig_%s_name" % cur_name, device_name) def write_config(self): for device_name, v in self.interface_configurations.items(): + net_config = "DHCP" if isinstance(v, dict): - self.set_rc_config_value( - 'ifconfig_' + device_name, - v.get('address') + ' netmask ' + v.get('netmask')) - else: - self.set_rc_config_value('ifconfig_' + device_name, 'DHCP') + net_config = v.get("address") + " netmask " + v.get("netmask") + mtu = v.get("mtu") + if mtu: + net_config += " mtu %d" % mtu + self.set_rc_config_value("ifconfig_" + device_name, net_config) def start_services(self, run=False): if not run: LOG.debug("freebsd generate postcmd disabled") return - subp.subp(['service', 'netif', 'restart'], capture=True) + for dhcp_interface in self.dhcp_interfaces(): + # Observed on DragonFlyBSD 6. If we use the "restart" parameter, + # the routes are not recreated. + subp.subp( + ["service", "dhclient", "stop", dhcp_interface], + rcs=[0, 1], + capture=True, + ) + + subp.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. - subp.subp(['service', 'routing', 'restart'], capture=True, rcs=[0, 1]) + subp.subp(["service", "routing", "restart"], capture=True, rcs=[0, 1]) for dhcp_interface in self.dhcp_interfaces(): - subp.subp(['service', 'dhclient', 'restart', dhcp_interface], - rcs=[0, 1], - capture=True) + subp.subp( + ["service", "dhclient", "start", dhcp_interface], + rcs=[0, 1], + capture=True, + ) def set_route(self, network, netmask, gateway): - if network == '0.0.0.0': - self.set_rc_config_value('defaultrouter', gateway) + if network == "0.0.0.0": + self.set_rc_config_value("defaultrouter", gateway) else: - route_name = 'route_net%d' % self._route_cpt + route_name = "route_net%d" % self._route_cpt route_cmd = "-route %s/%s %s" % (network, netmask, gateway) self.set_rc_config_value(route_name, route_cmd) self._route_cpt += 1 def available(target=None): - return util.is_FreeBSD() + return util.is_FreeBSD() or util.is_DragonFlyBSD() diff --git a/cloudinit/net/netbsd.py b/cloudinit/net/netbsd.py index 71b38ee6..3d6b85b7 100644 --- a/cloudinit/net/netbsd.py +++ b/cloudinit/net/netbsd.py @@ -1,43 +1,42 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util import cloudinit.net.bsd +from cloudinit import log as logging +from cloudinit import subp, util LOG = logging.getLogger(__name__) class Renderer(cloudinit.net.bsd.BSDRenderer): - def __init__(self, config=None): super(Renderer, self).__init__() def write_config(self): if self.dhcp_interfaces(): - self.set_rc_config_value('dhcpcd', 'YES') + self.set_rc_config_value("dhcpcd", "YES") self.set_rc_config_value( - 'dhcpcd_flags', - ' '.join(self.dhcp_interfaces()) + "dhcpcd_flags", " ".join(self.dhcp_interfaces()) ) for device_name, v in self.interface_configurations.items(): if isinstance(v, dict): - self.set_rc_config_value( - 'ifconfig_' + device_name, - v.get('address') + ' netmask ' + v.get('netmask')) + net_config = v.get("address") + " netmask " + v.get("netmask") + mtu = v.get("mtu") + if mtu: + net_config += " mtu %d" % mtu + self.set_rc_config_value("ifconfig_" + device_name, net_config) def start_services(self, run=False): if not run: LOG.debug("netbsd generate postcmd disabled") return - subp.subp(['service', 'network', 'restart'], capture=True) + subp.subp(["service", "network", "restart"], capture=True) if self.dhcp_interfaces(): - subp.subp(['service', 'dhcpcd', 'restart'], capture=True) + subp.subp(["service", "dhcpcd", "restart"], capture=True) def set_route(self, network, netmask, gateway): - if network == '0.0.0.0': - self.set_rc_config_value('defaultroute', gateway) + if network == "0.0.0.0": + self.set_rc_config_value("defaultroute", gateway) def available(target=None): diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 53347c83..57ba2d9a 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -3,15 +3,18 @@ import copy import os -from . import renderer -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 subp -from cloudinit import safeyaml +from cloudinit import safeyaml, subp, util from cloudinit.net import SYS_CLASS_NET, get_devicelist +from . import renderer +from .network_state import ( + IPV6_DYNAMIC_TYPES, + NET_CONFIG_TO_V2, + NetworkState, + subnet_is_ipv6, +) + KNOWN_SNAPD_CONFIG = b"""\ # This is the initial network config. # It can be overwritten by cloud-init or console-conf. @@ -32,8 +35,11 @@ LOG = logging.getLogger(__name__) def _get_params_dict_by_match(config, match): - return dict((key, value) for (key, value) in config.items() - if key.startswith(match)) + return dict( + (key, value) + for (key, value) in config.items() + if key.startswith(match) + ) def _extract_addresses(config, entry, ifname, features=None): @@ -73,14 +79,16 @@ def _extract_addresses(config, entry, ifname, features=None): """ - def _listify(obj, token=' '): + def _listify(obj, token=" "): "Helper to convert strings to list of strings, handle single string" if not obj or type(obj) not in [str]: return obj if token in obj: return obj.split(token) else: - return [obj, ] + return [ + obj, + ] if features is None: features = [] @@ -88,78 +96,85 @@ def _extract_addresses(config, entry, ifname, features=None): routes = [] nameservers = [] searchdomains = [] - subnets = config.get('subnets', []) + subnets = config.get("subnets", []) if subnets is None: subnets = [] for subnet in subnets: - sn_type = subnet.get('type') - if sn_type.startswith('dhcp'): - if sn_type == 'dhcp': - sn_type += '4' + sn_type = subnet.get("type") + if sn_type.startswith("dhcp"): + if sn_type == "dhcp": + sn_type += "4" entry.update({sn_type: True}) 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') - if 'gateway' in subnet and subnet.get('gateway'): - gateway = subnet.get('gateway') + entry.update({"dhcp6": True}) + elif sn_type in ["static", "static6"]: + addr = "%s" % subnet.get("address") + if "prefix" in subnet: + addr += "/%d" % subnet.get("prefix") + if "gateway" in subnet and subnet.get("gateway"): + gateway = subnet.get("gateway") if ":" in gateway: - entry.update({'gateway6': gateway}) + entry.update({"gateway6": gateway}) else: - entry.update({'gateway4': gateway}) - if 'dns_nameservers' in subnet: - nameservers += _listify(subnet.get('dns_nameservers', [])) - if 'dns_search' in subnet: - searchdomains += _listify(subnet.get('dns_search', [])) - if 'mtu' in subnet: - mtukey = 'mtu' - 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'), - route.get('prefix')) + entry.update({"gateway4": gateway}) + if "dns_nameservers" in subnet: + nameservers += _listify(subnet.get("dns_nameservers", [])) + if "dns_search" in subnet: + searchdomains += _listify(subnet.get("dns_search", [])) + if "mtu" in subnet: + mtukey = "mtu" + 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"), route.get("prefix")) new_route = { - 'via': route.get('gateway'), - 'to': to_net, + "via": route.get("gateway"), + "to": to_net, } - if 'metric' in route: - new_route.update({'metric': route.get('metric', 100)}) + if "metric" in route: + new_route.update({"metric": route.get("metric", 100)}) routes.append(new_route) addresses.append(addr) - if 'mtu' in config: - entry_mtu = entry.get('mtu') - if entry_mtu and config['mtu'] != entry_mtu: + if "mtu" in config: + entry_mtu = entry.get("mtu") + if entry_mtu and config["mtu"] != entry_mtu: LOG.warning( "Network config: ignoring %s device-level mtu:%s because" " ipv4 subnet-level mtu:%s provided.", - ifname, config['mtu'], entry_mtu) + ifname, + config["mtu"], + entry_mtu, + ) else: - entry['mtu'] = config['mtu'] + entry["mtu"] = config["mtu"] if len(addresses) > 0: - entry.update({'addresses': addresses}) + entry.update({"addresses": addresses}) if len(routes) > 0: - entry.update({'routes': routes}) + entry.update({"routes": routes}) if len(nameservers) > 0: - ns = {'addresses': nameservers} - entry.update({'nameservers': ns}) + ns = {"addresses": nameservers} + entry.update({"nameservers": ns}) if len(searchdomains) > 0: - 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'))}) + 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): - bond_slave_names = sorted([name for (name, cfg) in interfaces.items() - if cfg.get('bond-master', None) == bond_master]) + bond_slave_names = sorted( + [ + name + for (name, cfg) in interfaces.items() + if cfg.get("bond-master", None) == bond_master + ] + ) if len(bond_slave_names) > 0: - entry.update({'interfaces': bond_slave_names}) + entry.update({"interfaces": bond_slave_names}) def _clean_default(target=None): @@ -172,13 +187,20 @@ def _clean_default(target=None): if content != KNOWN_SNAPD_CONFIG: return - derived = [subp.target_path(target, f) for f in ( - 'run/systemd/network/10-netplan-all-en.network', - 'run/systemd/network/10-netplan-all-eth.network', - 'run/systemd/generator/netplan.stamp')] + derived = [ + subp.target_path(target, f) + for f in ( + "run/systemd/network/10-netplan-all-en.network", + "run/systemd/network/10-netplan-all-eth.network", + "run/systemd/generator/netplan.stamp", + ) + ] existing = [f for f in derived if os.path.isfile(f)] - LOG.debug("removing known config '%s' and derived existing files: %s", - tpath, existing) + LOG.debug( + "removing known config '%s' and derived existing files: %s", + tpath, + existing, + ) for f in [tpath] + existing: os.unlink(f) @@ -187,18 +209,19 @@ def _clean_default(target=None): class Renderer(renderer.Renderer): """Renders network information in a /etc/netplan/network.yaml format.""" - NETPLAN_GENERATE = ['netplan', 'generate'] - NETPLAN_INFO = ['netplan', 'info'] + NETPLAN_GENERATE = ["netplan", "generate"] + NETPLAN_INFO = ["netplan", "info"] def __init__(self, config=None): if not config: config = {} - self.netplan_path = config.get('netplan_path', - 'etc/netplan/50-cloud-init.yaml') - 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) + self.netplan_path = config.get( + "netplan_path", "etc/netplan/50-cloud-init.yaml" + ) + 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): @@ -206,13 +229,13 @@ class Renderer(renderer.Renderer): try: info_blob, _err = subp.subp(self.NETPLAN_INFO, capture=True) info = util.load_yaml(info_blob) - self._features = info['netplan.io']['features'] + self._features = info["netplan.io"]["features"] except subp.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) + 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): @@ -244,26 +267,30 @@ class Renderer(renderer.Renderer): def _net_setup_link(self, run=False): """To ensure device link properties are applied, we poke - udev to re-evaluate networkd .link files and call - the setup_link udev builtin command + udev to re-evaluate networkd .link files and call + the setup_link udev builtin command """ if not run: LOG.debug("netplan net_setup_link postcmd disabled") return - setup_lnk = ['udevadm', 'test-builtin', 'net_setup_link'] - for cmd in [setup_lnk + [SYS_CLASS_NET + iface] - for iface in get_devicelist() if - os.path.islink(SYS_CLASS_NET + iface)]: + setup_lnk = ["udevadm", "test-builtin", "net_setup_link"] + for cmd in [ + setup_lnk + [SYS_CLASS_NET + iface] + for iface in get_devicelist() + if os.path.islink(SYS_CLASS_NET + iface) + ]: subp.subp(cmd, capture=True) - def _render_content(self, network_state): + def _render_content(self, network_state: NetworkState): # if content already in netplan format, pass it back if network_state.version == 2: - LOG.debug('V2 to V2 passthrough') - return safeyaml.dumps({'network': network_state.config}, - explicit_start=False, - explicit_end=False) + LOG.debug("V2 to V2 passthrough") + return safeyaml.dumps( + {"network": network_state.config}, + explicit_start=False, + explicit_end=False, + ) ethernets = {} wifis = {} @@ -272,80 +299,83 @@ class Renderer(renderer.Renderer): vlans = {} content = [] - interfaces = network_state._network_state.get('interfaces', []) + interfaces = network_state._network_state.get("interfaces", []) nameservers = network_state.dns_nameservers searchdomains = network_state.dns_searchdomains for config in network_state.iter_interfaces(): - ifname = config.get('name') + ifname = config.get("name") # filter None (but not False) entries up front - ifcfg = dict((key, value) for (key, value) in config.items() - if value is not None) - - if_type = ifcfg.get('type') - if if_type == 'physical': + ifcfg = dict( + (key, value) + for (key, value) in config.items() + if value is not None + ) + + if_type = ifcfg.get("type") + if if_type == "physical": # required_keys = ['name', 'mac_address'] eth = { - 'set-name': ifname, - 'match': ifcfg.get('match', None), + "set-name": ifname, + "match": ifcfg.get("match", None), } - if eth['match'] is None: - macaddr = ifcfg.get('mac_address', None) + if eth["match"] is None: + macaddr = ifcfg.get("mac_address", None) if macaddr is not None: - eth['match'] = {'macaddress': macaddr.lower()} + eth["match"] = {"macaddress": macaddr.lower()} else: - del eth['match'] - del eth['set-name'] + del eth["match"] + del eth["set-name"] _extract_addresses(ifcfg, eth, ifname, self.features) ethernets.update({ifname: eth}) - elif if_type == 'bond': + elif if_type == "bond": # required_keys = ['name', 'bond_interfaces'] bond = {} bond_config = {} # extract bond params and drop the bond_ prefix as it's # redundent in v2 yaml format - v2_bond_map = NET_CONFIG_TO_V2.get('bond') - for match in ['bond_', 'bond-']: + v2_bond_map = NET_CONFIG_TO_V2.get("bond") + for match in ["bond_", "bond-"]: bond_params = _get_params_dict_by_match(ifcfg, match) for (param, value) in bond_params.items(): - newname = v2_bond_map.get(param.replace('_', '-')) + newname = v2_bond_map.get(param.replace("_", "-")) if newname is None: continue bond_config.update({newname: value}) if len(bond_config) > 0: - bond.update({'parameters': bond_config}) - if ifcfg.get('mac_address'): - bond['macaddress'] = ifcfg.get('mac_address').lower() - slave_interfaces = ifcfg.get('bond-slaves') - if slave_interfaces == 'none': + bond.update({"parameters": bond_config}) + if ifcfg.get("mac_address"): + bond["macaddress"] = ifcfg.get("mac_address").lower() + slave_interfaces = ifcfg.get("bond-slaves") + if slave_interfaces == "none": _extract_bond_slaves_by_name(interfaces, bond, ifname) _extract_addresses(ifcfg, bond, ifname, self.features) bonds.update({ifname: bond}) - elif if_type == 'bridge': + elif if_type == "bridge": # required_keys = ['name', 'bridge_ports'] - ports = sorted(copy.copy(ifcfg.get('bridge_ports'))) + ports = sorted(copy.copy(ifcfg.get("bridge_ports"))) bridge = { - 'interfaces': ports, + "interfaces": ports, } # extract bridge params and drop the bridge prefix as it's # redundent in v2 yaml format - match_prefix = 'bridge_' + match_prefix = "bridge_" params = _get_params_dict_by_match(ifcfg, match_prefix) br_config = {} # v2 yaml uses different names for the keys # and at least one value format change - v2_bridge_map = NET_CONFIG_TO_V2.get('bridge') + v2_bridge_map = NET_CONFIG_TO_V2.get("bridge") for (param, value) in params.items(): newname = v2_bridge_map.get(param) if newname is None: continue br_config.update({newname: value}) - if newname in ['path-cost', 'port-priority']: + if newname in ["path-cost", "port-priority"]: # <interface> <value> -> <interface>: int(<value>) newvalue = {} for val in value: @@ -354,58 +384,60 @@ class Renderer(renderer.Renderer): br_config.update({newname: newvalue}) if len(br_config) > 0: - bridge.update({'parameters': br_config}) - if ifcfg.get('mac_address'): - bridge['macaddress'] = ifcfg.get('mac_address').lower() + bridge.update({"parameters": br_config}) + if ifcfg.get("mac_address"): + bridge["macaddress"] = ifcfg.get("mac_address").lower() _extract_addresses(ifcfg, bridge, ifname, self.features) bridges.update({ifname: bridge}) - elif if_type == 'vlan': + elif if_type == "vlan": # required_keys = ['name', 'vlan_id', 'vlan-raw-device'] vlan = { - 'id': ifcfg.get('vlan_id'), - 'link': ifcfg.get('vlan-raw-device') + "id": ifcfg.get("vlan_id"), + "link": ifcfg.get("vlan-raw-device"), } - macaddr = ifcfg.get('mac_address', None) + macaddr = ifcfg.get("mac_address", None) if macaddr is not None: - vlan['macaddress'] = macaddr.lower() + vlan["macaddress"] = macaddr.lower() _extract_addresses(ifcfg, vlan, ifname, self.features) vlans.update({ifname: vlan}) # inject global nameserver values under each all interface which # has addresses and do not already have a DNS configuration if nameservers or searchdomains: - nscfg = {'addresses': nameservers, 'search': searchdomains} + nscfg = {"addresses": nameservers, "search": searchdomains} for section in [ethernets, wifis, bonds, bridges, vlans]: for _name, cfg in section.items(): - if 'nameservers' in cfg or 'addresses' not in cfg: + if "nameservers" in cfg or "addresses" not in cfg: continue - cfg.update({'nameservers': nscfg}) + cfg.update({"nameservers": nscfg}) # workaround yaml dictionary key sorting when dumping def _render_section(name, section): if section: - dump = safeyaml.dumps({name: section}, - explicit_start=False, - explicit_end=False, - noalias=True) - txt = util.indent(dump, ' ' * 4) + dump = safeyaml.dumps( + {name: section}, + explicit_start=False, + explicit_end=False, + noalias=True, + ) + txt = util.indent(dump, " " * 4) return [txt] return [] content.append("network:\n version: 2\n") - content += _render_section('ethernets', ethernets) - content += _render_section('wifis', wifis) - content += _render_section('bonds', bonds) - content += _render_section('bridges', bridges) - content += _render_section('vlans', vlans) + content += _render_section("ethernets", ethernets) + content += _render_section("wifis", wifis) + content += _render_section("bonds", bonds) + content += _render_section("bridges", bridges) + content += _render_section("vlans", vlans) return "".join(content) def available(target=None): - expected = ['netplan'] - search = ['/usr/sbin', '/sbin'] + expected = ["netplan"] + search = ["/usr/sbin", "/sbin"] for p in expected: if not subp.which(p, search=search, target=target): return False @@ -414,11 +446,13 @@ def available(target=None): def network_state_to_netplan(network_state, header=None): # render the provided network state, return a string of equivalent eni - netplan_path = 'etc/network/50-cloud-init.yaml' - renderer = Renderer({ - 'netplan_path': netplan_path, - 'netplan_header': header, - }) + netplan_path = "etc/network/50-cloud-init.yaml" + renderer = Renderer( + { + "netplan_path": netplan_path, + "netplan_header": header, + } + ) if not header: header = "" if not header.endswith("\n"): @@ -426,4 +460,5 @@ def network_state_to_netplan(network_state, header=None): contents = renderer._render_content(network_state) return header + contents + # vi: ts=4 expandtab diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index e8bf9e39..7bac8adf 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -6,88 +6,75 @@ import copy import functools +import ipaddress import logging import socket import struct -from cloudinit import safeyaml -from cloudinit import util +from cloudinit import safeyaml, util LOG = logging.getLogger(__name__) NETWORK_STATE_VERSION = 1 -IPV6_DYNAMIC_TYPES = ['dhcp6', - 'ipv6_slaac', - 'ipv6_dhcpv6-stateless', - 'ipv6_dhcpv6-stateful'] +IPV6_DYNAMIC_TYPES = [ + "dhcp6", + "ipv6_slaac", + "ipv6_dhcpv6-stateless", + "ipv6_dhcpv6-stateful", +] NETWORK_STATE_REQUIRED_KEYS = { - 1: ['version', 'config', 'network_state'], + 1: ["version", "config", "network_state"], } NETWORK_V2_KEY_FILTER = [ - 'addresses', 'dhcp4', 'dhcp4-overrides', 'dhcp6', 'dhcp6-overrides', - 'gateway4', 'gateway6', 'interfaces', 'match', 'mtu', 'nameservers', - 'renderer', 'set-name', 'wakeonlan', 'accept-ra' + "addresses", + "dhcp4", + "dhcp4-overrides", + "dhcp6", + "dhcp6-overrides", + "gateway4", + "gateway6", + "interfaces", + "match", + "mtu", + "nameservers", + "renderer", + "set-name", + "wakeonlan", + "accept-ra", ] NET_CONFIG_TO_V2 = { - 'bond': {'bond-ad-select': 'ad-select', - 'bond-arp-interval': 'arp-interval', - 'bond-arp-ip-target': 'arp-ip-target', - 'bond-arp-validate': 'arp-validate', - 'bond-downdelay': 'down-delay', - 'bond-fail-over-mac': 'fail-over-mac-policy', - 'bond-lacp-rate': 'lacp-rate', - 'bond-miimon': 'mii-monitor-interval', - 'bond-min-links': 'min-links', - 'bond-mode': 'mode', - 'bond-num-grat-arp': 'gratuitious-arp', - 'bond-primary': 'primary', - 'bond-primary-reselect': 'primary-reselect-policy', - 'bond-updelay': 'up-delay', - 'bond-xmit-hash-policy': 'transmit-hash-policy'}, - 'bridge': {'bridge_ageing': 'ageing-time', - 'bridge_bridgeprio': 'priority', - 'bridge_fd': 'forward-delay', - 'bridge_gcint': None, - 'bridge_hello': 'hello-time', - 'bridge_maxage': 'max-age', - 'bridge_maxwait': None, - 'bridge_pathcost': 'path-cost', - 'bridge_portprio': 'port-priority', - 'bridge_stp': 'stp', - 'bridge_waitport': None}} - - -def parse_net_config_data(net_config, skip_broken=True): - """Parses the config, returns NetworkState object - - :param net_config: curtin network config dict - """ - state = None - version = net_config.get('version') - config = net_config.get('config') - if version == 2: - # v2 does not have explicit 'config' key so we - # pass the whole net-config as-is - config = net_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() - - return state - - -def parse_net_config(path, skip_broken=True): - """Parses a curtin network configuration file and - return network state""" - ns = None - net_config = util.read_conf(path) - if 'network' in net_config: - ns = parse_net_config_data(net_config.get('network'), - skip_broken=skip_broken) - return ns + "bond": { + "bond-ad-select": "ad-select", + "bond-arp-interval": "arp-interval", + "bond-arp-ip-target": "arp-ip-target", + "bond-arp-validate": "arp-validate", + "bond-downdelay": "down-delay", + "bond-fail-over-mac": "fail-over-mac-policy", + "bond-lacp-rate": "lacp-rate", + "bond-miimon": "mii-monitor-interval", + "bond-min-links": "min-links", + "bond-mode": "mode", + "bond-num-grat-arp": "gratuitious-arp", + "bond-primary": "primary", + "bond-primary-reselect": "primary-reselect-policy", + "bond-updelay": "up-delay", + "bond-xmit-hash-policy": "transmit-hash-policy", + }, + "bridge": { + "bridge_ageing": "ageing-time", + "bridge_bridgeprio": "priority", + "bridge_fd": "forward-delay", + "bridge_gcint": None, + "bridge_hello": "hello-time", + "bridge_maxage": "max-age", + "bridge_maxwait": None, + "bridge_pathcost": "path-cost", + "bridge_portprio": "port-priority", + "bridge_stp": "stp", + "bridge_waitport": None, + }, +} def from_state_file(state_file): @@ -109,17 +96,16 @@ class InvalidCommand(Exception): def ensure_command_keys(required_keys): - def wrapper(func): - @functools.wraps(func) def decorator(self, command, *args, **kwargs): if required_keys: missing_keys = diff_keys(required_keys, command) if missing_keys: - raise InvalidCommand("Command missing %s of required" - " keys %s" % (missing_keys, - required_keys)) + raise InvalidCommand( + "Command missing %s of required keys %s" + % (missing_keys, required_keys) + ) return func(self, command, *args, **kwargs) return decorator @@ -134,29 +120,28 @@ class CommandHandlerMeta(type): 'handle_' and on finding those will populate a class attribute mapping so that those methods can be quickly located and called. """ + def __new__(cls, name, parents, dct): command_handlers = {} for attr_name, attr in dct.items(): - if callable(attr) and attr_name.startswith('handle_'): - handles_what = attr_name[len('handle_'):] + if callable(attr) and attr_name.startswith("handle_"): + handles_what = attr_name[len("handle_") :] if handles_what: command_handlers[handles_what] = attr - dct['command_handlers'] = command_handlers - return super(CommandHandlerMeta, cls).__new__(cls, name, - parents, dct) + dct["command_handlers"] = command_handlers + return super(CommandHandlerMeta, cls).__new__(cls, name, parents, dct) class NetworkState(object): - def __init__(self, network_state, version=NETWORK_STATE_VERSION): self._network_state = copy.deepcopy(network_state) self._version = version - self.use_ipv6 = network_state.get('use_ipv6', False) + self.use_ipv6 = network_state.get("use_ipv6", False) self._has_default_route = None @property def config(self): - return self._network_state['config'] + return self._network_state["config"] @property def version(self): @@ -165,14 +150,14 @@ class NetworkState(object): @property def dns_nameservers(self): try: - return self._network_state['dns']['nameservers'] + return self._network_state["dns"]["nameservers"] except KeyError: return [] @property def dns_searchdomains(self): try: - return self._network_state['dns']['search'] + return self._network_state["dns"]["search"] except KeyError: return [] @@ -183,7 +168,7 @@ class NetworkState(object): return self._has_default_route def iter_interfaces(self, filter_func=None): - ifaces = self._network_state.get('interfaces', {}) + ifaces = self._network_state.get("interfaces", {}) for iface in ifaces.values(): if filter_func is None: yield iface @@ -192,7 +177,7 @@ class NetworkState(object): yield iface def iter_routes(self, filter_func=None): - for route in self._network_state.get('routes', []): + for route in self._network_state.get("routes", []): if filter_func is not None: if filter_func(route): yield route @@ -204,39 +189,39 @@ class NetworkState(object): 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', []): + 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') + default_nets = ("::", "0.0.0.0") return ( - route.get('prefix') == 0 - and route.get('network') in default_nets + route.get("prefix") == 0 and route.get("network") in default_nets ) class NetworkStateInterpreter(metaclass=CommandHandlerMeta): initial_network_state = { - 'interfaces': {}, - 'routes': [], - 'dns': { - 'nameservers': [], - 'search': [], + "interfaces": {}, + "routes": [], + "dns": { + "nameservers": [], + "search": [], }, - 'use_ipv6': False, - 'config': None, + "use_ipv6": False, + "config": None, } def __init__(self, version=NETWORK_STATE_VERSION, config=None): self._version = version self._config = config self._network_state = copy.deepcopy(self.initial_network_state) - self._network_state['config'] = config + self._network_state["config"] = config self._parsed = False + self._interface_dns_map = {} @property def network_state(self): @@ -244,41 +229,41 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): @property def use_ipv6(self): - return self._network_state.get('use_ipv6') + return self._network_state.get("use_ipv6") @use_ipv6.setter def use_ipv6(self, val): - self._network_state.update({'use_ipv6': val}) + self._network_state.update({"use_ipv6": val}) def dump(self): state = { - 'version': self._version, - 'config': self._config, - 'network_state': self._network_state, + "version": self._version, + "config": self._config, + "network_state": self._network_state, } return safeyaml.dumps(state) def load(self, state): - if 'version' not in state: - LOG.error('Invalid state, missing version field') - raise ValueError('Invalid state, missing version field') + if "version" not in state: + LOG.error("Invalid state, missing version field") + raise ValueError("Invalid state, missing version field") - required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']] + required_keys = NETWORK_STATE_REQUIRED_KEYS[state["version"]] missing_keys = diff_keys(required_keys, state) if missing_keys: - msg = 'Invalid state, missing keys: %s' % (missing_keys) + msg = "Invalid state, missing keys: %s" % (missing_keys) LOG.error(msg) raise ValueError(msg) # v1 - direct attr mapping, except version - for key in [k for k in required_keys if k not in ['version']]: + for key in [k for k in required_keys if k not in ["version"]]: setattr(self, key, state[key]) def dump_network_state(self): return safeyaml.dumps(self._network_state) def as_dict(self): - return {'version': self._version, 'config': self._config} + return {"version": self._version, "config": self._config} def get_network_state(self): ns = self.network_state @@ -294,7 +279,7 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): def parse_config_v1(self, skip_broken=True): for command in self._config: - command_type = command['type'] + command_type = command["type"] try: handler = self.command_handlers[command_type] except KeyError as e: @@ -307,13 +292,29 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): if not skip_broken: raise else: - LOG.warning("Skipping invalid command: %s", command, - exc_info=True) + LOG.warning( + "Skipping invalid command: %s", command, exc_info=True + ) LOG.debug(self.dump_network_state()) + for interface, dns in self._interface_dns_map.items(): + iface = None + try: + iface = self._network_state["interfaces"][interface] + except KeyError as e: + raise ValueError( + "Nameserver specified for interface {0}, " + "but interface {0} does not exist!".format(interface) + ) from e + if iface: + nameservers, search = dns + iface["dns"] = { + "addresses": nameservers, + "search": search, + } def parse_config_v2(self, skip_broken=True): for command_type, command in self._config.items(): - if command_type in ['version', 'renderer']: + if command_type in ["version", "renderer"]: continue try: handler = self.command_handlers[command_type] @@ -328,17 +329,18 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): if not skip_broken: raise else: - LOG.warning("Skipping invalid command: %s", command, - exc_info=True) + LOG.warning( + "Skipping invalid command: %s", command, exc_info=True + ) LOG.debug(self.dump_network_state()) - @ensure_command_keys(['name']) + @ensure_command_keys(["name"]) def handle_loopback(self, command): return self.handle_physical(command) - @ensure_command_keys(['name']) + @ensure_command_keys(["name"]) def handle_physical(self, command): - ''' + """ command = { 'type': 'physical', 'mac_address': 'c0:d6:9f:2c:e8:80', @@ -348,119 +350,122 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): ], 'accept-ra': 'true' } - ''' + """ - interfaces = self._network_state.get('interfaces', {}) - iface = interfaces.get(command['name'], {}) - for param, val in command.get('params', {}).items(): + interfaces = self._network_state.get("interfaces", {}) + iface = interfaces.get(command["name"], {}) + for param, val in command.get("params", {}).items(): iface.update({param: val}) # convert subnet ipv6 netmask to cidr as needed - subnets = _normalize_subnets(command.get('subnets')) + subnets = _normalize_subnets(command.get("subnets")) # automatically set 'use_ipv6' if any addresses are ipv6 if not self.use_ipv6: for subnet in subnets: - if (subnet.get('type').endswith('6') or - is_ipv6_addr(subnet.get('address'))): + if subnet.get("type").endswith("6") or is_ipv6_addr( + subnet.get("address") + ): self.use_ipv6 = True break - accept_ra = command.get('accept-ra', None) + accept_ra = command.get("accept-ra", None) if accept_ra is not None: accept_ra = util.is_true(accept_ra) - wakeonlan = command.get('wakeonlan', None) + wakeonlan = command.get("wakeonlan", None) if wakeonlan is not None: wakeonlan = util.is_true(wakeonlan) - iface.update({ - 'name': command.get('name'), - 'type': command.get('type'), - 'mac_address': command.get('mac_address'), - 'inet': 'inet', - 'mode': 'manual', - 'mtu': command.get('mtu'), - 'address': None, - 'gateway': None, - 'subnets': subnets, - 'accept-ra': accept_ra, - 'wakeonlan': wakeonlan, - }) - self._network_state['interfaces'].update({command.get('name'): iface}) + iface.update( + { + "name": command.get("name"), + "type": command.get("type"), + "mac_address": command.get("mac_address"), + "inet": "inet", + "mode": "manual", + "mtu": command.get("mtu"), + "address": None, + "gateway": None, + "subnets": subnets, + "accept-ra": accept_ra, + "wakeonlan": wakeonlan, + } + ) + self._network_state["interfaces"].update({command.get("name"): iface}) self.dump_network_state() - @ensure_command_keys(['name', 'vlan_id', 'vlan_link']) + @ensure_command_keys(["name", "vlan_id", "vlan_link"]) def handle_vlan(self, command): - ''' - auto eth0.222 - iface eth0.222 inet static - address 10.10.10.1 - netmask 255.255.255.0 - hwaddress ether BC:76:4E:06:96:B3 - vlan-raw-device eth0 - ''' - interfaces = self._network_state.get('interfaces', {}) + """ + auto eth0.222 + iface eth0.222 inet static + address 10.10.10.1 + netmask 255.255.255.0 + hwaddress ether BC:76:4E:06:96:B3 + vlan-raw-device eth0 + """ + interfaces = self._network_state.get("interfaces", {}) self.handle_physical(command) - iface = interfaces.get(command.get('name'), {}) - iface['vlan-raw-device'] = command.get('vlan_link') - iface['vlan_id'] = command.get('vlan_id') - interfaces.update({iface['name']: iface}) + iface = interfaces.get(command.get("name"), {}) + iface["vlan-raw-device"] = command.get("vlan_link") + iface["vlan_id"] = command.get("vlan_id") + interfaces.update({iface["name"]: iface}) - @ensure_command_keys(['name', 'bond_interfaces', 'params']) + @ensure_command_keys(["name", "bond_interfaces", "params"]) def handle_bond(self, command): - ''' - #/etc/network/interfaces - auto eth0 - iface eth0 inet manual - bond-master bond0 - bond-mode 802.3ad - - auto eth1 - iface eth1 inet manual - bond-master bond0 - bond-mode 802.3ad - - auto bond0 - iface bond0 inet static - address 192.168.0.10 - gateway 192.168.0.1 - netmask 255.255.255.0 - bond-slaves none - bond-mode 802.3ad - bond-miimon 100 - bond-downdelay 200 - bond-updelay 200 - bond-lacp-rate 4 - ''' + """ + #/etc/network/interfaces + auto eth0 + iface eth0 inet manual + bond-master bond0 + bond-mode 802.3ad + + auto eth1 + iface eth1 inet manual + bond-master bond0 + bond-mode 802.3ad + + auto bond0 + iface bond0 inet static + address 192.168.0.10 + gateway 192.168.0.1 + netmask 255.255.255.0 + bond-slaves none + bond-mode 802.3ad + bond-miimon 100 + bond-downdelay 200 + bond-updelay 200 + bond-lacp-rate 4 + """ self.handle_physical(command) - interfaces = self._network_state.get('interfaces') - iface = interfaces.get(command.get('name'), {}) - for param, val in command.get('params').items(): + interfaces = self._network_state.get("interfaces") + iface = interfaces.get(command.get("name"), {}) + for param, val in command.get("params").items(): iface.update({param: val}) - iface.update({'bond-slaves': 'none'}) - self._network_state['interfaces'].update({iface['name']: iface}) + iface.update({"bond-slaves": "none"}) + self._network_state["interfaces"].update({iface["name"]: iface}) # handle bond slaves - for ifname in command.get('bond_interfaces'): + for ifname in command.get("bond_interfaces"): if ifname not in interfaces: cmd = { - 'name': ifname, - 'type': 'bond', + "name": ifname, + "type": "bond", } # inject placeholder self.handle_physical(cmd) - interfaces = self._network_state.get('interfaces', {}) + interfaces = self._network_state.get("interfaces", {}) bond_if = interfaces.get(ifname) - bond_if['bond-master'] = command.get('name') + bond_if["bond-master"] = command.get("name") # copy in bond config into slave - for param, val in command.get('params').items(): + for param, val in command.get("params").items(): bond_if.update({param: val}) - self._network_state['interfaces'].update({ifname: bond_if}) + self._network_state["interfaces"].update({ifname: bond_if}) - @ensure_command_keys(['name', 'bridge_interfaces']) + @ensure_command_keys(["name", "bridge_interfaces"]) def handle_bridge(self, command): - ''' + """ auto br0 iface br0 inet static address 10.10.10.1 @@ -485,70 +490,91 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): "bridge_stp", "bridge_waitport", ] - ''' + """ # find one of the bridge port ifaces to get mac_addr # handle bridge_slaves - interfaces = self._network_state.get('interfaces', {}) - for ifname in command.get('bridge_interfaces'): + interfaces = self._network_state.get("interfaces", {}) + for ifname in command.get("bridge_interfaces"): if ifname in interfaces: continue cmd = { - 'name': ifname, + "name": ifname, } # inject placeholder self.handle_physical(cmd) - interfaces = self._network_state.get('interfaces', {}) + interfaces = self._network_state.get("interfaces", {}) self.handle_physical(command) - iface = interfaces.get(command.get('name'), {}) - iface['bridge_ports'] = command['bridge_interfaces'] - for param, val in command.get('params', {}).items(): + iface = interfaces.get(command.get("name"), {}) + iface["bridge_ports"] = command["bridge_interfaces"] + for param, val in command.get("params", {}).items(): iface.update({param: val}) # convert value to boolean - bridge_stp = iface.get('bridge_stp') + bridge_stp = iface.get("bridge_stp") if bridge_stp is not None and type(bridge_stp) != bool: - if bridge_stp in ['on', '1', 1]: + if bridge_stp in ["on", "1", 1]: bridge_stp = True - elif bridge_stp in ['off', '0', 0]: + elif bridge_stp in ["off", "0", 0]: bridge_stp = False else: raise ValueError( - 'Cannot convert bridge_stp value ({stp}) to' - ' boolean'.format(stp=bridge_stp)) - iface.update({'bridge_stp': bridge_stp}) + "Cannot convert bridge_stp value ({stp}) to" + " boolean".format(stp=bridge_stp) + ) + iface.update({"bridge_stp": bridge_stp}) - interfaces.update({iface['name']: iface}) + interfaces.update({iface["name"]: iface}) - @ensure_command_keys(['name']) + @ensure_command_keys(["name"]) def handle_infiniband(self, command): self.handle_physical(command) - @ensure_command_keys(['address']) - def handle_nameserver(self, command): - dns = self._network_state.get('dns') - if 'address' in command: - addrs = command['address'] + def _parse_dns(self, command): + nameservers = [] + search = [] + if "address" in command: + addrs = command["address"] if not type(addrs) == list: addrs = [addrs] for addr in addrs: - dns['nameservers'].append(addr) - if 'search' in command: - paths = command['search'] + nameservers.append(addr) + if "search" in command: + paths = command["search"] if not isinstance(paths, list): paths = [paths] for path in paths: - dns['search'].append(path) + search.append(path) + return nameservers, search - @ensure_command_keys(['destination']) + @ensure_command_keys(["address"]) + def handle_nameserver(self, command): + dns = self._network_state.get("dns") + nameservers, search = self._parse_dns(command) + if "interface" in command: + self._interface_dns_map[command["interface"]] = ( + nameservers, + search, + ) + else: + dns["nameservers"].extend(nameservers) + dns["search"].extend(search) + + @ensure_command_keys(["address"]) + def _handle_individual_nameserver(self, command, iface): + _iface = self._network_state.get("interfaces") + nameservers, search = self._parse_dns(command) + _iface[iface]["dns"] = {"nameservers": nameservers, "search": search} + + @ensure_command_keys(["destination"]) def handle_route(self, command): - self._network_state['routes'].append(_normalize_route(command)) + self._network_state["routes"].append(_normalize_route(command)) # V2 handlers def handle_bonds(self, command): - ''' + """ v2_command = { bond0: { 'interfaces': ['interface0', 'interface1'], @@ -575,12 +601,12 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): } } - ''' - self._handle_bond_bridge(command, cmd_type='bond') + """ + self._handle_bond_bridge(command, cmd_type="bond") def handle_bridges(self, command): - ''' + """ v2_command = { br0: { 'interfaces': ['interface0', 'interface1'], @@ -601,11 +627,11 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): } } - ''' - self._handle_bond_bridge(command, cmd_type='bridge') + """ + self._handle_bond_bridge(command, cmd_type="bridge") def handle_ethernets(self, command): - ''' + """ ethernets: eno1: match: @@ -641,34 +667,38 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): {'type': 'dhcp4'} ] } - ''' + """ for eth, cfg in command.items(): phy_cmd = { - 'type': 'physical', - 'name': cfg.get('set-name', eth), + "type": "physical", + "name": cfg.get("set-name", eth), } - match = cfg.get('match', {}) - mac_address = 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['mac_address'] = mac_address - driver = match.get('driver', None) + LOG.debug( + 'NetworkState Version2: missing "macaddress" info ' + "in config entry: %s: %s", + eth, + str(cfg), + ) + 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']: + phy_cmd["params"] = {"driver": driver} + for key in ["mtu", "match", "wakeonlan", "accept-ra"]: if key in cfg: phy_cmd[key] = cfg[key] subnets = self._v2_to_v1_ipcfg(cfg) if len(subnets) > 0: - phy_cmd.update({'subnets': subnets}) + phy_cmd.update({"subnets": subnets}) - LOG.debug('v2(ethernets) -> v1(physical):\n%s', phy_cmd) + LOG.debug("v2(ethernets) -> v1(physical):\n%s", phy_cmd) self.handle_physical(phy_cmd) def handle_vlans(self, command): - ''' + """ v2_vlans = { 'eth0.123': { 'id': 123, @@ -684,135 +714,154 @@ class NetworkStateInterpreter(metaclass=CommandHandlerMeta): 'vlan_id': 123, 'subnets': [{'type': 'dhcp4'}], } - ''' + """ for vlan, cfg in command.items(): vlan_cmd = { - 'type': 'vlan', - 'name': vlan, - 'vlan_id': cfg.get('id'), - 'vlan_link': cfg.get('link'), + "type": "vlan", + "name": vlan, + "vlan_id": cfg.get("id"), + "vlan_link": cfg.get("link"), } - if 'mtu' in cfg: - vlan_cmd['mtu'] = cfg['mtu'] + 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}) - LOG.debug('v2(vlans) -> v1(vlan):\n%s', vlan_cmd) + vlan_cmd.update({"subnets": subnets}) + LOG.debug("v2(vlans) -> v1(vlan):\n%s", vlan_cmd) self.handle_vlan(vlan_cmd) def handle_wifis(self, command): - LOG.warning('Wifi configuration is only available to distros with' - ' netplan rendering support.') + LOG.warning( + "Wifi configuration is only available to distros with" + " netplan rendering support." + ) def _v2_common(self, cfg): - LOG.debug('v2_common: handling config:\n%s', cfg) - if 'nameservers' in cfg: - search = cfg.get('nameservers').get('search', []) - dns = cfg.get('nameservers').get('addresses', []) - name_cmd = {'type': 'nameserver'} - if len(search) > 0: - name_cmd.update({'search': search}) - if len(dns) > 0: - name_cmd.update({'addresses': dns}) - LOG.debug('v2(nameserver) -> v1(nameserver):\n%s', name_cmd) - self.handle_nameserver(name_cmd) + LOG.debug("v2_common: handling config:\n%s", cfg) + for iface, dev_cfg in cfg.items(): + if "set-name" in dev_cfg: + set_name_iface = dev_cfg.get("set-name") + if set_name_iface: + iface = set_name_iface + if "nameservers" in dev_cfg: + search = dev_cfg.get("nameservers").get("search", []) + dns = dev_cfg.get("nameservers").get("addresses", []) + name_cmd = {"type": "nameserver"} + if len(search) > 0: + name_cmd.update({"search": search}) + if len(dns) > 0: + name_cmd.update({"address": dns}) + self.handle_nameserver(name_cmd) + self._handle_individual_nameserver(name_cmd, iface) def _handle_bond_bridge(self, command, cmd_type=None): """Common handler for bond and bridge types""" # inverse mapping for v2 keynames to v1 keynames - v2key_to_v1 = dict((v, k) for k, v in - NET_CONFIG_TO_V2.get(cmd_type).items()) + v2key_to_v1 = dict( + (v, k) for k, v in NET_CONFIG_TO_V2.get(cmd_type).items() + ) for item_name, item_cfg in command.items(): - item_params = dict((key, value) for (key, value) in - item_cfg.items() if key not in - NETWORK_V2_KEY_FILTER) + 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 compatibility # Xenial does not have an updated netplan which supports the # correct spelling. LP: #1756701 - params = item_params.get('parameters', {}) - grat_value = params.pop('gratuitous-arp', None) + params = item_params.get("parameters", {}) + grat_value = params.pop("gratuitous-arp", None) if grat_value: - params['gratuitious-arp'] = grat_value + params["gratuitious-arp"] = grat_value v1_cmd = { - 'type': cmd_type, - 'name': item_name, - cmd_type + '_interfaces': item_cfg.get('interfaces'), - 'params': dict((v2key_to_v1[k], v) for k, v in params.items()) + "type": cmd_type, + "name": item_name, + cmd_type + "_interfaces": item_cfg.get("interfaces"), + "params": dict((v2key_to_v1[k], v) for k, v in params.items()), } - if 'mtu' in item_cfg: - v1_cmd['mtu'] = item_cfg['mtu'] + 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}) + v1_cmd.update({"subnets": subnets}) - LOG.debug('v2(%s) -> v1(%s):\n%s', cmd_type, cmd_type, v1_cmd) + LOG.debug("v2(%s) -> v1(%s):\n%s", cmd_type, cmd_type, v1_cmd) if cmd_type == "bridge": self.handle_bridge(v1_cmd) elif cmd_type == "bond": self.handle_bond(v1_cmd) else: - raise ValueError('Unknown command type: {cmd_type}'.format( - cmd_type=cmd_type)) + raise ValueError( + "Unknown command type: {cmd_type}".format( + cmd_type=cmd_type + ) + ) 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'] + if "route-metric" in overrides: + subnet["metric"] = overrides["route-metric"] subnets = [] - if cfg.get('dhcp4'): - subnet = {'type': 'dhcp4'} - _add_dhcp_overrides(cfg.get('dhcp4-overrides', {}), subnet) + 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'} + if cfg.get("dhcp6"): + subnet = {"type": "dhcp6"} self.use_ipv6 = True - _add_dhcp_overrides(cfg.get('dhcp6-overrides', {}), subnet) + _add_dhcp_overrides(cfg.get("dhcp6-overrides", {}), subnet) subnets.append(subnet) gateway4 = None gateway6 = None nameservers = {} - for address in cfg.get('addresses', []): + for address in cfg.get("addresses", []): subnet = { - 'type': 'static', - 'address': address, + "type": "static", + "address": address, } if ":" in address: - if 'gateway6' in cfg and gateway6 is None: - gateway6 = cfg.get('gateway6') - subnet.update({'gateway': gateway6}) + if "gateway6" in cfg and gateway6 is None: + gateway6 = cfg.get("gateway6") + subnet.update({"gateway": gateway6}) else: - if 'gateway4' in cfg and gateway4 is None: - gateway4 = cfg.get('gateway4') - subnet.update({'gateway': gateway4}) + if "gateway4" in cfg and gateway4 is None: + gateway4 = cfg.get("gateway4") + subnet.update({"gateway": gateway4}) - if 'nameservers' in cfg and not nameservers: - addresses = cfg.get('nameservers').get('addresses') + if "nameservers" in cfg and not nameservers: + addresses = cfg.get("nameservers").get("addresses") if addresses: - nameservers['dns_nameservers'] = addresses - search = cfg.get('nameservers').get('search') + nameservers["dns_nameservers"] = addresses + search = cfg.get("nameservers").get("search") if search: - nameservers['dns_search'] = search + nameservers["dns_search"] = search subnet.update(nameservers) subnets.append(subnet) routes = [] - for route in cfg.get('routes', []): - routes.append(_normalize_route( - {'destination': route.get('to'), 'gateway': route.get('via')})) + for route in cfg.get("routes", []): + routes.append( + _normalize_route( + { + "destination": route.get("to"), + "gateway": route.get("via"), + } + ) + ) # v2 routes are bound to the interface, in v1 we add them under # the first subnet since there isn't an equivalent interface level. if len(subnets) and len(routes): - subnets[0]['routes'] = routes + subnets[0]["routes"] = routes return subnets @@ -822,18 +871,25 @@ def _normalize_subnet(subnet): subnet = copy.deepcopy(subnet) normal_subnet = dict((k, v) for k, v in subnet.items() if v) - if subnet.get('type') in ('static', 'static6'): + if subnet.get("type") in ("static", "static6"): normal_subnet.update( - _normalize_net_keys(normal_subnet, address_keys=( - 'address', 'ip_address',))) - normal_subnet['routes'] = [_normalize_route(r) - for r in subnet.get('routes', [])] + _normalize_net_keys( + normal_subnet, + address_keys=( + "address", + "ip_address", + ), + ) + ) + normal_subnet["routes"] = [ + _normalize_route(r) for r in subnet.get("routes", []) + ] def listify(snet, name): if name in snet and not isinstance(snet[name], list): snet[name] = snet[name].split() - for k in ('dns_search', 'dns_nameservers'): + for k in ("dns_search", "dns_nameservers"): listify(normal_subnet, k) return normal_subnet @@ -857,42 +913,52 @@ def _normalize_net_keys(network, address_keys=()): addr_key = key break if not addr_key: - message = ( - 'No config network address keys [%s] found in %s' % - (','.join(address_keys), network)) + message = "No config network address keys [%s] found in %s" % ( + ",".join(address_keys), + network, + ) LOG.error(message) raise ValueError(message) addr = net.get(addr_key) ipv6 = is_ipv6_addr(addr) - netmask = net.get('netmask') + netmask = net.get("netmask") if "/" in addr: addr_part, _, maybe_prefix = addr.partition("/") net[addr_key] = addr_part try: prefix = int(maybe_prefix) except ValueError: - # this supports input of <address>/255.255.255.0 - prefix = mask_to_net_prefix(maybe_prefix) - elif netmask: - prefix = mask_to_net_prefix(netmask) - elif 'prefix' in net: - prefix = int(net['prefix']) + if ipv6: + # this supports input of ffff:ffff:ffff:: + prefix = ipv6_mask_to_net_prefix(maybe_prefix) + else: + # this supports input of 255.255.255.0 + prefix = ipv4_mask_to_net_prefix(maybe_prefix) + elif netmask and not ipv6: + prefix = ipv4_mask_to_net_prefix(netmask) + elif netmask and ipv6: + prefix = ipv6_mask_to_net_prefix(netmask) + elif "prefix" in net: + prefix = int(net["prefix"]) else: prefix = 64 if ipv6 else 24 - if 'prefix' in net and str(net['prefix']) != str(prefix): - LOG.warning("Overwriting existing 'prefix' with '%s' in " - "network info: %s", prefix, net) - net['prefix'] = prefix + if "prefix" in net and str(net["prefix"]) != str(prefix): + LOG.warning( + "Overwriting existing 'prefix' with '%s' in network info: %s", + prefix, + net, + ) + net["prefix"] = prefix if ipv6: # TODO: we could/maybe should add this back with the very uncommon # 'netmask' for ipv6. We need a 'net_prefix_to_ipv6_mask' for that. - if 'netmask' in net: - del net['netmask'] + if "netmask" in net: + del net["netmask"] else: - net['netmask'] = net_prefix_to_ipv4_mask(net['prefix']) + net["netmask"] = net_prefix_to_ipv4_mask(net["prefix"]) return net @@ -905,25 +971,28 @@ def _normalize_route(route): 'prefix': the network prefix for address as an integer. 'metric': integer metric (only if present in input). 'netmask': netmask (string) equivalent to prefix iff network is ipv4. - """ + """ # Prune None-value keys. Specifically allow 0 (a valid metric). - normal_route = dict((k, v) for k, v in route.items() - if v not in ("", None)) - if 'destination' in normal_route: - normal_route['network'] = normal_route['destination'] - del normal_route['destination'] + normal_route = dict( + (k, v) for k, v in route.items() if v not in ("", None) + ) + if "destination" in normal_route: + normal_route["network"] = normal_route["destination"] + del normal_route["destination"] normal_route.update( _normalize_net_keys( - normal_route, address_keys=('network', 'destination'))) + normal_route, address_keys=("network", "destination") + ) + ) - metric = normal_route.get('metric') + metric = normal_route.get("metric") if metric: try: - normal_route['metric'] = int(metric) + normal_route["metric"] = int(metric) except ValueError as e: raise TypeError( - 'Route config metric {} is not an integer'.format(metric) + "Route config metric {} is not an integer".format(metric) ) from e return normal_route @@ -944,10 +1013,10 @@ def subnet_is_ipv6(subnet): """Common helper for checking network_state subnets for ipv6.""" # 'static6', 'dhcp6', 'ipv6_dhcpv6-stateful', 'ipv6_dhcpv6-stateless' or # 'ipv6_slaac' - if subnet['type'].endswith('6') or subnet['type'] in IPV6_DYNAMIC_TYPES: + 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')): + elif subnet["type"] == "static" and is_ipv6_addr(subnet.get("address")): return True return False @@ -959,7 +1028,8 @@ def net_prefix_to_ipv4_mask(prefix): 24 -> "255.255.255.0" Also supports input as a string.""" mask = socket.inet_ntoa( - struct.pack(">I", (0xffffffff << (32 - int(prefix)) & 0xffffffff))) + struct.pack(">I", (0xFFFFFFFF << (32 - int(prefix)) & 0xFFFFFFFF)) + ) return mask @@ -972,84 +1042,82 @@ def ipv4_mask_to_net_prefix(mask): str(24) => 24 "24" => 24 """ - if isinstance(mask, int): - return mask - if isinstance(mask, str): - try: - return int(mask) - except ValueError: - pass - else: - raise TypeError("mask '%s' is not a string or int") - - if '.' not in mask: - raise ValueError("netmask '%s' does not contain a '.'" % mask) - - toks = mask.split(".") - if len(toks) != 4: - raise ValueError("netmask '%s' had only %d parts" % (mask, len(toks))) - - return sum([bin(int(x)).count('1') for x in toks]) + return ipaddress.ip_network(f"0.0.0.0/{mask}").prefixlen def ipv6_mask_to_net_prefix(mask): """Convert an ipv6 netmask (very uncommon) or prefix (64) to prefix. - If 'mask' is an integer or string representation of one then - int(mask) will be returned. + If the input is already an integer or a string representation of + an integer, then int(mask) will be returned. + "ffff:ffff:ffff::" => 48 + "48" => 48 """ - - if isinstance(mask, int): - return mask - if isinstance(mask, str): - try: - return int(mask) - except ValueError: - pass - else: - raise TypeError("mask '%s' is not a string or int") - - if ':' not in mask: - raise ValueError("mask '%s' does not have a ':'") - - bitCount = [0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00, - 0xff00, 0xff80, 0xffc0, 0xffe0, 0xfff0, 0xfff8, 0xfffc, - 0xfffe, 0xffff] - prefix = 0 - for word in mask.split(':'): - if not word or int(word, 16) == 0: - break - prefix += bitCount.index(int(word, 16)) - - return prefix - - -def mask_to_net_prefix(mask): - """Return the network prefix for the netmask provided. - - Supports ipv4 or ipv6 netmasks.""" try: - # if 'mask' is a prefix that is an integer. - # then just return it. - return int(mask) + # In the case the mask is already a prefix + prefixlen = ipaddress.ip_network(f"::/{mask}").prefixlen + return prefixlen except ValueError: + # ValueError means mask is an IPv6 address representation and need + # conversion. pass - if is_ipv6_addr(mask): - return ipv6_mask_to_net_prefix(mask) - else: - return ipv4_mask_to_net_prefix(mask) + + netmask = ipaddress.ip_address(mask) + mask_int = int(netmask) + # If the mask is all zeroes, just return it + if mask_int == 0: + return mask_int + + trailing_zeroes = min( + ipaddress.IPV6LENGTH, (~mask_int & (mask_int - 1)).bit_length() + ) + leading_ones = mask_int >> trailing_zeroes + prefixlen = ipaddress.IPV6LENGTH - trailing_zeroes + all_ones = (1 << prefixlen) - 1 + if leading_ones != all_ones: + raise ValueError("Invalid network mask '%s'" % mask) + + return prefixlen def mask_and_ipv4_to_bcast_addr(mask, ip): """Calculate the broadcast address from the subnet mask and ip addr. Supports ipv4 only.""" - ip_bin = int(''.join([bin(int(x) + 256)[3:] for x in ip.split('.')]), 2) + ip_bin = int("".join([bin(int(x) + 256)[3:] for x in ip.split(".")]), 2) mask_dec = ipv4_mask_to_net_prefix(mask) - bcast_bin = ip_bin | (2**(32 - mask_dec) - 1) - bcast_str = '.'.join([str(bcast_bin >> (i << 3) & 0xFF) - for i in range(4)[::-1]]) + bcast_bin = ip_bin | (2 ** (32 - mask_dec) - 1) + bcast_str = ".".join( + [str(bcast_bin >> (i << 3) & 0xFF) for i in range(4)[::-1]] + ) return bcast_str +def parse_net_config_data(net_config, skip_broken=True) -> NetworkState: + """Parses the config, returns NetworkState object + + :param net_config: curtin network config dict + """ + state = None + version = net_config.get("version") + config = net_config.get("config") + if version == 2: + # v2 does not have explicit 'config' key so we + # pass the whole net-config as-is + config = net_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() + + if not state: + raise RuntimeError( + "No valid network_state object created from network config. " + "Did you specify the correct version?" + ) + + return state + + # vi: ts=4 expandtab diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py new file mode 100644 index 00000000..3bbeb284 --- /dev/null +++ b/cloudinit/net/networkd.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +# vi: ts=4 expandtab +# +# Copyright (C) 2021 VMware Inc. +# +# Author: Shreenidhi Shedi <yesshedi@gmail.com> +# +# This file is part of cloud-init. See LICENSE file for license information. + +import os +from collections import OrderedDict + +from cloudinit import log as logging +from cloudinit import subp, util + +from . import renderer + +LOG = logging.getLogger(__name__) + + +class CfgParser: + def __init__(self): + self.conf_dict = OrderedDict( + { + "Match": [], + "Link": [], + "Network": [], + "DHCPv4": [], + "DHCPv6": [], + "Address": [], + "Route": [], + } + ) + + def update_section(self, sec, key, val): + for k in self.conf_dict.keys(): + if k == sec: + self.conf_dict[k].append(key + "=" + str(val)) + # remove duplicates from list + self.conf_dict[k] = list(dict.fromkeys(self.conf_dict[k])) + self.conf_dict[k].sort() + + def get_final_conf(self): + contents = "" + for k, v in sorted(self.conf_dict.items()): + if not v: + continue + contents += "[" + k + "]\n" + for e in sorted(v): + contents += e + "\n" + contents += "\n" + + return contents + + def dump_data(self, target_fn): + if not target_fn: + LOG.warning("Target file not given") + return + + contents = self.get_final_conf() + LOG.debug("Final content: %s", contents) + util.write_file(target_fn, contents) + + +class Renderer(renderer.Renderer): + """ + Renders network information in /etc/systemd/network + + This Renderer is currently experimental and doesn't support all the + use cases supported by the other renderers yet. + """ + + def __init__(self, config=None): + if not config: + config = {} + self.resolve_conf_fn = config.get( + "resolve_conf_fn", "/etc/systemd/resolved.conf" + ) + self.network_conf_dir = config.get( + "network_conf_dir", "/etc/systemd/network/" + ) + + def generate_match_section(self, iface, cfg): + sec = "Match" + match_dict = { + "name": "Name", + "driver": "Driver", + "mac_address": "MACAddress", + } + + if not iface: + return + + for k, v in match_dict.items(): + if k in iface and iface[k]: + cfg.update_section(sec, v, iface[k]) + + return iface["name"] + + def generate_link_section(self, iface, cfg): + sec = "Link" + + if not iface: + return + + if "mtu" in iface and iface["mtu"]: + cfg.update_section(sec, "MTUBytes", iface["mtu"]) + + def parse_routes(self, conf, cfg): + sec = "Route" + route_cfg_map = { + "gateway": "Gateway", + "network": "Destination", + "metric": "Metric", + } + + # prefix is derived using netmask by network_state + prefix = "" + if "prefix" in conf: + prefix = "/" + str(conf["prefix"]) + + for k, v in conf.items(): + if k not in route_cfg_map: + continue + if k == "network": + v += prefix + cfg.update_section(sec, route_cfg_map[k], v) + + def parse_subnets(self, iface, cfg): + dhcp = "no" + sec = "Network" + for e in iface.get("subnets", []): + t = e["type"] + if t == "dhcp4" or t == "dhcp": + if dhcp == "no": + dhcp = "ipv4" + elif dhcp == "ipv6": + dhcp = "yes" + elif t == "dhcp6": + if dhcp == "no": + dhcp = "ipv6" + elif dhcp == "ipv4": + dhcp = "yes" + if "routes" in e and e["routes"]: + for i in e["routes"]: + self.parse_routes(i, cfg) + if "address" in e: + subnet_cfg_map = { + "address": "Address", + "gateway": "Gateway", + "dns_nameservers": "DNS", + "dns_search": "Domains", + } + for k, v in e.items(): + if k == "address": + if "prefix" in e: + v += "/" + str(e["prefix"]) + cfg.update_section("Address", subnet_cfg_map[k], v) + elif k == "gateway": + cfg.update_section("Route", subnet_cfg_map[k], v) + elif k == "dns_nameservers" or k == "dns_search": + cfg.update_section(sec, subnet_cfg_map[k], " ".join(v)) + + cfg.update_section(sec, "DHCP", dhcp) + + if dhcp in ["ipv6", "yes"] and isinstance( + iface.get("accept-ra", ""), bool + ): + cfg.update_section(sec, "IPv6AcceptRA", iface["accept-ra"]) + + # This is to accommodate extra keys present in VMware config + def dhcp_domain(self, d, cfg): + for item in ["dhcp4domain", "dhcp6domain"]: + if item not in d: + continue + ret = str(d[item]).casefold() + try: + ret = util.translate_bool(ret) + ret = "yes" if ret else "no" + except ValueError: + if ret != "route": + LOG.warning("Invalid dhcp4domain value - %s", ret) + ret = "no" + if item == "dhcp4domain": + section = "DHCPv4" + else: + section = "DHCPv6" + cfg.update_section(section, "UseDomains", ret) + + def parse_dns(self, iface, cfg, ns): + sec = "Network" + + dns_cfg_map = { + "search": "Domains", + "nameservers": "DNS", + "addresses": "DNS", + } + + dns = iface.get("dns") + if not dns and ns.version == 1: + dns = { + "search": ns.dns_searchdomains, + "nameservers": ns.dns_nameservers, + } + elif not dns and ns.version == 2: + return + + for k, v in dns_cfg_map.items(): + if k in dns and dns[k]: + cfg.update_section(sec, v, " ".join(dns[k])) + + def create_network_file(self, link, conf, nwk_dir): + net_fn_owner = "systemd-network" + + LOG.debug("Setting Networking Config for %s", link) + + net_fn = nwk_dir + "10-cloud-init-" + link + ".network" + util.write_file(net_fn, conf) + util.chownbyname(net_fn, net_fn_owner, net_fn_owner) + + def render_network_state(self, network_state, templates=None, target=None): + fp_nwkd = self.network_conf_dir + if target: + fp_nwkd = subp.target_path(target) + fp_nwkd + + util.ensure_dir(os.path.dirname(fp_nwkd)) + + ret_dict = self._render_content(network_state) + for k, v in ret_dict.items(): + self.create_network_file(k, v, fp_nwkd) + + def _render_content(self, ns): + ret_dict = {} + for iface in ns.iter_interfaces(): + cfg = CfgParser() + + link = self.generate_match_section(iface, cfg) + self.generate_link_section(iface, cfg) + self.parse_subnets(iface, cfg) + self.parse_dns(iface, cfg, ns) + + for route in ns.iter_routes(): + self.parse_routes(route, cfg) + + if ns.version == 2: + name = iface["name"] + # network state doesn't give dhcp domain info + # using ns.config as a workaround here + + # Check to see if this interface matches against an interface + # from the network state that specified a set-name directive. + # If there is a device with a set-name directive and it has + # set-name value that matches the current name, then update the + # current name to the device's name. That will be the value in + # the ns.config['ethernets'] dict below. + for dev_name, dev_cfg in ns.config["ethernets"].items(): + if "set-name" in dev_cfg: + if dev_cfg.get("set-name") == name: + name = dev_name + break + + self.dhcp_domain(ns.config["ethernets"][name], cfg) + + ret_dict.update({link: cfg.get_final_conf()}) + + return ret_dict + + +def available(target=None): + expected = ["ip", "systemctl"] + search = ["/usr/sbin", "/bin"] + for p in expected: + if not subp.which(p, search=search, target=target): + return False + return True + + +def network_state_to_networkd(ns): + renderer = Renderer({}) + return renderer._render_content(ns) diff --git a/cloudinit/net/openbsd.py b/cloudinit/net/openbsd.py index 166d77e6..70e9f461 100644 --- a/cloudinit/net/openbsd.py +++ b/cloudinit/net/openbsd.py @@ -1,44 +1,58 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util +import platform + import cloudinit.net.bsd +from cloudinit import log as logging +from cloudinit import subp, util LOG = logging.getLogger(__name__) class Renderer(cloudinit.net.bsd.BSDRenderer): - def write_config(self): for device_name, v in self.interface_configurations.items(): - if_file = 'etc/hostname.{}'.format(device_name) + if_file = "etc/hostname.{}".format(device_name) fn = subp.target_path(self.target, if_file) if device_name in self.dhcp_interfaces(): - content = 'dhcp\n' + content = "dhcp\n" elif isinstance(v, dict): try: - content = "inet {address} {netmask}\n".format( - address=v['address'], - netmask=v['netmask'] + content = "inet {address} {netmask}".format( + address=v["address"], netmask=v["netmask"] ) except KeyError: LOG.error( - "Invalid static configuration for %s", - device_name) + "Invalid static configuration for %s", device_name + ) + mtu = v.get("mtu") + if mtu: + content += " mtu %d" % mtu + content += "\n" util.write_file(fn, content) def start_services(self, run=False): + has_dhcpleasectl = bool(int(platform.release().split(".")[0]) > 6) if not self._postcmds: LOG.debug("openbsd generate postcmd disabled") return - subp.subp(['sh', '/etc/netstart'], capture=True) + if has_dhcpleasectl: # OpenBSD 7.0+ + subp.subp(["sh", "/etc/netstart"], capture=True) + for interface in self.dhcp_interfaces(): + subp.subp( + ["dhcpleasectl", "-w", "30", interface], capture=True + ) + else: + subp.subp(["pkill", "dhclient"], capture=True, rcs=[0, 1]) + subp.subp(["route", "del", "default"], capture=True, rcs=[0, 1]) + subp.subp(["route", "flush", "default"], capture=True, rcs=[0, 1]) + subp.subp(["sh", "/etc/netstart"], capture=True) def set_route(self, network, netmask, gateway): - if network == '0.0.0.0': - if_file = 'etc/mygate' + if network == "0.0.0.0": + if_file = "etc/mygate" fn = subp.target_path(self.target, if_file) - content = gateway + '\n' + content = gateway + "\n" util.write_file(fn, content) diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py index 2a61a7a8..34b74b80 100644 --- a/cloudinit/net/renderer.py +++ b/cloudinit/net/renderer.py @@ -8,26 +8,28 @@ import abc import io -from .network_state import parse_net_config_data -from .udev import generate_udev_rule +from cloudinit.net.network_state import parse_net_config_data +from cloudinit.net.udev import generate_udev_rule def filter_by_type(match_type): - return lambda iface: match_type == iface['type'] + return lambda iface: match_type == iface["type"] def filter_by_name(match_name): - return lambda iface: match_name == iface['name'] + return lambda iface: match_name == iface["name"] def filter_by_attr(match_name): return lambda iface: (match_name in iface and iface[match_name]) -filter_by_physical = filter_by_type('physical') +filter_by_physical = filter_by_type("physical") class Renderer(object): + def __init__(self, config=None): + pass @staticmethod def _render_persistent_net(network_state): @@ -37,22 +39,27 @@ class Renderer(object): 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'): - driver = iface.get('driver', None) - content.write(generate_udev_rule(iface['name'], - iface['mac_address'], - driver=driver)) + if "name" in iface and iface.get("mac_address"): + driver = iface.get("driver", None) + content.write( + generate_udev_rule( + iface["name"], iface["mac_address"], driver=driver + ) + ) return content.getvalue() @abc.abstractmethod - def render_network_state(self, network_state, templates=None, - target=None): + def render_network_state(self, network_state, templates=None, target=None): """Render network state.""" - def render_network_config(self, network_config, templates=None, - target=None): + def render_network_config( + self, network_config, templates=None, target=None + ): return self.render_network_state( network_state=parse_net_config_data(network_config), - templates=templates, target=target) + templates=templates, + target=target, + ) + # vi: ts=4 expandtab diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py index e2de4d55..c755f04c 100644 --- a/cloudinit/net/renderers.py +++ b/cloudinit/net/renderers.py @@ -1,27 +1,43 @@ # This file is part of cloud-init. See LICENSE file for license information. -from . import eni -from . import freebsd -from . import netbsd -from . import netplan -from . import RendererNotFoundError -from . import openbsd -from . import sysconfig +from typing import List, Tuple, Type + +from . import ( + RendererNotFoundError, + eni, + freebsd, + netbsd, + netplan, + networkd, + openbsd, + renderer, + sysconfig, +) NAME_TO_RENDERER = { "eni": eni, "freebsd": freebsd, "netbsd": netbsd, "netplan": netplan, + "networkd": networkd, "openbsd": openbsd, "sysconfig": sysconfig, } -DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd", - "netbsd", "openbsd"] +DEFAULT_PRIORITY = [ + "eni", + "sysconfig", + "netplan", + "freebsd", + "netbsd", + "openbsd", + "networkd", +] -def search(priority=None, target=None, first=False): +def search( + priority=None, target=None, first=False +) -> List[Tuple[str, Type[renderer.Renderer]]]: if priority is None: priority = DEFAULT_PRIORITY @@ -30,7 +46,8 @@ def search(priority=None, target=None, first=False): unknown = [i for i in priority if i not in available] if unknown: raise ValueError( - "Unknown renderers provided in priority list: %s" % unknown) + "Unknown renderers provided in priority list: %s" % unknown + ) found = [] for name in priority: @@ -38,13 +55,13 @@ def search(priority=None, target=None, first=False): if render_mod.available(target): cur = (name, render_mod.Renderer) if first: - return cur + return [cur] found.append(cur) return found -def select(priority=None, target=None): +def select(priority=None, target=None) -> Tuple[str, Type[renderer.Renderer]]: found = search(priority, target=target, first=True) if not found: if priority is None: @@ -53,8 +70,10 @@ def select(priority=None, target=None): if target and target != "/": tmsg = " in target=%s" % target raise RendererNotFoundError( - "No available network renderers found%s. Searched " - "through list: %s" % (tmsg, priority)) - return found + "No available network renderers found%s. Searched through list: %s" + % (tmsg, priority) + ) + return found[0] + # vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index a930e612..ba85c4f6 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -8,21 +8,36 @@ import re from configobj import ConfigObj from cloudinit import log as logging -from cloudinit import util -from cloudinit import subp -from cloudinit.distros.parsers import networkmanager_conf -from cloudinit.distros.parsers import resolv_conf +from cloudinit import subp, util +from cloudinit.distros.parsers import networkmanager_conf, resolv_conf +from cloudinit.net import network_state from . import renderer from .network_state import ( - is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES) + IPV6_DYNAMIC_TYPES, + is_ipv6_addr, + net_prefix_to_ipv4_mask, + subnet_is_ipv6, +) LOG = logging.getLogger(__name__) +KNOWN_DISTROS = [ + "almalinux", + "centos", + "cloudlinux", + "eurolinux", + "fedora", + "miraclelinux", + "openEuler", + "rhel", + "rocky", + "suse", + "virtuozzo", +] NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" -KNOWN_DISTROS = ['centos', 'fedora', 'rhel', 'suse'] -def _make_header(sep='#'): +def _make_header(sep="#"): lines = [ "Created by cloud-init on instance boot automatically, do not edit.", "", @@ -36,8 +51,8 @@ def _make_header(sep='#'): def _is_default_route(route): - default_nets = ('::', '0.0.0.0') - return route['prefix'] == 0 and route['network'] in default_nets + default_nets = ("::", "0.0.0.0") + return route["prefix"] == 0 and route["network"] in default_nets def _quote_value(value): @@ -54,19 +69,19 @@ def _quote_value(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']: + if "main" in config: + if "plugins" in config["main"]: + if "ifcfg-rh" in config["main"]["plugins"]: return else: - config['main']['plugins'] = [] + config["main"]["plugins"] = [] - if isinstance(config['main']['plugins'], list): - config['main']['plugins'].append('ifcfg-rh') + if isinstance(config["main"]["plugins"], list): + config["main"]["plugins"].append("ifcfg-rh") else: - config['main']['plugins'] = [config['main']['plugins'], 'ifcfg-rh'] + config["main"]["plugins"] = [config["main"]["plugins"], "ifcfg-rh"] config.write() - LOG.debug('Enabled ifcfg-rh NetworkManager plugins') + LOG.debug("Enabled ifcfg-rh NetworkManager plugins") class ConfigMap(object): @@ -74,8 +89,8 @@ class ConfigMap(object): # Why does redhat prefer yes/no to true/false?? _bool_map = { - True: 'yes', - False: 'no', + True: "yes", + False: "no", } def __init__(self): @@ -126,8 +141,7 @@ class ConfigMap(object): class Route(ConfigMap): """Represents a route configuration.""" - def __init__(self, route_name, base_sysconf_dir, - ipv4_tpl, ipv6_tpl): + def __init__(self, route_name, base_sysconf_dir, ipv4_tpl, ipv6_tpl): super(Route, self).__init__() self.last_idx = 1 self.has_set_default_ipv4 = False @@ -138,8 +152,12 @@ class Route(ConfigMap): self.route_fn_tpl_ipv6 = ipv6_tpl def copy(self): - r = Route(self._route_name, self._base_sysconf_dir, - self.route_fn_tpl_ipv4, self.route_fn_tpl_ipv6) + r = Route( + self._route_name, + self._base_sysconf_dir, + self.route_fn_tpl_ipv4, + self.route_fn_tpl_ipv6, + ) r._conf = self._conf.copy() r.last_idx = self.last_idx r.has_set_default_ipv4 = self.has_set_default_ipv4 @@ -148,20 +166,22 @@ class Route(ConfigMap): @property def path_ipv4(self): - return self.route_fn_tpl_ipv4 % ({'base': self._base_sysconf_dir, - 'name': self._route_name}) + return self.route_fn_tpl_ipv4 % ( + {"base": self._base_sysconf_dir, "name": self._route_name} + ) @property def path_ipv6(self): - return self.route_fn_tpl_ipv6 % ({'base': self._base_sysconf_dir, - 'name': self._route_name}) + return self.route_fn_tpl_ipv6 % ( + {"base": self._base_sysconf_dir, "name": self._route_name} + ) def is_ipv6_route(self, address): - return ':' in address + return ":" in address def to_string(self, proto="ipv4"): # only accept ipv4 and ipv6 - if proto not in ['ipv4', 'ipv6']: + if proto not in ["ipv4", "ipv6"]: raise ValueError("Unknown protocol '%s'" % (str(proto))) buf = io.StringIO() buf.write(_make_header()) @@ -171,43 +191,61 @@ class Route(ConfigMap): # (because Route can contain a mix of IPv4 and IPv6) reindex = -1 for key in sorted(self._conf.keys()): - if 'ADDRESS' in key: - index = key.replace('ADDRESS', '') - address_value = str(self._conf[key]) - # only accept combinations: - # if proto ipv6 only display ipv6 routes - # if proto ipv4 only display ipv4 routes - # do not add ipv6 routes if proto is ipv4 - # do not add ipv4 routes if proto is ipv6 - # (this array will contain a mix of ipv4 and ipv6) - if proto == "ipv4" and not self.is_ipv6_route(address_value): - netmask_value = str(self._conf['NETMASK' + index]) - gateway_value = str(self._conf['GATEWAY' + index]) - # increase IPv4 index - reindex = reindex + 1 - buf.write("%s=%s\n" % ('ADDRESS' + str(reindex), - _quote_value(address_value))) - buf.write("%s=%s\n" % ('GATEWAY' + str(reindex), - _quote_value(gateway_value))) - buf.write("%s=%s\n" % ('NETMASK' + str(reindex), - _quote_value(netmask_value))) - metric_key = 'METRIC' + index - if metric_key in self._conf: - metric_value = str(self._conf['METRIC' + index]) - buf.write("%s=%s\n" % ('METRIC' + str(reindex), - _quote_value(metric_value))) - elif proto == "ipv6" and self.is_ipv6_route(address_value): - netmask_value = str(self._conf['NETMASK' + index]) - gateway_value = str(self._conf['GATEWAY' + index]) - metric_value = ( - 'metric ' + str(self._conf['METRIC' + index]) - if 'METRIC' + index in self._conf else '') + if "ADDRESS" not in key: + continue + + index = key.replace("ADDRESS", "") + address_value = str(self._conf[key]) + netmask_value = str(self._conf["NETMASK" + index]) + gateway_value = str(self._conf["GATEWAY" + index]) + + # only accept combinations: + # if proto ipv6 only display ipv6 routes + # if proto ipv4 only display ipv4 routes + # do not add ipv6 routes if proto is ipv4 + # do not add ipv4 routes if proto is ipv6 + # (this array will contain a mix of ipv4 and ipv6) + if proto == "ipv4" and not self.is_ipv6_route(address_value): + # increase IPv4 index + reindex = reindex + 1 + buf.write( + "%s=%s\n" + % ("ADDRESS" + str(reindex), _quote_value(address_value)) + ) + buf.write( + "%s=%s\n" + % ("GATEWAY" + str(reindex), _quote_value(gateway_value)) + ) + buf.write( + "%s=%s\n" + % ("NETMASK" + str(reindex), _quote_value(netmask_value)) + ) + metric_key = "METRIC" + index + if metric_key in self._conf: + metric_value = str(self._conf["METRIC" + index]) buf.write( - "%s/%s via %s %s dev %s\n" % (address_value, - netmask_value, - gateway_value, - metric_value, - self._route_name)) + "%s=%s\n" + % ("METRIC" + str(reindex), _quote_value(metric_value)) + ) + elif proto == "ipv6" and self.is_ipv6_route(address_value): + prefix_value = network_state.ipv6_mask_to_net_prefix( + netmask_value + ) + metric_value = ( + "metric " + str(self._conf["METRIC" + index]) + if "METRIC" + index in self._conf + else "" + ) + buf.write( + "%s/%s via %s %s dev %s\n" + % ( + address_value, + prefix_value, + gateway_value, + metric_value, + self._route_name, + ) + ) return buf.getvalue() @@ -216,27 +254,31 @@ class NetInterface(ConfigMap): """Represents a sysconfig/networking-script (and its config + children).""" iface_types = { - 'ethernet': 'Ethernet', - 'bond': 'Bond', - 'bridge': 'Bridge', - 'infiniband': 'InfiniBand', - 'vlan': 'Vlan', + "ethernet": "Ethernet", + "bond": "Bond", + "bridge": "Bridge", + "infiniband": "InfiniBand", + "vlan": "Vlan", } - def __init__(self, iface_name, base_sysconf_dir, templates, - kind='ethernet'): + def __init__( + self, iface_name, base_sysconf_dir, templates, kind="ethernet" + ): super(NetInterface, self).__init__() self.children = [] self.templates = templates - route_tpl = self.templates.get('route_templates') - self.routes = Route(iface_name, base_sysconf_dir, - ipv4_tpl=route_tpl.get('ipv4'), - ipv6_tpl=route_tpl.get('ipv6')) - self.iface_fn_tpl = self.templates.get('iface_templates') + route_tpl = self.templates.get("route_templates") + self.routes = Route( + iface_name, + base_sysconf_dir, + ipv4_tpl=route_tpl.get("ipv4"), + ipv6_tpl=route_tpl.get("ipv6"), + ) + self.iface_fn_tpl = self.templates.get("iface_templates") self.kind = kind self._iface_name = iface_name - self._conf['DEVICE'] = iface_name + self._conf["DEVICE"] = iface_name self._base_sysconf_dir = base_sysconf_dir @property @@ -246,7 +288,7 @@ class NetInterface(ConfigMap): @name.setter def name(self, iface_name): self._iface_name = iface_name - self._conf['DEVICE'] = iface_name + self._conf["DEVICE"] = iface_name @property def kind(self): @@ -257,16 +299,18 @@ class NetInterface(ConfigMap): if kind not in self.iface_types: raise ValueError(kind) self._kind = kind - self._conf['TYPE'] = self.iface_types[kind] + self._conf["TYPE"] = self.iface_types[kind] @property def path(self): - return self.iface_fn_tpl % ({'base': self._base_sysconf_dir, - 'name': self.name}) + return self.iface_fn_tpl % ( + {"base": self._base_sysconf_dir, "name": self.name} + ) def copy(self, copy_children=False, copy_routes=False): - c = NetInterface(self.name, self._base_sysconf_dir, - self.templates, kind=self._kind) + c = NetInterface( + self.name, self._base_sysconf_dir, self.templates, kind=self._kind + ) c._conf = self._conf.copy() if copy_children: c.children = list(self.children) @@ -275,7 +319,7 @@ class NetInterface(ConfigMap): return c def skip_key_value(self, key, val): - if key == 'TYPE' and val == 'Vlan': + if key == "TYPE" and val == "Vlan": return True return False @@ -289,158 +333,180 @@ class Renderer(renderer.Renderer): # details about this) iface_defaults = { - 'rhel': {'ONBOOT': True, 'USERCTL': False, 'NM_CONTROLLED': False, - 'BOOTPROTO': 'none'}, - 'suse': {'BOOTPROTO': 'static', 'STARTMODE': 'auto'}, + "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', + "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', + "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. - bond_tpl_opts = tuple([ - ('bond_mode', "mode=%s"), - ('bond_xmit_hash_policy', "xmit_hash_policy=%s"), - ('bond_miimon', "miimon=%s"), - ('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"), - ]) + # a BONDING_OPTS / BONDING_MODULE_OPTS grouping; otherwise no + # grouping will be set. + bond_tpl_opts = tuple( + [ + ("bond_mode", "mode=%s"), + ("bond_xmit_hash_policy", "xmit_hash_policy=%s"), + ("bond_miimon", "miimon=%s"), + ("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 = {} def __init__(self, config=None): if not config: config = {} - self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig') + self.sysconf_dir = config.get("sysconf_dir", "etc/sysconfig") self.netrules_path = config.get( - 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules') - self.dns_path = config.get('dns_path', 'etc/resolv.conf') - nm_conf_path = 'etc/NetworkManager/conf.d/99-cloud-init.conf' - self.networkmanager_conf_path = config.get('networkmanager_conf_path', - nm_conf_path) + "netrules_path", "etc/udev/rules.d/70-persistent-net.rules" + ) + self.dns_path = config.get("dns_path", "etc/resolv.conf") + nm_conf_path = "etc/NetworkManager/conf.d/99-cloud-init.conf" + self.networkmanager_conf_path = config.get( + "networkmanager_conf_path", nm_conf_path + ) self.templates = { - 'control': config.get('control'), - 'iface_templates': config.get('iface_templates'), - 'route_templates': config.get('route_templates'), + "control": config.get("control"), + "iface_templates": config.get("iface_templates"), + "route_templates": config.get("route_templates"), } - self.flavor = config.get('flavor', 'rhel') + self.flavor = config.get("flavor", "rhel") @classmethod 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 in ('mac_address', 'mtu', 'accept-ra'): + 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'] not in ['physical', 'infiniband']): + if old_key == "mac_address" and iface["type"] not in [ + "physical", + "infiniband", + ]: continue new_key = cls.cfg_key_maps[flavor].get(old_key) if new_key: iface_cfg[new_key] = old_value # only set WakeOnLan for physical interfaces - if ('wakeonlan' in iface and iface['wakeonlan'] and - iface['type'] == 'physical'): - iface_cfg['ETHTOOL_OPTS'] = 'wol g' + if ( + "wakeonlan" in iface + and iface["wakeonlan"] + and iface["type"] == "physical" + ): + iface_cfg["ETHTOOL_OPTS"] = "wol g" @classmethod def _render_subnets(cls, iface_cfg, subnets, has_default_route, flavor): # setting base values - if flavor == 'suse': - iface_cfg['BOOTPROTO'] = 'static' - if 'BRIDGE' in iface_cfg: - iface_cfg['BOOTPROTO'] = 'dhcp' - iface_cfg.drop('BRIDGE') + 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' + 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' or subnet_type == 'ipv6_dhcpv6-stateful': - if flavor == 'suse': + mtu_key = "MTU" + subnet_type = subnet.get("type") + 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' + 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' + iface_cfg["BOOTPROTO"] = "dhcp6" + iface_cfg["DHCLIENT6_MODE"] = "managed" + # only if rhel AND dhcpv6 stateful + elif ( + flavor == "rhel" and subnet_type == "ipv6_dhcpv6-stateful" + ): + iface_cfg["BOOTPROTO"] = "dhcp" + iface_cfg["DHCPV6C"] = True + iface_cfg["IPV6INIT"] = True + iface_cfg["IPV6_AUTOCONF"] = False else: - iface_cfg['IPV6INIT'] = True + iface_cfg["IPV6INIT"] = True # Configure network settings using DHCPv6 - iface_cfg['DHCPV6C'] = True - elif subnet_type == 'ipv6_dhcpv6-stateless': - if flavor == 'suse': + 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' + 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' + iface_cfg["BOOTPROTO"] = "dhcp6" + iface_cfg["DHCLIENT6_MODE"] = "info" else: - iface_cfg['IPV6INIT'] = True + 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 + 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': + 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' + 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' + iface_cfg["BOOTPROTO"] = "dhcp6" + iface_cfg["DHCLIENT6_MODE"] = "info" else: - iface_cfg['IPV6INIT'] = True + 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' - if flavor == 'suse' and subnet_type == 'dhcp4': + iface_cfg["IPV6_AUTOCONF"] = True + elif subnet_type in ["dhcp4", "dhcp"]: + bootproto_in = iface_cfg["BOOTPROTO"] + iface_cfg["BOOTPROTO"] = "dhcp" + if flavor == "suse" and subnet_type == "dhcp4": # If dhcp6 is already specified the user wants dhcp # for both protocols - if bootproto_in != 'dhcp6': + if bootproto_in != "dhcp6": # Only IPv4 is DHCP, IPv6 may be static - iface_cfg['BOOTPROTO'] = 'dhcp4' - elif subnet_type in ['static', 'static6']: + iface_cfg["BOOTPROTO"] = "dhcp4" + elif subnet_type in ["static", "static6"]: # RH info # grep BOOTPROTO sysconfig.txt -A2 | head -3 # BOOTPROTO=none|bootp|dhcp @@ -448,174 +514,189 @@ class Renderer(renderer.Renderer): # to run on the device. Any other # value causes any static configuration # in the file to be applied. - if subnet_is_ipv6(subnet) and flavor != 'suse': - mtu_key = 'IPV6_MTU' - iface_cfg['IPV6INIT'] = True - if 'mtu' in subnet: - mtu_mismatch = bool(mtu_key in iface_cfg and - subnet['mtu'] != iface_cfg[mtu_key]) + if subnet_is_ipv6(subnet) and flavor != "suse": + mtu_key = "IPV6_MTU" + iface_cfg["IPV6INIT"] = True + if "mtu" in subnet: + mtu_mismatch = bool( + mtu_key in iface_cfg + and subnet["mtu"] != iface_cfg[mtu_key] + ) if mtu_mismatch: LOG.warning( - 'Network config: ignoring %s device-level mtu:%s' - ' because ipv4 subnet-level mtu:%s provided.', - iface_cfg.name, iface_cfg[mtu_key], subnet['mtu']) + "Network config: ignoring %s device-level mtu:%s" + " because ipv4 subnet-level mtu:%s provided.", + iface_cfg.name, + iface_cfg[mtu_key], + subnet["mtu"], + ) if subnet_is_ipv6(subnet): - if flavor == 'suse': + if flavor == "suse": # TODO(rjschwei) write mtu setting to # /etc/sysctl.d/ pass else: - iface_cfg[mtu_key] = subnet['mtu'] + iface_cfg[mtu_key] = subnet["mtu"] else: - iface_cfg[mtu_key] = subnet['mtu'] + iface_cfg[mtu_key] = subnet["mtu"] - if subnet_is_ipv6(subnet) and flavor == 'rhel': - iface_cfg['IPV6_FORCE_ACCEPT_RA'] = False - iface_cfg['IPV6_AUTOCONF'] = False - elif subnet_type == 'manual': - if flavor == 'suse': + if subnet_is_ipv6(subnet) and flavor == "rhel": + iface_cfg["IPV6_FORCE_ACCEPT_RA"] = False + iface_cfg["IPV6_AUTOCONF"] = False + elif subnet_type == "manual": + 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 + 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': - if flavor == 'suse': - iface_cfg['STARTMODE'] = 'manual' + raise ValueError( + "Unknown subnet type '%s' found for interface '%s'" + % (subnet_type, iface_cfg.name) + ) + if subnet.get("control") == "manual": + if flavor == "suse": + iface_cfg["STARTMODE"] = "manual" else: - iface_cfg['ONBOOT'] = False + 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') + subnet_type = subnet.get("type") # 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']: + 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 + if flavor != "suse": + if has_default_route and iface_cfg["BOOTPROTO"] != "none": + iface_cfg["DHCLIENT_SET_DEFAULT_ROUTE"] = False continue elif subnet_type in IPV6_DYNAMIC_TYPES: continue - elif subnet_type in ['static', 'static6']: + elif subnet_type in ["static", "static6"]: if subnet_is_ipv6(subnet): ipv6_index = ipv6_index + 1 - ipv6_cidr = "%s/%s" % (subnet['address'], subnet['prefix']) + ipv6_cidr = "%s/%s" % (subnet["address"], subnet["prefix"]) if ipv6_index == 0: - if flavor == 'suse': - iface_cfg['IPADDR6'] = ipv6_cidr + if flavor == "suse": + iface_cfg["IPADDR6"] = ipv6_cidr else: - iface_cfg['IPV6ADDR'] = ipv6_cidr + iface_cfg["IPV6ADDR"] = ipv6_cidr elif ipv6_index == 1: - if flavor == 'suse': - iface_cfg['IPADDR6_1'] = ipv6_cidr + if flavor == "suse": + iface_cfg["IPADDR6_1"] = ipv6_cidr else: - iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr + iface_cfg["IPV6ADDR_SECONDARIES"] = ipv6_cidr else: - if flavor == 'suse': - iface_cfg['IPADDR6_%d' % ipv6_index] = ipv6_cidr + if flavor == "suse": + iface_cfg["IPADDR6_%d" % ipv6_index] = ipv6_cidr else: - iface_cfg['IPV6ADDR_SECONDARIES'] += \ + iface_cfg["IPV6ADDR_SECONDARIES"] += ( " " + ipv6_cidr + ) else: ipv4_index = ipv4_index + 1 suff = "" if ipv4_index == 0 else str(ipv4_index) - iface_cfg['IPADDR' + suff] = subnet['address'] - iface_cfg['NETMASK' + suff] = \ - net_prefix_to_ipv4_mask(subnet['prefix']) - - if 'gateway' in subnet and flavor != 'suse': - iface_cfg['DEFROUTE'] = True - if is_ipv6_addr(subnet['gateway']): - iface_cfg['IPV6_DEFAULTGW'] = subnet['gateway'] + iface_cfg["IPADDR" + suff] = subnet["address"] + iface_cfg["NETMASK" + suff] = net_prefix_to_ipv4_mask( + subnet["prefix"] + ) + + 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'] + iface_cfg["GATEWAY"] = subnet["gateway"] - if 'dns_search' in subnet and flavor != 'suse': - iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search']) + if "dns_search" in subnet and flavor != "suse": + iface_cfg["DOMAIN"] = " ".join(subnet["dns_search"]) - if 'dns_nameservers' in subnet and flavor != 'suse': - if len(subnet['dns_nameservers']) > 3: + 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. " - "Only 3 are used.", iface_cfg.name, - len(subnet['dns_nameservers'])) - for i, k in enumerate(subnet['dns_nameservers'][:3], 1): - iface_cfg['DNS' + str(i)] = k + LOG.debug( + "%s has %d entries in dns_nameservers. " + "Only 3 are used.", + iface_cfg.name, + len(subnet["dns_nameservers"]), + ) + for i, k in enumerate(subnet["dns_nameservers"][:3], 1): + iface_cfg["DNS" + str(i)] = k @classmethod 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': + 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']) + subnet_type = subnet.get("type") + for route in subnet.get("routes", []): + is_ipv6 = subnet.get("ipv6") or is_ipv6_addr(route["gateway"]) # 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 ( + _is_default_route(route) + and subnet_type not in IPV6_DYNAMIC_TYPES + ): if ( - (subnet.get('ipv4') and - route_cfg.has_set_default_ipv4) or - (subnet.get('ipv6') and - route_cfg.has_set_default_ipv6) + subnet.get("ipv4") and route_cfg.has_set_default_ipv4 + ) or ( + subnet.get("ipv6") and route_cfg.has_set_default_ipv6 ): - raise ValueError("Duplicate declaration of default " - "route found for interface '%s'" - % (iface_cfg.name)) + raise ValueError( + "Duplicate declaration of default " + "route found for interface '%s'" % (iface_cfg.name) + ) # NOTE(harlowja): ipv6 and ipv4 default gateways - gw_key = 'GATEWAY0' - nm_key = 'NETMASK0' - addr_key = 'ADDRESS0' + gw_key = "GATEWAY0" + nm_key = "NETMASK0" + addr_key = "ADDRESS0" # The owning interface provides the default route. # # 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: + 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: - iface_cfg['IPV6_DEFAULTGW'] = route['gateway'] + iface_cfg["IPV6_DEFAULTGW"] = route["gateway"] route_cfg.has_set_default_ipv6 = True else: - iface_cfg['GATEWAY'] = route['gateway'] + iface_cfg["GATEWAY"] = route["gateway"] route_cfg.has_set_default_ipv4 = True - if 'metric' in route: - iface_cfg['METRIC'] = route['metric'] + if "metric" in route: + iface_cfg["METRIC"] = route["metric"] else: - gw_key = 'GATEWAY%s' % route_cfg.last_idx - nm_key = 'NETMASK%s' % route_cfg.last_idx - addr_key = 'ADDRESS%s' % route_cfg.last_idx - metric_key = 'METRIC%s' % route_cfg.last_idx + gw_key = "GATEWAY%s" % route_cfg.last_idx + nm_key = "NETMASK%s" % route_cfg.last_idx + addr_key = "ADDRESS%s" % route_cfg.last_idx + metric_key = "METRIC%s" % route_cfg.last_idx route_cfg.last_idx += 1 # add default routes only to ifcfg files, not # to route-* or route6-* - for (old_key, new_key) in [('gateway', gw_key), - ('metric', metric_key), - ('netmask', nm_key), - ('network', addr_key)]: + for (old_key, new_key) in [ + ("gateway", gw_key), + ("metric", metric_key), + ("netmask", nm_key), + ("network", addr_key), + ]: if old_key in route: route_cfg[new_key] = route[old_key] @classmethod - def _render_bonding_opts(cls, iface_cfg, iface): + def _render_bonding_opts(cls, iface_cfg, iface, flavor): bond_opts = [] for (bond_key, value_tpl) in cls.bond_tpl_opts: # Seems like either dash or underscore is possible? @@ -628,22 +709,35 @@ class Renderer(renderer.Renderer): bond_opts.append(value_tpl % (bond_value)) break if bond_opts: - iface_cfg['BONDING_OPTS'] = " ".join(bond_opts) + if flavor == "suse": + # suse uses the sysconfig support which requires + # BONDING_MODULE_OPTS see + # https://www.kernel.org/doc/Documentation/networking/bonding.txt + # 3.1 Configuration with Sysconfig Support + iface_cfg["BONDING_MODULE_OPTS"] = " ".join(bond_opts) + else: + # rhel uses initscript support and thus requires BONDING_OPTS + # this is also the old default see + # https://www.kernel.org/doc/Documentation/networking/bonding.txt + # 3.2 Configuration with Initscripts Support + iface_cfg["BONDING_OPTS"] = " ".join(bond_opts) @classmethod def _render_physical_interfaces( - cls, network_state, iface_contents, flavor + 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'] + iface_name = iface["name"] iface_subnets = iface.get("subnets", []) iface_cfg = iface_contents[iface_name] route_cfg = iface_cfg.routes cls._render_subnets( - iface_cfg, iface_subnets, network_state.has_default_route, - flavor + iface_cfg, + iface_subnets, + network_state.has_default_route, + flavor, ) cls._render_subnet_routes( iface_cfg, route_cfg, iface_subnets, flavor @@ -651,33 +745,35 @@ class Renderer(renderer.Renderer): @classmethod 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') + bond_filter = renderer.filter_by_type("bond") + slave_filter = renderer.filter_by_attr("bond-master") for iface in network_state.iter_interfaces(bond_filter): - iface_name = iface['name'] + iface_name = iface["name"] iface_cfg = iface_contents[iface_name] - cls._render_bonding_opts(iface_cfg, iface) + cls._render_bonding_opts(iface_cfg, iface, flavor) # Ensure that the master interface (and any of its children) # are actually marked as being bond types... master_cfgs = [iface_cfg] master_cfgs.extend(iface_cfg.children) for master_cfg in master_cfgs: - master_cfg['BONDING_MASTER'] = True - if flavor != 'suse': - master_cfg.kind = 'bond' + master_cfg["BONDING_MASTER"] = True + if flavor != "suse": + master_cfg.kind = "bond" - if iface.get('mac_address'): - if flavor == 'suse': - iface_cfg['LLADDR'] = iface.get('mac_address') + if iface.get("mac_address"): + if flavor == "suse": + iface_cfg["LLADDR"] = iface.get("mac_address") else: - iface_cfg['MACADDR'] = iface.get('mac_address') + iface_cfg["MACADDR"] = iface.get("mac_address") iface_subnets = iface.get("subnets", []) route_cfg = iface_cfg.routes cls._render_subnets( - iface_cfg, iface_subnets, network_state.has_default_route, - flavor + iface_cfg, + iface_subnets, + network_state.has_default_route, + flavor, ) cls._render_subnet_routes( iface_cfg, route_cfg, iface_subnets, flavor @@ -686,54 +782,64 @@ class Renderer(renderer.Renderer): # iter_interfaces on network-state is not sorted to produce # consistent numbers we need to sort. bond_slaves = sorted( - [slave_iface['name'] for slave_iface in - network_state.iter_interfaces(slave_filter) - if slave_iface['bond-master'] == iface_name]) + [ + slave_iface["name"] + for slave_iface in network_state.iter_interfaces( + slave_filter + ) + if slave_iface["bond-master"] == iface_name + ] + ) for index, bond_slave in enumerate(bond_slaves): - if flavor == 'suse': - slavestr = 'BONDING_SLAVE_%s' % index + if flavor == "suse": + slavestr = "BONDING_SLAVE_%s" % index else: - slavestr = 'BONDING_SLAVE%s' % index + slavestr = "BONDING_SLAVE%s" % index iface_cfg[slavestr] = bond_slave slave_cfg = iface_contents[bond_slave] - if flavor == 'suse': - slave_cfg['BOOTPROTO'] = 'none' - slave_cfg['STARTMODE'] = 'hotplug' + if flavor == "suse": + slave_cfg["BOOTPROTO"] = "none" + slave_cfg["STARTMODE"] = "hotplug" else: - slave_cfg['MASTER'] = iface_name - slave_cfg['SLAVE'] = True + slave_cfg["MASTER"] = iface_name + slave_cfg["SLAVE"] = True @classmethod def _render_vlan_interfaces(cls, network_state, iface_contents, flavor): - vlan_filter = renderer.filter_by_type('vlan') + vlan_filter = renderer.filter_by_type("vlan") for iface in network_state.iter_interfaces(vlan_filter): - iface_name = iface['name'] + iface_name = iface["name"] iface_cfg = iface_contents[iface_name] - if flavor == 'suse': - vlan_id = iface.get('vlan_id') + 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('.')] + iface_cfg["VLAN_ID"] = vlan_id + iface_cfg["ETHERDEVICE"] = iface_name[: iface_name.rfind(".")] else: - iface_cfg['VLAN'] = True - iface_cfg.kind = 'vlan' + iface_cfg["VLAN"] = True + iface_cfg.kind = "vlan" - rdev = iface['vlan-raw-device'] - supported = _supported_vlan_names(rdev, iface['vlan_id']) + rdev = iface["vlan-raw-device"] + supported = _supported_vlan_names(rdev, iface["vlan_id"]) if iface_name not in supported: LOG.info( "Name '%s' for vlan '%s' is not officially supported" "by RHEL. Supported: %s", - iface_name, rdev, ' '.join(supported)) - iface_cfg['PHYSDEV'] = rdev + iface_name, + rdev, + " ".join(supported), + ) + iface_cfg["PHYSDEV"] = rdev iface_subnets = iface.get("subnets", []) route_cfg = iface_cfg.routes cls._render_subnets( - iface_cfg, iface_subnets, network_state.has_default_route, - flavor + iface_cfg, + iface_subnets, + network_state.has_default_route, + flavor, ) cls._render_subnet_routes( iface_cfg, route_cfg, iface_subnets, flavor @@ -742,8 +848,12 @@ class Renderer(renderer.Renderer): @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)]): + 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): @@ -752,10 +862,10 @@ class Renderer(renderer.Renderer): content.add_nameserver(nameserver) for searchdomain in network_state.dns_searchdomains: content.add_search_domain(searchdomain) - header = _make_header(';') + header = _make_header(";") content_str = str(content) if not content_str.startswith(header): - content_str = header + '\n' + content_str + content_str = header + "\n" + content_str return content_str @staticmethod @@ -766,7 +876,7 @@ class Renderer(renderer.Renderer): # NetworkManager to not manage dns, so that /etc/resolv.conf # does not get clobbered. if network_state.dns_nameservers: - content.set_section_keypair('main', 'dns', 'none') + content.set_section_keypair("main", "dns", "none") if len(content) == 0: return None @@ -776,39 +886,41 @@ class Renderer(renderer.Renderer): @classmethod 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') + 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_name = iface["name"] iface_cfg = iface_contents[iface_name] - if flavor != 'suse': - iface_cfg.kind = 'bridge' + 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 flavor == 'suse': - if 'BRIDGE_STP' in iface_cfg: - if iface_cfg.get('BRIDGE_STP'): - iface_cfg['BRIDGE_STP'] = 'on' + 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') + 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', []): + for bridged_iface_name in iface.get("bridge_ports", []): # Ensure all bridged interfaces are correctly tagged # as being bridged to this interface. bridged_cfg = iface_contents[bridged_iface_name] @@ -816,15 +928,17 @@ class Renderer(renderer.Renderer): bridged_cfgs.extend(bridged_cfg.children) for bridge_cfg in bridged_cfgs: bridge_value = iface_name - if flavor == 'suse': - bridge_value = 'yes' - bridge_cfg['BRIDGE'] = bridge_value + 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, network_state.has_default_route, - flavor + iface_cfg, + iface_subnets, + network_state.has_default_route, + flavor, ) cls._render_subnet_routes( iface_cfg, route_cfg, iface_subnets, flavor @@ -832,37 +946,40 @@ class Renderer(renderer.Renderer): @classmethod def _render_ib_interfaces(cls, network_state, iface_contents, flavor): - ib_filter = renderer.filter_by_type('infiniband') + ib_filter = renderer.filter_by_type("infiniband") for iface in network_state.iter_interfaces(ib_filter): - iface_name = iface['name'] + iface_name = iface["name"] iface_cfg = iface_contents[iface_name] - iface_cfg.kind = 'infiniband' + iface_cfg.kind = "infiniband" iface_subnets = iface.get("subnets", []) route_cfg = iface_cfg.routes cls._render_subnets( - iface_cfg, iface_subnets, network_state.has_default_route, - flavor + 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, flavor, - templates=None): - '''Given state, return /etc/sysconfig files + contents''' + def _render_sysconfig( + cls, base_sysconf_dir, network_state, flavor, templates=None + ): + """Given state, return /etc/sysconfig files + contents""" if not templates: templates = cls.templates iface_contents = {} for iface in network_state.iter_interfaces(): - if iface['type'] == "loopback": + if iface["type"] == "loopback": continue - iface_name = iface['name'] + iface_name = iface["name"] iface_cfg = NetInterface(iface_name, base_sysconf_dir, templates) - if flavor == 'suse': - iface_cfg.drop('DEVICE') + if flavor == "suse": + iface_cfg.drop("DEVICE") # If type detection fails it is considered a bug in SUSE - iface_cfg.drop('TYPE') + 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, flavor) @@ -878,9 +995,10 @@ class Renderer(renderer.Renderer): if iface_cfg: contents[iface_cfg.path] = iface_cfg.to_string() if iface_cfg.routes: - for cpath, proto in zip([iface_cfg.routes.path_ipv4, - iface_cfg.routes.path_ipv6], - ["ipv4", "ipv6"]): + for cpath, proto in zip( + [iface_cfg.routes.path_ipv4, iface_cfg.routes.path_ipv6], + ["ipv4", "ipv6"], + ): if cpath not in contents: contents[cpath] = iface_cfg.routes.to_string(proto) return contents @@ -890,21 +1008,24 @@ class Renderer(renderer.Renderer): templates = self.templates file_mode = 0o644 base_sysconf_dir = subp.target_path(target, self.sysconf_dir) - for path, data in self._render_sysconfig(base_sysconf_dir, - network_state, self.flavor, - templates=templates).items(): + for path, data in self._render_sysconfig( + base_sysconf_dir, network_state, self.flavor, templates=templates + ).items(): util.write_file(path, data, file_mode) if self.dns_path: dns_path = subp.target_path(target, self.dns_path) - resolv_content = self._render_dns(network_state, - existing_dns_path=dns_path) + resolv_content = self._render_dns( + network_state, existing_dns_path=dns_path + ) if resolv_content: util.write_file(dns_path, resolv_content, file_mode) if self.networkmanager_conf_path: - nm_conf_path = subp.target_path(target, - self.networkmanager_conf_path) - nm_conf_content = self._render_networkmanager_conf(network_state, - templates) + nm_conf_path = subp.target_path( + target, self.networkmanager_conf_path + ) + nm_conf_content = self._render_networkmanager_conf( + network_state, templates + ) if nm_conf_content: util.write_file(nm_conf_path, nm_conf_content, file_mode) if self.netrules_path: @@ -914,16 +1035,17 @@ class Renderer(renderer.Renderer): if available_nm(target=target): enable_ifcfg_rh(subp.target_path(target, path=NM_CFG_FILE)) - sysconfig_path = subp.target_path(target, templates.get('control')) + sysconfig_path = subp.target_path(target, templates.get("control")) # Distros configuring /etc/sysconfig/network as a file e.g. Centos - if sysconfig_path.endswith('network'): + if sysconfig_path.endswith("network"): util.ensure_dir(os.path.dirname(sysconfig_path)) - netcfg = [_make_header(), 'NETWORKING=yes'] + netcfg = [_make_header(), "NETWORKING=yes"] if network_state.use_ipv6: - netcfg.append('NETWORKING_IPV6=yes') - netcfg.append('IPV6_AUTOCONF=no') - util.write_file(sysconfig_path, - "\n".join(netcfg) + "\n", file_mode) + netcfg.append("NETWORKING_IPV6=yes") + netcfg.append("IPV6_AUTOCONF=no") + util.write_file( + sysconfig_path, "\n".join(netcfg) + "\n", file_mode + ) def _supported_vlan_names(rdev, vid): @@ -931,27 +1053,34 @@ def _supported_vlan_names(rdev, vid): 11.5. Naming Scheme for VLAN Interfaces.""" return [ v.format(rdev=rdev, vid=int(vid)) - for v in ("{rdev}{vid:04}", "{rdev}{vid}", - "{rdev}.{vid:04}", "{rdev}.{vid}")] + for v in ( + "{rdev}{vid:04}", + "{rdev}{vid}", + "{rdev}.{vid:04}", + "{rdev}.{vid}", + ) + ] 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])) + return util.system_info()["variant"] in KNOWN_DISTROS and any( + [nm, sysconfig] + ) def available_sysconfig(target=None): - expected = ['ifup', 'ifdown'] - search = ['/sbin', '/usr/sbin'] + expected = ["ifup", "ifdown"] + search = ["/sbin", "/usr/sbin"] for p in expected: if not subp.which(p, search=search, target=target): return False expected_paths = [ - 'etc/sysconfig/network-scripts/network-functions', - 'etc/sysconfig/config'] + "etc/sysconfig/network-scripts/network-functions", + "etc/sysconfig/config", + ] for p in expected_paths: if os.path.isfile(subp.target_path(target, p)): return True diff --git a/cloudinit/net/tests/__init__.py b/cloudinit/net/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/cloudinit/net/tests/__init__.py +++ /dev/null diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py deleted file mode 100644 index 74cf4b94..00000000 --- a/cloudinit/net/tests/test_dhcp.py +++ /dev/null @@ -1,634 +0,0 @@ -# This file is part of cloud-init. See LICENSE file for license information. - -import httpretty -import os -import signal -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_static_routes) -from cloudinit.util import ensure_file, write_file -from cloudinit.tests.helpers import ( - CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call) - - -class TestParseDHCPLeasesFile(CiTestCase): - - def test_parse_empty_lease_file_errors(self): - """parse_dhcp_lease_file errors when file content is empty.""" - empty_file = self.tmp_path('leases') - ensure_file(empty_file) - with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager: - parse_dhcp_lease_file(empty_file) - error = context_manager.exception - self.assertIn('Cannot parse empty dhcp lease file', str(error)) - - def test_parse_malformed_lease_file_content_errors(self): - """parse_dhcp_lease_file errors when file content isn't dhcp leases.""" - non_lease_file = self.tmp_path('leases') - write_file(non_lease_file, 'hi mom.') - with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager: - parse_dhcp_lease_file(non_lease_file) - error = context_manager.exception - self.assertIn('Cannot parse dhcp lease file', str(error)) - - def test_parse_multiple_leases(self): - """parse_dhcp_lease_file returns a list of all leases within.""" - 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; - renew 4 2017/07/27 18:02:30; - expire 5 2017/07/28 07:08:15; - } - lease { - interface "wlp3s0"; - fixed-address 192.168.2.74; - option subnet-mask 255.255.255.0; - option routers 192.168.2.1; - } - """) - expected = [ - {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74', - 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1', - 'renew': '4 2017/07/27 18:02:30', - 'expire': '5 2017/07/28 07:08:15'}, - {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74', - 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}] - write_file(lease_file, content) - self.assertCountEqual(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.assertCountEqual(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.assertCountEqual(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 - - @mock.patch('cloudinit.net.dhcp.find_fallback_nic') - def test_no_fallback_nic_found(self, m_fallback_nic): - """Log and do nothing when nic is absent and no fallback is found.""" - m_fallback_nic.return_value = None # No fallback nic found - self.assertEqual([], maybe_perform_dhcp_discovery()) - self.assertIn( - 'Skip dhcp_discovery: Unable to find fallback nic.', - self.logs.getvalue()) - - def test_provided_nic_does_not_exist(self): - """When the provided nic doesn't exist, log a message and no-op.""" - self.assertEqual([], maybe_perform_dhcp_discovery('idontexist')) - self.assertIn( - 'Skip dhcp_discovery: nic idontexist not found in get_devicelist.', - self.logs.getvalue()) - - @mock.patch('cloudinit.net.dhcp.subp.which') - @mock.patch('cloudinit.net.dhcp.find_fallback_nic') - def test_absent_dhclient_command(self, m_fallback, m_which): - """When dhclient doesn't exist in the OS, log the issue and no-op.""" - m_fallback.return_value = 'eth9' - m_which.return_value = None # dhclient isn't found - self.assertEqual([], maybe_perform_dhcp_discovery()) - self.assertIn( - 'Skip dhclient configuration: No dhclient command found.', - self.logs.getvalue()) - - @mock.patch('cloudinit.temp_utils.os.getuid') - @mock.patch('cloudinit.net.dhcp.dhcp_discovery') - @mock.patch('cloudinit.net.dhcp.subp.which') - @mock.patch('cloudinit.net.dhcp.find_fallback_nic') - def test_dhclient_run_with_tmpdir(self, m_fback, m_which, m_dhcp, m_uid): - """maybe_perform_dhcp_discovery passes tmpdir to dhcp_discovery.""" - m_uid.return_value = 0 # Fake root user for tmpdir - m_fback.return_value = 'eth9' - m_which.return_value = '/sbin/dhclient' - m_dhcp.return_value = {'address': '192.168.2.2'} - retval = wrap_and_call( - 'cloudinit.temp_utils', - {'_TMPDIR': {'new': None}, - 'os.getuid': 0}, - maybe_perform_dhcp_discovery) - self.assertEqual({'address': '192.168.2.2'}, retval) - self.assertEqual( - 1, m_dhcp.call_count, 'dhcp_discovery not called once') - call = m_dhcp.call_args_list[0] - self.assertEqual('/sbin/dhclient', call[0][0]) - 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.subp.subp') - def test_dhcp_discovery_run_in_sandbox_warns_invalid_pid(self, m_subp, - m_kill): - """dhcp_discovery logs a warning when pidfile contains invalid content. - - Lease processing still occurs and no proc kill is attempted. - """ - m_subp.return_value = ('', '') - tmpdir = self.tmp_dir() - dhclient_script = os.path.join(tmpdir, 'dhclient.orig') - script_content = '#!/bin/bash\necho fake-dhclient' - write_file(dhclient_script, script_content, mode=0o755) - write_file(self.tmp_path('dhclient.pid', tmpdir), '') # Empty pid '' - lease_content = dedent(""" - lease { - interface "eth9"; - fixed-address 192.168.2.74; - option subnet-mask 255.255.255.0; - option routers 192.168.2.1; - } - """) - write_file(self.tmp_path('dhcp.leases', tmpdir), lease_content) - - self.assertCountEqual( - [{'interface': 'eth9', 'fixed-address': '192.168.2.74', - 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], - dhcp_discovery(dhclient_script, 'eth9', tmpdir)) - self.assertIn( - "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.subp.subp') - def test_dhcp_discovery_run_in_sandbox_waits_on_lease_and_pid(self, - m_subp, - m_wait, - m_kill, - m_getppid): - """dhcp_discovery waits for the presence of pidfile and dhcp.leases.""" - m_subp.return_value = ('', '') - tmpdir = self.tmp_dir() - dhclient_script = os.path.join(tmpdir, 'dhclient.orig') - script_content = '#!/bin/bash\necho fake-dhclient' - write_file(dhclient_script, script_content, mode=0o755) - # Don't create pid or leases file - 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), - m_wait.call_args_list[0]) - self.assertIn( - 'WARNING: dhclient did not produce expected files: dhclient.pid', - 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.subp.subp') - 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. - """ - m_subp.return_value = ('', '') - tmpdir = self.tmp_dir() - dhclient_script = os.path.join(tmpdir, 'dhclient.orig') - script_content = '#!/bin/bash\necho fake-dhclient' - write_file(dhclient_script, script_content, mode=0o755) - lease_content = dedent(""" - lease { - interface "eth9"; - fixed-address 192.168.2.74; - option subnet-mask 255.255.255.0; - option routers 192.168.2.1; - } - """) - lease_file = os.path.join(tmpdir, 'dhcp.leases') - write_file(lease_file, lease_content) - 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.assertCountEqual( - [{'interface': 'eth9', 'fixed-address': '192.168.2.74', - 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], - dhcp_discovery(dhclient_script, 'eth9', tmpdir)) - # dhclient script got copied - with open(os.path.join(tmpdir, 'dhclient')) as stream: - self.assertEqual(script_content, stream.read()) - # Interface was brought up before dhclient called from sandbox - m_subp.assert_has_calls([ - mock.call( - ['ip', 'link', 'set', 'dev', 'eth9', 'up'], capture=True), - mock.call( - [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf', - lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'), - 'eth9', '-sf', '/bin/true'], capture=True)]) - m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)]) - - @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid') - @mock.patch('cloudinit.net.dhcp.os.kill') - @mock.patch('cloudinit.net.dhcp.subp.subp') - def test_dhcp_discovery_outside_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. - """ - m_subp.return_value = ('', '') - tmpdir = self.tmp_dir() - dhclient_script = os.path.join(tmpdir, 'dhclient.orig') - script_content = '#!/bin/bash\necho fake-dhclient' - write_file(dhclient_script, script_content, mode=0o755) - lease_content = dedent(""" - lease { - interface "eth9"; - fixed-address 192.168.2.74; - option subnet-mask 255.255.255.0; - option routers 192.168.2.1; - } - """) - lease_file = os.path.join(tmpdir, 'dhcp.leases') - write_file(lease_file, lease_content) - 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 - - with mock.patch('os.access', return_value=False): - self.assertCountEqual( - [{'interface': 'eth9', 'fixed-address': '192.168.2.74', - 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], - dhcp_discovery(dhclient_script, 'eth9', tmpdir)) - # dhclient script got copied - with open(os.path.join(tmpdir, 'dhclient.orig')) as stream: - self.assertEqual(script_content, stream.read()) - # Interface was brought up before dhclient called from sandbox - m_subp.assert_has_calls([ - mock.call( - ['ip', 'link', 'set', 'dev', 'eth9', 'up'], capture=True), - mock.call( - [os.path.join(tmpdir, 'dhclient.orig'), '-1', '-v', '-lf', - lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'), - 'eth9', '-sf', '/bin/true'], capture=True)]) - m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)]) - - @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid') - @mock.patch('cloudinit.net.dhcp.os.kill') - @mock.patch('cloudinit.net.dhcp.subp.subp') - def test_dhcp_output_error_stream(self, m_subp, m_kill, m_getppid): - """"dhcp_log_func is called with the output and error streams of - dhclinet when the callable is passed.""" - dhclient_err = 'FAKE DHCLIENT ERROR' - dhclient_out = 'FAKE DHCLIENT OUT' - m_subp.return_value = (dhclient_out, dhclient_err) - tmpdir = self.tmp_dir() - dhclient_script = os.path.join(tmpdir, 'dhclient.orig') - script_content = '#!/bin/bash\necho fake-dhclient' - write_file(dhclient_script, script_content, mode=0o755) - lease_content = dedent(""" - lease { - interface "eth9"; - fixed-address 192.168.2.74; - option subnet-mask 255.255.255.0; - option routers 192.168.2.1; - } - """) - lease_file = os.path.join(tmpdir, 'dhcp.leases') - write_file(lease_file, lease_content) - 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 - - def dhcp_log_func(out, err): - self.assertEqual(out, dhclient_out) - self.assertEqual(err, dhclient_err) - - dhcp_discovery( - dhclient_script, 'eth9', tmpdir, dhcp_log_func=dhcp_log_func) - - -class TestSystemdParseLeases(CiTestCase): - - lxd_lease = dedent("""\ - # This is private data. Do not parse. - ADDRESS=10.75.205.242 - NETMASK=255.255.255.0 - ROUTER=10.75.205.1 - SERVER_ADDRESS=10.75.205.1 - NEXT_SERVER=10.75.205.1 - BROADCAST=10.75.205.255 - T1=1580 - T2=2930 - LIFETIME=3600 - DNS=10.75.205.1 - DOMAINNAME=lxd - HOSTNAME=a1 - CLIENTID=ffe617693400020000ab110c65a6a0866931c2 - """) - - lxd_parsed = { - 'ADDRESS': '10.75.205.242', - 'NETMASK': '255.255.255.0', - 'ROUTER': '10.75.205.1', - 'SERVER_ADDRESS': '10.75.205.1', - 'NEXT_SERVER': '10.75.205.1', - 'BROADCAST': '10.75.205.255', - 'T1': '1580', - 'T2': '2930', - 'LIFETIME': '3600', - 'DNS': '10.75.205.1', - 'DOMAINNAME': 'lxd', - 'HOSTNAME': 'a1', - 'CLIENTID': 'ffe617693400020000ab110c65a6a0866931c2', - } - - azure_lease = dedent("""\ - # This is private data. Do not parse. - ADDRESS=10.132.0.5 - NETMASK=255.255.255.255 - ROUTER=10.132.0.1 - SERVER_ADDRESS=169.254.169.254 - NEXT_SERVER=10.132.0.1 - MTU=1460 - T1=43200 - T2=75600 - LIFETIME=86400 - DNS=169.254.169.254 - NTP=169.254.169.254 - DOMAINNAME=c.ubuntu-foundations.internal - DOMAIN_SEARCH_LIST=c.ubuntu-foundations.internal google.internal - HOSTNAME=tribaal-test-171002-1349.c.ubuntu-foundations.internal - ROUTES=10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1 - CLIENTID=ff405663a200020000ab11332859494d7a8b4c - OPTION_245=624c3620 - """) - - azure_parsed = { - 'ADDRESS': '10.132.0.5', - 'NETMASK': '255.255.255.255', - 'ROUTER': '10.132.0.1', - 'SERVER_ADDRESS': '169.254.169.254', - 'NEXT_SERVER': '10.132.0.1', - 'MTU': '1460', - 'T1': '43200', - 'T2': '75600', - 'LIFETIME': '86400', - 'DNS': '169.254.169.254', - 'NTP': '169.254.169.254', - 'DOMAINNAME': 'c.ubuntu-foundations.internal', - 'DOMAIN_SEARCH_LIST': 'c.ubuntu-foundations.internal google.internal', - 'HOSTNAME': 'tribaal-test-171002-1349.c.ubuntu-foundations.internal', - 'ROUTES': '10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1', - 'CLIENTID': 'ff405663a200020000ab11332859494d7a8b4c', - 'OPTION_245': '624c3620'} - - def setUp(self): - super(TestSystemdParseLeases, self).setUp() - self.lease_d = self.tmp_dir() - - def test_no_leases_returns_empty_dict(self): - """A leases dir with no lease files should return empty dictionary.""" - self.assertEqual({}, networkd_load_leases(self.lease_d)) - - def test_no_leases_dir_returns_empty_dict(self): - """A non-existing leases dir should return empty dict.""" - enodir = os.path.join(self.lease_d, 'does-not-exist') - self.assertEqual({}, networkd_load_leases(enodir)) - - def test_single_leases_file(self): - """A leases dir with one leases file.""" - populate_dir(self.lease_d, {'2': self.lxd_lease}) - self.assertEqual( - {'2': self.lxd_parsed}, networkd_load_leases(self.lease_d)) - - def test_single_azure_leases_file(self): - """On Azure, option 245 should be present, verify it specifically.""" - populate_dir(self.lease_d, {'1': self.azure_lease}) - self.assertEqual( - {'1': self.azure_parsed}, networkd_load_leases(self.lease_d)) - - def test_multiple_files(self): - """Multiple leases files on azure with one found return that value.""" - self.maxDiff = None - populate_dir(self.lease_d, {'1': self.azure_lease, - '9': self.lxd_lease}) - self.assertEqual({'1': self.azure_parsed, '9': self.lxd_parsed}, - networkd_load_leases(self.lease_d)) - - -class TestEphemeralDhcpNoNetworkSetup(HttprettyTestCase): - - @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') - def test_ephemeral_dhcp_no_network_if_url_connectivity(self, m_dhcp): - """No EphemeralDhcp4 network setup when connectivity_url succeeds.""" - url = 'http://example.org/index.html' - - httpretty.register_uri(httpretty.GET, url) - with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease: - self.assertIsNone(lease) - # Ensure that no teardown happens: - m_dhcp.assert_not_called() - - @mock.patch('cloudinit.net.dhcp.subp.subp') - @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') - def test_ephemeral_dhcp_setup_network_if_url_connectivity( - self, m_dhcp, m_subp): - """No EphemeralDhcp4 network setup when connectivity_url succeeds.""" - url = 'http://example.org/index.html' - fake_lease = { - 'interface': 'eth9', 'fixed-address': '192.168.2.2', - 'subnet-mask': '255.255.0.0'} - m_dhcp.return_value = [fake_lease] - m_subp.return_value = ('', '') - - httpretty.register_uri(httpretty.GET, url, body={}, status=404) - with net.dhcp.EphemeralDHCPv4(connectivity_url=url) as lease: - 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 deleted file mode 100644 index 0535387a..00000000 --- a/cloudinit/net/tests/test_init.py +++ /dev/null @@ -1,1270 +0,0 @@ -# This file is part of cloud-init. See LICENSE file for license information. - -import copy -import errno -import ipaddress -import os -import textwrap -from unittest import mock - -import httpretty -import pytest -import requests - -import cloudinit.net as net -from cloudinit import safeyaml as yaml -from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase -from cloudinit.subp import ProcessExecutionError -from cloudinit.util import ensure_file, write_file - - -class TestSysDevPath(CiTestCase): - - def test_sys_dev_path(self): - """sys_dev_path returns a path under SYS_CLASS_NET for a device.""" - dev = 'something' - path = 'attribute' - expected = net.SYS_CLASS_NET + dev + '/' + path - self.assertEqual(expected, net.sys_dev_path(dev, path)) - - def test_sys_dev_path_without_path(self): - """When path param isn't provided it defaults to empty string.""" - dev = 'something' - expected = net.SYS_CLASS_NET + dev + '/' - self.assertEqual(expected, net.sys_dev_path(dev)) - - -class TestReadSysNet(CiTestCase): - with_logs = True - - def setUp(self): - super(TestReadSysNet, self).setUp() - sys_mock = mock.patch('cloudinit.net.get_sys_class_path') - self.m_sys_path = sys_mock.start() - self.sysdir = self.tmp_dir() + '/' - self.m_sys_path.return_value = self.sysdir - self.addCleanup(sys_mock.stop) - - def test_read_sys_net_strips_contents_of_sys_path(self): - """read_sys_net strips whitespace from the contents of a sys file.""" - content = 'some stuff with trailing whitespace\t\r\n' - write_file(os.path.join(self.sysdir, 'dev', 'attr'), content) - self.assertEqual(content.strip(), net.read_sys_net('dev', 'attr')) - - def test_read_sys_net_reraises_oserror(self): - """read_sys_net raises OSError/IOError when file doesn't exist.""" - # Non-specific Exception because versions of python OSError vs IOError. - with self.assertRaises(Exception) as context_manager: # noqa: H202 - net.read_sys_net('dev', 'attr') - error = context_manager.exception - self.assertIn('No such file or directory', str(error)) - - def test_read_sys_net_handles_error_with_on_enoent(self): - """read_sys_net handles OSError/IOError with on_enoent if provided.""" - handled_errors = [] - - def on_enoent(e): - handled_errors.append(e) - - net.read_sys_net('dev', 'attr', on_enoent=on_enoent) - error = handled_errors[0] - self.assertIsInstance(error, Exception) - self.assertIn('No such file or directory', str(error)) - - def test_read_sys_net_translates_content(self): - """read_sys_net translates content when translate dict is provided.""" - content = "you're welcome\n" - write_file(os.path.join(self.sysdir, 'dev', 'attr'), content) - translate = {"you're welcome": 'de nada'} - self.assertEqual( - 'de nada', - net.read_sys_net('dev', 'attr', translate=translate)) - - def test_read_sys_net_errors_on_translation_failures(self): - """read_sys_net raises a KeyError and logs details on failure.""" - content = "you're welcome\n" - write_file(os.path.join(self.sysdir, 'dev', 'attr'), content) - with self.assertRaises(KeyError) as context_manager: - net.read_sys_net('dev', 'attr', translate={}) - error = context_manager.exception - self.assertEqual('"you\'re welcome"', str(error)) - self.assertIn( - "Found unexpected (not translatable) value 'you're welcome' in " - "'{0}dev/attr".format(self.sysdir), - self.logs.getvalue()) - - def test_read_sys_net_handles_handles_with_onkeyerror(self): - """read_sys_net handles translation errors calling on_keyerror.""" - content = "you're welcome\n" - write_file(os.path.join(self.sysdir, 'dev', 'attr'), content) - handled_errors = [] - - def on_keyerror(e): - handled_errors.append(e) - - net.read_sys_net('dev', 'attr', translate={}, on_keyerror=on_keyerror) - error = handled_errors[0] - self.assertIsInstance(error, KeyError) - self.assertEqual('"you\'re welcome"', str(error)) - - def test_read_sys_net_safe_false_on_translate_failure(self): - """read_sys_net_safe returns False on translation failures.""" - content = "you're welcome\n" - write_file(os.path.join(self.sysdir, 'dev', 'attr'), content) - self.assertFalse(net.read_sys_net_safe('dev', 'attr', translate={})) - - def test_read_sys_net_safe_returns_false_on_noent_failure(self): - """read_sys_net_safe returns False on file not found failures.""" - self.assertFalse(net.read_sys_net_safe('dev', 'attr')) - - def test_read_sys_net_int_returns_none_on_error(self): - """read_sys_net_safe returns None on failures.""" - self.assertFalse(net.read_sys_net_int('dev', 'attr')) - - def test_read_sys_net_int_returns_none_on_valueerror(self): - """read_sys_net_safe returns None when content is not an int.""" - write_file(os.path.join(self.sysdir, 'dev', 'attr'), 'NOTINT\n') - self.assertFalse(net.read_sys_net_int('dev', 'attr')) - - def test_read_sys_net_int_returns_integer_from_content(self): - """read_sys_net_safe returns None on failures.""" - write_file(os.path.join(self.sysdir, 'dev', 'attr'), '1\n') - self.assertEqual(1, net.read_sys_net_int('dev', 'attr')) - - def test_is_up_true(self): - """is_up is True if sys/net/devname/operstate is 'up' or 'unknown'.""" - for state in ['up', 'unknown']: - write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state) - self.assertTrue(net.is_up('eth0')) - - def test_is_up_false(self): - """is_up is False if sys/net/devname/operstate is 'down' or invalid.""" - for state in ['down', 'incomprehensible']: - write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state) - self.assertFalse(net.is_up('eth0')) - - def test_is_bridge(self): - """is_bridge is True when /sys/net/devname/bridge exists.""" - self.assertFalse(net.is_bridge('eth0')) - ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge')) - self.assertTrue(net.is_bridge('eth0')) - - def test_is_bond(self): - """is_bond is True when /sys/net/devname/bonding exists.""" - self.assertFalse(net.is_bond('eth0')) - ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding')) - self.assertTrue(net.is_bond('eth0')) - - def test_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_master_is_openvswitch(self): - ovs_mac = 'bb:cc:aa:bb:cc:aa' - - # No master => False - write_file(os.path.join(self.sysdir, 'eth1', 'address'), ovs_mac) - - self.assertFalse(net.master_is_bridge_or_bond('eth1')) - - # masters without ovs-system => False - write_file(os.path.join(self.sysdir, 'ovs-system', 'address'), ovs_mac) - - os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth1', - 'master')) - - self.assertFalse(net.master_is_openvswitch('eth1')) - - # masters with ovs-system => True - os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth1', - 'upper_ovs-system')) - - self.assertTrue(net.master_is_openvswitch('eth1')) - - def test_is_vlan(self): - """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan.""" - ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent')) - self.assertFalse(net.is_vlan('eth0')) - content = 'junk\nDEVTYPE=vlan\njunk\n' - write_file(os.path.join(self.sysdir, 'eth0', 'uevent'), content) - self.assertTrue(net.is_vlan('eth0')) - - -class TestGenerateFallbackConfig(CiTestCase): - - def setUp(self): - super(TestGenerateFallbackConfig, self).setUp() - sys_mock = mock.patch('cloudinit.net.get_sys_class_path') - self.m_sys_path = sys_mock.start() - self.sysdir = self.tmp_dir() + '/' - self.m_sys_path.return_value = self.sysdir - self.addCleanup(sys_mock.stop) - self.add_patch('cloudinit.net.util.is_container', 'm_is_container', - return_value=False) - self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle') - 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.""" - write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1') - write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1') - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac) - expected = { - '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): - """generate_fallback_config finds any dormant device with a mac.""" - write_file(os.path.join(self.sysdir, 'eth0', 'dormant'), '1') - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac) - expected = { - '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): - """generate_fallback_config finds any dormant device with a mac.""" - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac) - expected = { - '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) - self.assertEqual(expected, net.generate_fallback_config()) - write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'noworky') - self.assertIsNone(net.generate_fallback_config()) - - def test_generate_fallback_config_skips_veth(self): - """generate_fallback_config will skip any veth interfaces.""" - # A connected veth which gets ignored - write_file(os.path.join(self.sysdir, 'veth0', 'carrier'), '1') - self.assertIsNone(net.generate_fallback_config()) - - def test_generate_fallback_config_skips_bridges(self): - """generate_fallback_config will skip any bridges interfaces.""" - # A connected veth which gets ignored - write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1') - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac) - ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge')) - self.assertIsNone(net.generate_fallback_config()) - - def test_generate_fallback_config_skips_bonds(self): - """generate_fallback_config will skip any bonded interfaces.""" - # A connected veth which gets ignored - write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1') - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac) - ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding')) - self.assertIsNone(net.generate_fallback_config()) - - 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): - - 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): - - def setUp(self): - super(TestGetDeviceList, self).setUp() - sys_mock = mock.patch('cloudinit.net.get_sys_class_path') - self.m_sys_path = sys_mock.start() - self.sysdir = self.tmp_dir() + '/' - self.m_sys_path.return_value = self.sysdir - self.addCleanup(sys_mock.stop) - - def test_get_devicelist_raise_oserror(self): - """get_devicelist raise any non-ENOENT OSerror.""" - error = OSError('Can not do it') - error.errno = errno.EPERM # Set non-ENOENT - self.m_sys_path.side_effect = error - with self.assertRaises(OSError) as context_manager: - net.get_devicelist() - exception = context_manager.exception - self.assertEqual('Can not do it', str(exception)) - - def test_get_devicelist_empty_without_sys_net(self): - """get_devicelist returns empty list when missing SYS_CLASS_NET.""" - self.m_sys_path.return_value = 'idontexist' - self.assertEqual([], net.get_devicelist()) - - def test_get_devicelist_empty_with_no_devices_in_sys_net(self): - """get_devicelist returns empty directoty listing for SYS_CLASS_NET.""" - self.assertEqual([], net.get_devicelist()) - - def test_get_devicelist_lists_any_subdirectories_in_sys_net(self): - """get_devicelist returns a directory listing for SYS_CLASS_NET.""" - write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'up') - write_file(os.path.join(self.sysdir, 'eth1', 'operstate'), 'up') - self.assertCountEqual(['eth0', 'eth1'], net.get_devicelist()) - - -class TestGetInterfaceMAC(CiTestCase): - - def setUp(self): - super(TestGetInterfaceMAC, self).setUp() - sys_mock = mock.patch('cloudinit.net.get_sys_class_path') - self.m_sys_path = sys_mock.start() - self.sysdir = self.tmp_dir() + '/' - self.m_sys_path.return_value = self.sysdir - self.addCleanup(sys_mock.stop) - - def test_get_interface_mac_false_with_no_mac(self): - """get_device_list returns False when no mac is reported.""" - ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding')) - mac_path = os.path.join(self.sysdir, 'eth0', 'address') - self.assertFalse(os.path.exists(mac_path)) - self.assertFalse(net.get_interface_mac('eth0')) - - def test_get_interface_mac(self): - """get_interfaces returns the mac from SYS_CLASS_NET/dev/address.""" - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac) - self.assertEqual(mac, net.get_interface_mac('eth1')) - - def test_get_interface_mac_grabs_bonding_address(self): - """get_interfaces returns the source device mac for bonded devices.""" - source_dev_mac = 'aa:bb:cc:aa:bb:cc' - bonded_mac = 'dd:ee:ff:dd:ee:ff' - write_file(os.path.join(self.sysdir, 'eth1', 'address'), bonded_mac) - write_file( - os.path.join(self.sysdir, 'eth1', 'bonding_slave', 'perm_hwaddr'), - source_dev_mac) - self.assertEqual(source_dev_mac, net.get_interface_mac('eth1')) - - def test_get_interfaces_empty_list_without_sys_net(self): - """get_interfaces returns an empty list when missing SYS_CLASS_NET.""" - self.m_sys_path.return_value = 'idontexist' - self.assertEqual([], net.get_interfaces()) - - def test_get_interfaces_by_mac_skips_empty_mac(self): - """Ignore 00:00:00:00:00:00 addresses from get_interfaces_by_mac.""" - empty_mac = '00:00:00:00:00:00' - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth1', 'address'), empty_mac) - write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0') - write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0') - write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac) - expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)] - self.assertEqual(expected, net.get_interfaces()) - - def test_get_interfaces_by_mac_skips_missing_mac(self): - """Ignore interfaces without an address from get_interfaces_by_mac.""" - write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0') - address_path = os.path.join(self.sysdir, 'eth1', 'address') - self.assertFalse(os.path.exists(address_path)) - mac = 'aa:bb:cc:aa:bb:cc' - write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0') - write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac) - expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)] - self.assertEqual(expected, net.get_interfaces()) - - 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' - ovs_mac = 'bb:cc:aa:bb:cc: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, 'ovs-system', 'address'), - ovs_mac) - - 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')) - - write_file(os.path.join(self.sysdir, 'eth3', 'address'), ovs_mac) - os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth3', - 'master')) - os.symlink('../ovs-system', os.path.join(self.sysdir, 'eth3', - 'upper_ovs-system')) - - interface_names = [interface[0] for interface in net.get_interfaces()] - self.assertEqual(['eth1', 'eth2', 'eth3', 'ovs-system'], - sorted(interface_names)) - - -class TestInterfaceHasOwnMAC(CiTestCase): - - def setUp(self): - super(TestInterfaceHasOwnMAC, self).setUp() - sys_mock = mock.patch('cloudinit.net.get_sys_class_path') - self.m_sys_path = sys_mock.start() - self.sysdir = self.tmp_dir() + '/' - self.m_sys_path.return_value = self.sysdir - self.addCleanup(sys_mock.stop) - - def test_interface_has_own_mac_false_when_stolen(self): - """Return False from interface_has_own_mac when address is stolen.""" - write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '2') - self.assertFalse(net.interface_has_own_mac('eth1')) - - def test_interface_has_own_mac_true_when_not_stolen(self): - """Return False from interface_has_own_mac when mac isn't stolen.""" - valid_assign_types = ['0', '1', '3'] - assign_path = os.path.join(self.sysdir, 'eth1', 'addr_assign_type') - for _type in valid_assign_types: - write_file(assign_path, _type) - self.assertTrue(net.interface_has_own_mac('eth1')) - - def test_interface_has_own_mac_strict_errors_on_absent_assign_type(self): - """When addr_assign_type is absent, interface_has_own_mac errors.""" - with self.assertRaises(ValueError): - net.interface_has_own_mac('eth1', strict=True) - - -@mock.patch('cloudinit.net.subp.subp') -class TestEphemeralIPV4Network(CiTestCase): - - with_logs = True - - def setUp(self): - super(TestEphemeralIPV4Network, self).setUp() - sys_mock = mock.patch('cloudinit.net.get_sys_class_path') - self.m_sys_path = sys_mock.start() - self.sysdir = self.tmp_dir() + '/' - self.m_sys_path.return_value = self.sysdir - self.addCleanup(sys_mock.stop) - - def test_ephemeral_ipv4_network_errors_on_missing_params(self, m_subp): - """No required params for EphemeralIPv4Network can be None.""" - required_params = { - 'interface': 'eth0', 'ip': '192.168.2.2', - 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'} - for key in required_params.keys(): - params = copy.deepcopy(required_params) - params[key] = None - with self.assertRaises(ValueError) as context_manager: - net.EphemeralIPv4Network(**params) - error = context_manager.exception - self.assertIn('Cannot init network on', str(error)) - self.assertEqual(0, m_subp.call_count) - - def test_ephemeral_ipv4_network_errors_invalid_mask_prefix(self, m_subp): - """Raise an error when prefix_or_mask is not a netmask or prefix.""" - params = { - 'interface': 'eth0', 'ip': '192.168.2.2', - 'broadcast': '192.168.2.255'} - invalid_masks = ('invalid', 'invalid.', '123.123.123') - for error_val in invalid_masks: - params['prefix_or_mask'] = error_val - with self.assertRaises(ValueError) as context_manager: - with net.EphemeralIPv4Network(**params): - pass - error = context_manager.exception - self.assertIn('Cannot setup network: netmask', str(error)) - self.assertEqual(0, m_subp.call_count) - - def test_ephemeral_ipv4_network_performs_teardown(self, m_subp): - """EphemeralIPv4Network performs teardown on the device if setup.""" - expected_setup_calls = [ - mock.call( - ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24', - 'broadcast', '192.168.2.255', 'dev', 'eth0'], - capture=True, update_env={'LANG': 'C'}), - mock.call( - ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'], - capture=True)] - expected_teardown_calls = [ - mock.call( - ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', - 'down'], capture=True), - mock.call( - ['ip', '-family', 'inet', 'addr', 'del', '192.168.2.2/24', - 'dev', 'eth0'], capture=True)] - params = { - 'interface': 'eth0', 'ip': '192.168.2.2', - 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'} - with net.EphemeralIPv4Network(**params): - self.assertEqual(expected_setup_calls, m_subp.call_args_list) - m_subp.assert_has_calls(expected_teardown_calls) - - @mock.patch('cloudinit.net.readurl') - def test_ephemeral_ipv4_no_network_if_url_connectivity( - self, m_readurl, m_subp): - """No network setup is performed if we can successfully connect to - connectivity_url.""" - params = { - 'interface': 'eth0', 'ip': '192.168.2.2', - 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255', - 'connectivity_url': 'http://example.org/index.html'} - - with net.EphemeralIPv4Network(**params): - self.assertEqual([mock.call('http://example.org/index.html', - timeout=5)], m_readurl.call_args_list) - # Ensure that no teardown happens: - m_subp.assert_has_calls([]) - - def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp): - """EphemeralIPv4Network handles exception when address is setup. - - It performs no cleanup as the interface was already setup. - """ - params = { - 'interface': 'eth0', 'ip': '192.168.2.2', - 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'} - m_subp.side_effect = ProcessExecutionError( - '', 'RTNETLINK answers: File exists', 2) - expected_calls = [ - mock.call( - ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24', - 'broadcast', '192.168.2.255', 'dev', 'eth0'], - capture=True, update_env={'LANG': 'C'})] - with net.EphemeralIPv4Network(**params): - pass - self.assertEqual(expected_calls, m_subp.call_args_list) - self.assertIn( - 'Skip ephemeral network setup, eth0 already has address', - self.logs.getvalue()) - - def test_ephemeral_ipv4_network_with_prefix(self, m_subp): - """EphemeralIPv4Network takes a valid prefix to setup the network.""" - params = { - 'interface': 'eth0', 'ip': '192.168.2.2', - 'prefix_or_mask': '24', 'broadcast': '192.168.2.255'} - for prefix_val in ['24', 16]: # prefix can be int or string - params['prefix_or_mask'] = prefix_val - with net.EphemeralIPv4Network(**params): - pass - m_subp.assert_has_calls([mock.call( - ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24', - 'broadcast', '192.168.2.255', 'dev', 'eth0'], - capture=True, update_env={'LANG': 'C'})]) - m_subp.assert_has_calls([mock.call( - ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/16', - 'broadcast', '192.168.2.255', 'dev', 'eth0'], - capture=True, update_env={'LANG': 'C'})]) - - def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp): - """Add the route when router is set and no default route exists.""" - params = { - 'interface': 'eth0', 'ip': '192.168.2.2', - 'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255', - 'router': '192.168.2.1'} - m_subp.return_value = '', '' # Empty response from ip route gw check - expected_setup_calls = [ - mock.call( - ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24', - 'broadcast', '192.168.2.255', 'dev', 'eth0'], - capture=True, update_env={'LANG': 'C'}), - mock.call( - ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'], - capture=True), - mock.call( - ['ip', 'route', 'show', '0.0.0.0/0'], capture=True), - mock.call(['ip', '-4', 'route', 'add', '192.168.2.1', - 'dev', 'eth0', 'src', '192.168.2.2'], capture=True), - mock.call( - ['ip', '-4', 'route', 'add', 'default', 'via', - '192.168.2.1', 'dev', 'eth0'], capture=True)] - expected_teardown_calls = [ - mock.call(['ip', '-4', 'route', 'del', 'default', 'dev', 'eth0'], - capture=True), - mock.call(['ip', '-4', 'route', 'del', '192.168.2.1', - 'dev', 'eth0', 'src', '192.168.2.2'], capture=True), - ] - - with net.EphemeralIPv4Network(**params): - self.assertEqual(expected_setup_calls, m_subp.call_args_list) - m_subp.assert_has_calls(expected_teardown_calls) - - 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("""\ - version: 1 - config: - - type: physical - name: interface0 - mac_address: "52:54:00:12:34:00" - subnets: - - type: static - address: 10.0.2.15 - netmask: 255.255.255.0 - gateway: 10.0.2.2 - """) - V2_CONFIG = textwrap.dedent("""\ - version: 2 - ethernets: - interface0: - match: - macaddress: "52:54:00:12:34:00" - addresses: - - 10.0.2.15/24 - gateway4: 10.0.2.2 - set-name: interface0 - """) - - V2_CONFIG_NO_SETNAME = textwrap.dedent("""\ - version: 2 - ethernets: - interface0: - match: - macaddress: "52:54:00:12:34:00" - addresses: - - 10.0.2.15/24 - gateway4: 10.0.2.2 - """) - - V2_CONFIG_NO_MAC = textwrap.dedent("""\ - version: 2 - ethernets: - interface0: - match: - driver: virtio-net - addresses: - - 10.0.2.15/24 - gateway4: 10.0.2.2 - set-name: interface0 - """) - - @mock.patch('cloudinit.net.device_devid') - @mock.patch('cloudinit.net.device_driver') - @mock.patch('cloudinit.net._rename_interfaces') - def test_apply_v1_renames(self, m_rename_interfaces, m_device_driver, - m_device_devid): - m_device_driver.return_value = 'virtio_net' - m_device_devid.return_value = '0x15d8' - - net.apply_network_config_names(yaml.load(self.V1_CONFIG)) - - call = ['52:54:00:12:34:00', 'interface0', 'virtio_net', '0x15d8'] - m_rename_interfaces.assert_called_with([call]) - - @mock.patch('cloudinit.net.device_devid') - @mock.patch('cloudinit.net.device_driver') - @mock.patch('cloudinit.net._rename_interfaces') - def test_apply_v2_renames(self, m_rename_interfaces, m_device_driver, - m_device_devid): - m_device_driver.return_value = 'virtio_net' - m_device_devid.return_value = '0x15d8' - - net.apply_network_config_names(yaml.load(self.V2_CONFIG)) - - call = ['52:54:00:12:34:00', 'interface0', 'virtio_net', '0x15d8'] - m_rename_interfaces.assert_called_with([call]) - - @mock.patch('cloudinit.net._rename_interfaces') - def test_apply_v2_renames_skips_without_setname(self, m_rename_interfaces): - net.apply_network_config_names(yaml.load(self.V2_CONFIG_NO_SETNAME)) - m_rename_interfaces.assert_called_with([]) - - @mock.patch('cloudinit.net._rename_interfaces') - def test_apply_v2_renames_skips_without_mac(self, m_rename_interfaces): - net.apply_network_config_names(yaml.load(self.V2_CONFIG_NO_MAC)) - m_rename_interfaces.assert_called_with([]) - - def test_apply_v2_renames_raises_runtime_error_on_unknown_version(self): - with self.assertRaises(RuntimeError): - net.apply_network_config_names(yaml.load("version: 3")) - - -class TestHasURLConnectivity(HttprettyTestCase): - - def setUp(self): - super(TestHasURLConnectivity, self).setUp() - self.url = 'http://fake/' - self.kwargs = {'allow_redirects': True, 'timeout': 5.0} - - @mock.patch('cloudinit.net.readurl') - def test_url_timeout_on_connectivity_check(self, m_readurl): - """A timeout of 5 seconds is provided when reading a url.""" - self.assertTrue( - net.has_url_connectivity(self.url), 'Expected True on url connect') - - def test_true_on_url_connectivity_success(self): - httpretty.register_uri(httpretty.GET, self.url) - self.assertTrue( - net.has_url_connectivity(self.url), 'Expected True on url connect') - - @mock.patch('requests.Session.request') - def test_true_on_url_connectivity_timeout(self, m_request): - """A timeout raised accessing the url will return False.""" - m_request.side_effect = requests.Timeout('Fake Connection Timeout') - self.assertFalse( - net.has_url_connectivity(self.url), - 'Expected False on url timeout') - - def test_true_on_url_connectivity_failure(self): - 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 TestNetFailOver(CiTestCase): - - 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)) - - -class TestIsIpAddress: - """Tests for net.is_ip_address. - - Instead of testing with values we rely on the ipaddress stdlib module to - handle all values correctly, so simply test that is_ip_address defers to - the ipaddress module correctly. - """ - - @pytest.mark.parametrize('ip_address_side_effect,expected_return', ( - (ValueError, False), - (lambda _: ipaddress.IPv4Address('192.168.0.1'), True), - (lambda _: ipaddress.IPv6Address('2001:db8::'), True), - )) - def test_is_ip_address(self, ip_address_side_effect, expected_return): - with mock.patch('cloudinit.net.ipaddress.ip_address', - side_effect=ip_address_side_effect) as m_ip_address: - ret = net.is_ip_address(mock.sentinel.ip_address_in) - assert expected_return == ret - expected_call = mock.call(mock.sentinel.ip_address_in) - assert [expected_call] == m_ip_address.call_args_list - - -class TestIsIpv4Address: - """Tests for net.is_ipv4_address. - - Instead of testing with values we rely on the ipaddress stdlib module to - handle all values correctly, so simply test that is_ipv4_address defers to - the ipaddress module correctly. - """ - - @pytest.mark.parametrize('ipv4address_mock,expected_return', ( - (mock.Mock(side_effect=ValueError), False), - (mock.Mock(return_value=ipaddress.IPv4Address('192.168.0.1')), True), - )) - def test_is_ip_address(self, ipv4address_mock, expected_return): - with mock.patch('cloudinit.net.ipaddress.IPv4Address', - ipv4address_mock) as m_ipv4address: - ret = net.is_ipv4_address(mock.sentinel.ip_address_in) - assert expected_return == ret - expected_call = mock.call(mock.sentinel.ip_address_in) - assert [expected_call] == m_ipv4address.call_args_list - - -# vi: ts=4 expandtab diff --git a/cloudinit/net/tests/test_network_state.py b/cloudinit/net/tests/test_network_state.py deleted file mode 100644 index 07d726e2..00000000 --- a/cloudinit/net/tests/test_network_state.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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) - - -class TestNetworkStateParseConfigV2(CiTestCase): - - def test_version_2_ignores_renderer_key(self): - ncfg = {'version': 2, 'renderer': 'networkd', 'ethernets': {}} - nsi = network_state.NetworkStateInterpreter(version=ncfg['version'], - config=ncfg) - nsi.parse_config(skip_broken=False) - self.assertEqual(ncfg, nsi.as_dict()['config']) - - -# vi: ts=4 expandtab diff --git a/cloudinit/net/udev.py b/cloudinit/net/udev.py index 58c0a708..b79e4426 100644 --- a/cloudinit/net/udev.py +++ b/cloudinit/net/udev.py @@ -32,15 +32,18 @@ def generate_udev_rule(interface, mac, driver=None): ATTR{address}="ff:ee:dd:cc:bb:aa", NAME="eth0" """ if not driver: - driver = '?*' - - rule = ', '.join([ - compose_udev_equality('SUBSYSTEM', 'net'), - compose_udev_equality('ACTION', 'add'), - compose_udev_equality('DRIVERS', driver), - compose_udev_attr_equality('address', mac), - compose_udev_setting('NAME', interface), - ]) - return '%s\n' % rule + driver = "?*" + + rule = ", ".join( + [ + compose_udev_equality("SUBSYSTEM", "net"), + compose_udev_equality("ACTION", "add"), + compose_udev_equality("DRIVERS", driver), + compose_udev_attr_equality("address", mac), + compose_udev_setting("NAME", interface), + ] + ) + return "%s\n" % rule + # vi: ts=4 expandtab syntax=python |