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