diff options
Diffstat (limited to 'cloudinit/net/netplan.py')
-rw-r--r-- | cloudinit/net/netplan.py | 317 |
1 files changed, 176 insertions, 141 deletions
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 |