summaryrefslogtreecommitdiff
path: root/cloudinit/net/networkd.py
blob: 3bbeb2841c82fb2480a161a041cd4aa6de844682 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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)