Merge ~slyon/netplan/+git/ubuntu:slyon/0100-release into ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu/master

Proposed by Lukas Märdian
Status: Merged
Approved by: Łukasz Zemczak
Approved revision: 7236ab1317fe4ed54d125fa4fe7130ab2af044ed
Merged at revision: 7236ab1317fe4ed54d125fa4fe7130ab2af044ed
Proposed branch: ~slyon/netplan/+git/ubuntu:slyon/0100-release
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu/master
Diff against target: 8854 lines (+5784/-592)
51 files modified
Makefile (+3/-2)
debian/changelog (+31/-0)
debian/control (+1/-0)
debian/libnetplan0.symbols (+7/-0)
debian/patches/series (+0/-5)
debian/tests/control (+15/-0)
dev/null (+0/-79)
doc/netplan.md (+298/-74)
examples/sriov.yaml (+14/-0)
examples/sriov_vlan.yaml (+18/-0)
examples/static.yaml (+2/-2)
netplan/cli/commands/apply.py (+91/-17)
netplan/cli/commands/try_command.py (+2/-2)
netplan/cli/ovs.py (+168/-0)
netplan/cli/sriov.py (+32/-18)
netplan/cli/utils.py (+32/-0)
netplan/configmanager.py (+32/-0)
src/generate.c (+14/-2)
src/networkd.c (+114/-32)
src/networkd.h (+2/-0)
src/nm.c (+109/-26)
src/openvswitch.c (+484/-0)
src/openvswitch.h (+24/-0)
src/parse.c (+763/-241)
src/parse.h (+56/-4)
src/sriov.c (+40/-0)
src/sriov.h (+21/-0)
src/util.c (+29/-0)
src/util.h (+6/-2)
src/validation.c (+155/-10)
src/validation.h (+3/-0)
tests/generator/base.py (+54/-0)
tests/generator/test_args.py (+18/-4)
tests/generator/test_bonds.py (+42/-0)
tests/generator/test_common.py (+194/-1)
tests/generator/test_errors.py (+69/-1)
tests/generator/test_ethernets.py (+23/-7)
tests/generator/test_ovs.py (+1021/-0)
tests/generator/test_routing.py (+145/-17)
tests/generator/test_tunnels.py (+477/-5)
tests/generator/test_wifis.py (+143/-0)
tests/integration/base.py (+6/-4)
tests/integration/ethernets.py (+38/-0)
tests/integration/ovs.py (+559/-0)
tests/integration/routing.py (+76/-21)
tests/integration/tunnels.py (+71/-0)
tests/test_configmanager.py (+21/-0)
tests/test_ovs.py (+129/-0)
tests/test_sriov.py (+55/-12)
tests/test_utils.py (+73/-0)
tests/validate_docs.sh (+4/-4)
Reviewer Review Type Date Requested Status
Łukasz Zemczak Approve
Review via email: mp+390288@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Łukasz Zemczak (sil2100) wrote :

Yes, glimpsing through the changes everything looks correct! I also think it has been done in a similar way to how we did it in the past, so +1 on proceeding.

I'll merge and sponsor it when we get an official +1 on the FFe.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index 3ce1347..4b80a18 100644
3--- a/Makefile
4+++ b/Makefile
5@@ -43,7 +43,7 @@ libnetplan.so.$(NETPLAN_SOVER): parse.o util.o validation.o error.o
6 ln -snf libnetplan.so.$(NETPLAN_SOVER) libnetplan.so
7
8 #generate: src/generate.[hc] src/parse.[hc] src/util.[hc] src/networkd.[hc] src/nm.[hc] src/validation.[hc] src/error.[hc]
9-generate: libnetplan.so.$(NETPLAN_SOVER) nm.o networkd.o generate.o
10+generate: libnetplan.so.$(NETPLAN_SOVER) nm.o networkd.o openvswitch.o generate.o sriov.o
11 $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ -L. -lnetplan `pkg-config --cflags --libs glib-2.0 gio-2.0 yaml-0.1 uuid`
12
13 netplan-dbus: src/dbus.c src/_features.h
14@@ -66,7 +66,8 @@ clean:
15 rm -f *.o *.so*
16 rm -f netplan-dbus dbus/*.service
17 rm -f *.gcda *.gcno generate.info
18- rm -rf test-coverage .coverage
19+ rm -rf test-coverage .coverage coverage.xml
20+ find . | grep -E "(__pycache__|\.pyc)" | xargs rm -rf
21
22 check: default linting
23 tests/cli.py
24diff --git a/debian/changelog b/debian/changelog
25index c34e72b..524dea4 100644
26--- a/debian/changelog
27+++ b/debian/changelog
28@@ -1,3 +1,34 @@
29+netplan.io (0.100-0ubuntu1) UNRELEASED; urgency=medium
30+
31+ * New upstream release: 0.100
32+ - Documentation improvements
33+ - Improved integration tests
34+ - Overall cleanup and refactoring
35+ - Improved SR-IOV first boot experience
36+ - Initial Open vSwitch support (LP: #1728134)
37+ - Add support for Wireguard tunnels
38+ - Add support for IP address labels (LP: #1743200)
39+ - Improved routing & globbing in NetworkManager backend
40+ - route attributes "from", "onlink" and "table"
41+ - matching interfaces by glob pattern
42+ - Add support for hidden wireless SSIDs (LP: #1866100)
43+ - Introduce support for networkd address options (LP: #1803203)
44+ - Implement ipv6-address-token key (LP: #1737976)
45+ Bug fixes:
46+ - Not connect to WiFi after 'netplan apply' (LP: #1874377)
47+ - Call daemon-reload after we touched systemd unit files (LP: #1874494)
48+ - Don't fail if same primary slave was set before (LP: #1817651)
49+ - Fix process_link_changes handling 'up' interfaces (LP: #1875411)
50+ - Fix GCC-10 -fno-common linker errors (LP: #1875412)
51+ - Flush IPs of NM managed interfaces (LP: #1870561)
52+ * Enable autopkgtests for OVS and wireguard
53+ * Drop all distro patches, which have been integrated upstream
54+ * Update symbols file
55+ * Added openvswitch-switch to Build-Depends
56+ - ovs-vsctl is required to pass the unit-tests
57+
58+ -- Lukas Märdian <lukas.maerdian@canonical.com> Thu, 03 Sep 2020 15:51:29 +0200
59+
60 netplan.io (0.99-0ubuntu6) groovy; urgency=medium
61
62 * d/p/0005-Fix-GCC-10-fno-common-linker-errors-LP-1875412-155.patch
63diff --git a/debian/control b/debian/control
64index d6a69b1..029f74d 100644
65--- a/debian/control
66+++ b/debian/control
67@@ -24,6 +24,7 @@ Build-Depends: debhelper (>= 11),
68 pycodestyle | pep8,
69 python3-nose,
70 pandoc,
71+ openvswitch-switch,
72 Vcs-Git: https://salsa.debian.org/debian/netplan.io.git
73 Vcs-Browser: https://salsa.debian.org/debian/netplan.io
74 Homepage: https://netplan.io/
75diff --git a/debian/libnetplan0.symbols b/debian/libnetplan0.symbols
76index 62adadd..36b49bb 100644
77--- a/debian/libnetplan0.symbols
78+++ b/debian/libnetplan0.symbols
79@@ -1,10 +1,13 @@
80 libnetplan.so.0.0 libnetplan0 #MINVER#
81 NETPLAN_OPTIONAL_ADDRESS_TYPES@Base 0.99
82 NETPLAN_WIFI_WOWLAN_TYPES@Base 0.99
83+ address_option_handlers@Base 0.100
84 current_file@Base 0.99
85 g_string_free_to_file@Base 0.99
86+ is_hostname@Base 0.100
87 is_ip4_address@Base 0.99
88 is_ip6_address@Base 0.99
89+ is_wireguard_key@Base 0.100
90 missing_id@Base 0.99
91 missing_ids_found@Base 0.99
92 netdefs@Base 0.99
93@@ -12,14 +15,18 @@ libnetplan.so.0.0 libnetplan0 #MINVER#
94 netplan_finish_parse@Base 0.99
95 netplan_get_global_backend@Base 0.99
96 netplan_parse_yaml@Base 0.99
97+ ovs_settings_global@Base 0.100
98 parser_error@Base 0.99
99 safe_mkdir_p_dir@Base 0.99
100+ systemd_escape@Base 0.100
101 tunnel_mode_to_string@Base 0.99
102 unlink_glob@Base 0.99
103 validate_backend_rules@Base 0.99
104 validate_netdef_grammar@Base 0.99
105+ validate_ovs_target@Base 0.100
106 wifi_frequency_24@Base 0.99
107 wifi_frequency_5@Base 0.99
108 wifi_get_freq24@Base 0.99
109 wifi_get_freq5@Base 0.99
110+ wireguard_peer_handlers@Base 0.100
111 yaml_error@Base 0.99
112diff --git a/debian/patches/0001-Fix-LP-1874377-Not-connect-to-WiFi-after-netplan-app.patch b/debian/patches/0001-Fix-LP-1874377-Not-connect-to-WiFi-after-netplan-app.patch
113deleted file mode 100644
114index d401e97..0000000
115--- a/debian/patches/0001-Fix-LP-1874377-Not-connect-to-WiFi-after-netplan-app.patch
116+++ /dev/null
117@@ -1,175 +0,0 @@
118-From: Lukas Maerdian <lukas.maerdian@canonical.com>
119-Date: Tue, 28 Apr 2020 14:35:36 +0200
120-Subject: Fix LP#1874377: Not connect to WiFi after 'netplan apply' (#133)
121-
122-* Fix LP#1874377: Not connect to WiFi after 'netplan apply'
123-
124-Seems like the 'netplan apply' command was not properly adopted in #109 when wired wpa_supplicant support was introduced.
125-
126-Fixes: https://bugs.launchpad.net/ubuntu/+source/netplan.io/+bug/1874377
127----
128- netplan/cli/commands/apply.py | 10 ++++--
129- netplan/cli/utils.py | 7 +++++
130- src/networkd.c | 4 +++
131- tests/generator/test_wifis.py | 71 +++++++++++++++++++++++++++++++++++++++++++
132- tests/integration/base.py | 2 +-
133- 5 files changed, 91 insertions(+), 3 deletions(-)
134-
135-diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py
136-index 0ec95f5..cf9f122 100644
137---- a/netplan/cli/commands/apply.py
138-+++ b/netplan/cli/commands/apply.py
139-@@ -108,7 +108,13 @@ class NetplanApply(utils.NetplanCommand):
140- # stop backends
141- if restart_networkd:
142- logging.debug('netplan generated networkd configuration changed, restarting networkd')
143-- utils.systemctl_networkd('stop', sync=sync, extra_services=['netplan-wpa@*.service'])
144-+ wpa_services = ['netplan-wpa-*.service']
145-+ # Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an
146-+ # upgraded system, we need to make sure to stop those.
147-+ if utils.systemctl_is_active('netplan-wpa@*.service'):
148-+ wpa_services.insert(0, 'netplan-wpa@*.service')
149-+ utils.systemctl_networkd('stop', sync=sync, extra_services=wpa_services)
150-+
151- else:
152- logging.debug('no netplan generated networkd configuration exists')
153-
154-@@ -169,7 +175,7 @@ class NetplanApply(utils.NetplanCommand):
155-
156- # (re)start backends
157- if restart_networkd:
158-- netplan_wpa = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-wpa@*.service')]
159-+ netplan_wpa = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-wpa-*.service')]
160- utils.systemctl_networkd('start', sync=sync, extra_services=netplan_wpa)
161- if restart_nm:
162- utils.systemctl_network_manager('start', sync=sync)
163-diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py
164-index 5f54b1a..c0eee03 100644
165---- a/netplan/cli/utils.py
166-+++ b/netplan/cli/utils.py
167-@@ -86,6 +86,13 @@ def systemctl_networkd(action, sync=False, extra_services=[]): # pragma: nocove
168- subprocess.check_call(command)
169-
170-
171-+def systemctl_is_active(unit_pattern): # pragma: nocover (covered in autopkgtest)
172-+ '''Return True if at least one matching unit is running'''
173-+ if subprocess.call(['systemctl', '--quiet', 'is-active', unit_pattern]) == 0:
174-+ return True
175-+ return False
176-+
177-+
178- def get_interface_driver_name(interface, only_down=False): # pragma: nocover (covered in autopkgtest)
179- devdir = os.path.join('/sys/class/net', interface)
180- if only_down:
181-diff --git a/src/networkd.c b/src/networkd.c
182-index e2bb111..6f6173a 100644
183---- a/src/networkd.c
184-+++ b/src/networkd.c
185-@@ -990,8 +990,12 @@ cleanup_networkd_conf(const char* rootdir)
186- {
187- unlink_glob(rootdir, "/run/systemd/network/10-netplan-*");
188- unlink_glob(rootdir, "/run/netplan/wpa-*.conf");
189-+ unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-*.service");
190- unlink_glob(rootdir, "/run/systemd/system/netplan-wpa-*.service");
191- unlink_glob(rootdir, "/run/udev/rules.d/99-netplan-*");
192-+ /* Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an
193-+ * upgraded system, we need to make sure to clean those up. */
194-+ unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa@*.service");
195- }
196-
197- /**
198-diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
199-index 8eb804e..d5b79cf 100644
200---- a/tests/generator/test_wifis.py
201-+++ b/tests/generator/test_wifis.py
202-@@ -116,6 +116,77 @@ network={
203- self.assertTrue(os.path.islink(os.path.join(
204- self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
205-
206-+ def test_wifi_upgrade(self):
207-+ # pretend an old 'netplan-wpa@*.service' link still exists on an upgraded system
208-+ os.makedirs(os.path.join(self.workdir.name, 'lib/systemd/system'))
209-+ os.makedirs(os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants'))
210-+ with open(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), 'w') as out:
211-+ out.write('''[Unit]
212-+Description=WPA supplicant for netplan %I
213-+DefaultDependencies=no
214-+Requires=sys-subsystem-net-devices-%i.device
215-+After=sys-subsystem-net-devices-%i.device
216-+Before=network.target
217-+Wants=network.target
218-+
219-+[Service]
220-+Type=simple
221-+ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%I.conf -i%I''')
222-+ os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'),
223-+ os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service'))
224-+
225-+ # run generate, which should cleanup the old files/symlinks
226-+ self.generate('''network:
227-+ version: 2
228-+ wifis:
229-+ wl0:
230-+ access-points:
231-+ "Joe's Home":
232-+ password: "s0s3kr1t"
233-+ dhcp4: yes''')
234-+
235-+ # verify new files/links exist, while old have been removed
236-+ self.assertTrue(os.path.isfile(os.path.join(
237-+ self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')))
238-+ self.assertTrue(os.path.islink(os.path.join(
239-+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
240-+ # old files/links
241-+ self.assertTrue(os.path.isfile(os.path.join(
242-+ self.workdir.name, 'lib/systemd/system/netplan-wpa@.service')))
243-+ self.assertFalse(os.path.islink(os.path.join(
244-+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service')))
245-+
246-+ # pretend another old systemd service file exists for wl1
247-+ os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'),
248-+ os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service'))
249-+
250-+ # run generate again, to verify the historical netplan-wpa@.service links and wl0 links are gone
251-+ self.generate('''network:
252-+ version: 2
253-+ wifis:
254-+ wl1:
255-+ access-points:
256-+ "Other Home":
257-+ password: "s0s3kr1t"
258-+ dhcp4: yes''')
259-+
260-+ # verify new files/links exist, while old have been removed
261-+ self.assertTrue(os.path.isfile(os.path.join(
262-+ self.workdir.name, 'run/systemd/system/netplan-wpa-wl1.service')))
263-+ self.assertTrue(os.path.islink(os.path.join(
264-+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl1.service')))
265-+ # old files/links
266-+ self.assertTrue(os.path.isfile(os.path.join(
267-+ self.workdir.name, 'lib/systemd/system/netplan-wpa@.service')))
268-+ self.assertFalse(os.path.islink(os.path.join(
269-+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service')))
270-+ self.assertFalse(os.path.islink(os.path.join(
271-+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service')))
272-+ self.assertFalse(os.path.isfile(os.path.join(
273-+ self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')))
274-+ self.assertFalse(os.path.islink(os.path.join(
275-+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
276-+
277- def test_wifi_route(self):
278- self.generate('''network:
279- version: 2
280-diff --git a/tests/integration/base.py b/tests/integration/base.py
281-index ed7100e..16fd2ee 100644
282---- a/tests/integration/base.py
283-+++ b/tests/integration/base.py
284-@@ -93,7 +93,7 @@ class IntegrationTestsBase(unittest.TestCase):
285- pass
286-
287- def tearDown(self):
288-- subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa@*',
289-+ subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa-*',
290- 'systemd-networkd.socket'])
291- # NM has KillMode=process and leaks dhclient processes
292- subprocess.call(['systemctl', 'kill', 'NetworkManager'])
293diff --git a/debian/patches/0002-Fix-process_link_changes-handling-up-interfaces.patch b/debian/patches/0002-Fix-process_link_changes-handling-up-interfaces.patch
294deleted file mode 100644
295index f9a2e56..0000000
296--- a/debian/patches/0002-Fix-process_link_changes-handling-up-interfaces.patch
297+++ /dev/null
298@@ -1,33 +0,0 @@
299-From: Heitor Alves de Siqueira <halves@canonical.com>
300-Date: Thu, 28 May 2020 09:19:07 -0300
301-Subject: Fix process_link_changes handling 'up' interfaces
302-
303-b7f1d9b04212 refactored process_link_changes with helper methods to get
304-the interface driver name and MAC address. This new code introduced a
305-regression where it's possible for an interface in the up state to be
306-included in the changelist by its MAC address.
307-
308-This patch restores the previous behaviour, skipping interfaces that
309-don't have driver_name set.
310-
311-Fixes: https://bugs.launchpad.net/bugs/1875411
312-Origin: upstream, https://github.com/CanonicalLtd/netplan/commit/8f77deec17ce
313-Bug-Ubuntu: https://bugs.launchpad.net/bugs/1875411
314----
315- netplan/cli/commands/apply.py | 3 +++
316- 1 file changed, 3 insertions(+)
317-
318-diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py
319-index cf9f122..4b91ae9 100644
320---- a/netplan/cli/commands/apply.py
321-+++ b/netplan/cli/commands/apply.py
322-@@ -244,6 +244,9 @@ class NetplanApply(utils.NetplanCommand):
323- continue
324-
325- driver_name = utils.get_interface_driver_name(interface, only_down=True)
326-+ if not driver_name:
327-+ # don't allow up interfaces to match by mac
328-+ continue
329- macaddress = utils.get_interface_macaddress(interface)
330- if driver_name in matches['by-driver']:
331- new_name = matches['by-driver'][driver_name]
332diff --git a/debian/patches/0003-Call-daemon-reload-after-we-touched-systemd-unit-fil.patch b/debian/patches/0003-Call-daemon-reload-after-we-touched-systemd-unit-fil.patch
333deleted file mode 100644
334index 470dfbd..0000000
335--- a/debian/patches/0003-Call-daemon-reload-after-we-touched-systemd-unit-fil.patch
336+++ /dev/null
337@@ -1,64 +0,0 @@
338-From: Lukas Maerdian <lukas.maerdian@canonical.com>
339-Date: Thu, 14 May 2020 10:12:14 +0200
340-Subject: [PATCH 1/8] Call daemon-reload after we touched systemd unit files
341- (LP: #1874494) (#135)
342-
343-When running netplan apply systemd unit files might be created/deleted/changed on disk. We need to run systemctl daemon-reload, to make systemd aware of those new units and calculate the new unit/service dependencies. Otherwise it will throw warnings at us:
344-
345-ubuntu@ubuntu:~$ sudo netplan apply
346-Warning: The unit file, source configuration file or drop-ins of netplan-wpa-wlan0.service changed on disk. Run 'systemctl daemon-reload' to reload units.
347-Warning: The unit file, source configuration file or drop-ins of netplan-wpa-wlan0.service changed on disk. Run 'systemctl daemon-reload' to reload units.
348-
349-Fixes: https://bugs.launchpad.net/ubuntu/+source/netplan.io/+bug/1874494
350----
351- netplan/cli/commands/apply.py | 4 ++++
352- netplan/cli/utils.py | 5 +++++
353- tests/integration/base.py | 4 +++-
354- 3 files changed, 12 insertions(+), 1 deletion(-)
355-
356-diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py
357-index 4b91ae9..33ff5b5 100644
358---- a/netplan/cli/commands/apply.py
359-+++ b/netplan/cli/commands/apply.py
360-@@ -108,6 +108,10 @@ class NetplanApply(utils.NetplanCommand):
361- # stop backends
362- if restart_networkd:
363- logging.debug('netplan generated networkd configuration changed, restarting networkd')
364-+ # Running 'systemctl daemon-reload' will re-run the netplan systemd generator,
365-+ # so let's make sure we only run it iff we're willing to run 'netplan generate'
366-+ if run_generate:
367-+ utils.systemctl_daemon_reload()
368- wpa_services = ['netplan-wpa-*.service']
369- # Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an
370- # upgraded system, we need to make sure to stop those.
371-diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py
372-index c0eee03..85342fc 100644
373---- a/netplan/cli/utils.py
374-+++ b/netplan/cli/utils.py
375-@@ -93,6 +93,11 @@ def systemctl_is_active(unit_pattern): # pragma: nocover (covered in autopkgtes
376- return False
377-
378-
379-+def systemctl_daemon_reload(): # pragma: nocover (covered in autopkgtest)
380-+ '''Reload systemd unit files from disk and re-calculate its dependencies'''
381-+ subprocess.check_call(['systemctl', 'daemon-reload'])
382-+
383-+
384- def get_interface_driver_name(interface, only_down=False): # pragma: nocover (covered in autopkgtest)
385- devdir = os.path.join('/sys/class/net', interface)
386- if only_down:
387-diff --git a/tests/integration/base.py b/tests/integration/base.py
388-index 16fd2ee..7d36198 100644
389---- a/tests/integration/base.py
390-+++ b/tests/integration/base.py
391-@@ -352,7 +352,9 @@ class IntegrationTestsBase(unittest.TestCase):
392- '''Generate config, launch and settle NM and networkd'''
393-
394- # regenerate netplan config
395-- subprocess.check_call(['netplan', 'apply'])
396-+ out = subprocess.check_output(['netplan', 'apply'], universal_newlines=True)
397-+ if 'Run \'systemctl daemon-reload\' to reload units.' in out:
398-+ self.fail('systemd units changed without reload')
399- # start NM so that we can verify that it does not manage anything
400- subprocess.check_call(['systemctl', 'start', '--no-block', 'NetworkManager.service'])
401- # wait until networkd is done
402diff --git a/debian/patches/0004-Fix-autopkgtest-on-arm64-with-NM-1.24-146.patch b/debian/patches/0004-Fix-autopkgtest-on-arm64-with-NM-1.24-146.patch
403deleted file mode 100644
404index 3de2c71..0000000
405--- a/debian/patches/0004-Fix-autopkgtest-on-arm64-with-NM-1.24-146.patch
406+++ /dev/null
407@@ -1,160 +0,0 @@
408-From: Lukas Maerdian <lukas.maerdian@canonical.com>
409-Date: Thu, 18 Jun 2020 11:28:28 +0200
410-Subject: [PATCH] Fix autopkgtest on arm64 with NM 1.24 (#146)
411-
412-On arm64 we had a test failure in tests/integration/ethernets.py -> test_manual_addresses, which was caused by the fact that netplan apply was disconnecting ALL available network interfaces via nmcli, in order to reload the new netplan config. This way the IP of the 2nd test DHCP server (dnsmasq on veth43) was lost and did not provide correct responses for the rest of the test.
413-
414-It is not necessary to take all interfaces down, only the ones which are (or were) configured via netplan. This was fixed in the netplan apply command.
415-
416-Note: This should make its way into the Groovy package as a distro-patch, to avoid future autopkgtest failures.
417-
418-Commits:
419-* tests: fix ethernets/test_manual_addresses on arm64
420-* netplan:apply: disconnect/re-configure only netplan-NM interfaces
421-* tests:ethernets: remove deprecated fix, was fixed via 'netplan apply'
422-* tests: add unit-test for python utils
423-* utils: get rid of default glob in nm_interfaces
424----
425- netplan/cli/commands/apply.py | 11 ++++++++--
426- netplan/cli/utils.py | 14 ++++++++++++
427- tests/integration/ethernets.py | 2 ++
428- tests/test_utils.py | 49 ++++++++++++++++++++++++++++++++++++++++++
429- 4 files changed, 74 insertions(+), 2 deletions(-)
430- create mode 100644 tests/test_utils.py
431-
432-diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py
433-index 33ff5b5..a38fe88 100644
434---- a/netplan/cli/commands/apply.py
435-+++ b/netplan/cli/commands/apply.py
436-@@ -74,7 +74,9 @@ class NetplanApply(utils.NetplanCommand):
437- return
438-
439- old_files_networkd = bool(glob.glob('/run/systemd/network/*netplan-*'))
440-- old_files_nm = bool(glob.glob('/run/NetworkManager/system-connections/netplan-*'))
441-+ old_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*')
442-+ nm_ifaces = utils.nm_interfaces(old_nm_glob)
443-+ old_files_nm = bool(old_nm_glob)
444-
445- generator_call = []
446- generate_out = None
447-@@ -101,7 +103,10 @@ class NetplanApply(utils.NetplanCommand):
448- restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*'))
449- if not restart_networkd and old_files_networkd:
450- restart_networkd = True
451-- restart_nm = bool(glob.glob('/run/NetworkManager/system-connections/netplan-*'))
452-+
453-+ restart_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*')
454-+ nm_ifaces += utils.nm_interfaces(restart_nm_glob)
455-+ restart_nm = bool(restart_nm_glob)
456- if not restart_nm and old_files_nm:
457- restart_nm = True
458-
459-@@ -127,6 +132,8 @@ class NetplanApply(utils.NetplanCommand):
460- if utils.nm_running():
461- # restarting NM does not cause new config to be applied, need to shut down devices first
462- for device in devices:
463-+ if device not in nm_ifaces:
464-+ continue # do not touch this interface
465- # ignore failures here -- some/many devices might not be managed by NM
466- try:
467- utils.nmcli(['device', 'disconnect', device])
468-diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py
469-index 85342fc..4f7e266 100644
470---- a/netplan/cli/utils.py
471-+++ b/netplan/cli/utils.py
472-@@ -23,6 +23,7 @@ import fnmatch
473- import argparse
474- import subprocess
475- import netifaces
476-+import re
477-
478- NM_SERVICE_NAME = 'NetworkManager.service'
479- NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service'
480-@@ -55,6 +56,19 @@ def nm_running(): # pragma: nocover (covered in autopkgtest)
481- return False
482-
483-
484-+def nm_interfaces(paths):
485-+ pat = re.compile('^interface-name=(.*)$')
486-+ interfaces = []
487-+ for path in paths:
488-+ with open(path, 'r') as f:
489-+ for line in f:
490-+ m = pat.match(line)
491-+ if m:
492-+ interfaces.append(m.group(1))
493-+ break # skip to next file
494-+ return interfaces
495-+
496-+
497- def systemctl_network_manager(action, sync=False): # pragma: nocover (covered in autopkgtest)
498- service_name = NM_SERVICE_NAME
499-
500-diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py
501-index 231d337..dc5783b 100644
502---- a/tests/integration/ethernets.py
503-+++ b/tests/integration/ethernets.py
504-@@ -156,6 +156,8 @@ class _CommonTests():
505- ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client})
506- self.start_dnsmasq(None, self.dev_e2_ap)
507- self.generate_and_settle()
508-+ if self.backend == 'NetworkManager':
509-+ self.nm_online_full(self.dev_e2_client)
510- self.assert_iface_up(self.dev_e_client,
511- ['inet 172.16.5.3/20'],
512- ['inet 192.168.5', # old DHCP
513-diff --git a/tests/test_utils.py b/tests/test_utils.py
514-new file mode 100644
515-index 0000000..2c137d4
516---- /dev/null
517-+++ b/tests/test_utils.py
518-@@ -0,0 +1,49 @@
519-+#!/usr/bin/python3
520-+#
521-+# Copyright (C) 2020 Canonical, Ltd.
522-+# Author: Lukas Märdian <lukas.maerdian@canonical.com>
523-+#
524-+# This program is free software; you can redistribute it and/or modify
525-+# it under the terms of the GNU General Public License as published by
526-+# the Free Software Foundation; version 3.
527-+#
528-+# This program is distributed in the hope that it will be useful,
529-+# but WITHOUT ANY WARRANTY; without even the implied warranty of
530-+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
531-+# GNU General Public License for more details.
532-+#
533-+# You should have received a copy of the GNU General Public License
534-+# along with this program. If not, see <http://www.gnu.org/licenses/>.
535-+
536-+import os
537-+import unittest
538-+import tempfile
539-+import glob
540-+
541-+import netplan.cli.utils as utils
542-+
543-+
544-+class TestUtils(unittest.TestCase):
545-+
546-+ def setUp(self):
547-+ self.workdir = tempfile.TemporaryDirectory()
548-+ os.makedirs(os.path.join(self.workdir.name, 'etc/netplan'))
549-+ os.makedirs(os.path.join(self.workdir.name,
550-+ 'run/NetworkManager/system-connections'))
551-+
552-+ def _create_nm_keyfile(self, filename, ifname):
553-+ with open(os.path.join(self.workdir.name,
554-+ 'run/NetworkManager/system-connections/', filename), 'w') as f:
555-+ f.write('[connection]\n')
556-+ f.write('key=value\n')
557-+ f.write('interface-name=%s\n' % ifname)
558-+ f.write('key2=value2\n')
559-+
560-+ def test_nm_interfaces(self):
561-+ self._create_nm_keyfile('netplan-test.nmconnection', 'eth0')
562-+ self._create_nm_keyfile('netplan-test2.nmconnection', 'eth1')
563-+ ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name,
564-+ 'run/NetworkManager/system-connections/*.nmconnection')))
565-+ self.assertTrue('eth0' in ifaces)
566-+ self.assertTrue('eth1' in ifaces)
567-+ self.assertTrue(len(ifaces) == 2)
568diff --git a/debian/patches/0005-Fix-GCC-10-fno-common-linker-errors-LP-1875412-155.patch b/debian/patches/0005-Fix-GCC-10-fno-common-linker-errors-LP-1875412-155.patch
569deleted file mode 100644
570index afad0fc..0000000
571--- a/debian/patches/0005-Fix-GCC-10-fno-common-linker-errors-LP-1875412-155.patch
572+++ /dev/null
573@@ -1,79 +0,0 @@
574-From: Lukas Maerdian <lukas.maerdian@canonical.com>
575-Date: Tue, 28 Jul 2020 09:54:19 +0200
576-Subject: Fix GCC-10 -fno-common linker errors (LP: #1875412) (#155)
577-
578-https://gcc.gnu.org/gcc-10/porting_to.html
579----
580- src/parse.c | 5 +++++
581- src/parse.h | 6 +++---
582- src/util.c | 3 +++
583- src/util.h | 4 ++--
584- 4 files changed, 13 insertions(+), 5 deletions(-)
585-
586-diff --git a/src/parse.c b/src/parse.c
587-index c1dc175..e705ef3 100644
588---- a/src/parse.c
589-+++ b/src/parse.c
590-@@ -62,6 +62,11 @@ GList* netdefs_ordered;
591- * existing definition */
592- static GHashTable* ids_in_file;
593-
594-+/* Global variables, defined in this file */
595-+int missing_ids_found;
596-+const char* current_file;
597-+GHashTable* missing_id;
598-+
599- /**
600- * Load YAML file name into a yaml_document_t.
601- *
602-diff --git a/src/parse.h b/src/parse.h
603-index b84ff4c..b5b767f 100644
604---- a/src/parse.h
605-+++ b/src/parse.h
606-@@ -25,14 +25,14 @@
607-
608-
609- /* file that is currently being processed, for useful error messages */
610--const char* current_file;
611-+extern const char* current_file;
612-
613- /* List of "seen" ids not found in netdefs yet by the parser.
614- * These are removed when it exists in this list and we reach the point of
615- * creating a netdef for that id; so by the time we're done parsing the yaml
616- * document it should be empty. */
617--GHashTable *missing_id;
618--int missing_ids_found;
619-+extern GHashTable *missing_id;
620-+extern int missing_ids_found;
621-
622- /****************************************************
623- * Parsed definitions
624-diff --git a/src/util.c b/src/util.c
625-index 4b71ef1..ee00e93 100644
626---- a/src/util.c
627-+++ b/src/util.c
628-@@ -24,6 +24,9 @@
629-
630- #include "util.h"
631-
632-+GHashTable* wifi_frequency_24;
633-+GHashTable* wifi_frequency_5;
634-+
635- /**
636- * Create the parent directories of given file path. Exit program on failure.
637- */
638-diff --git a/src/util.h b/src/util.h
639-index 4ed1a60..4cd6ca2 100644
640---- a/src/util.h
641-+++ b/src/util.h
642-@@ -17,8 +17,8 @@
643-
644- #pragma once
645-
646--GHashTable* wifi_frequency_24;
647--GHashTable* wifi_frequency_5;
648-+extern GHashTable* wifi_frequency_24;
649-+extern GHashTable* wifi_frequency_5;
650-
651- void safe_mkdir_p_dir(const char* file_path);
652- void g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix);
653diff --git a/debian/patches/series b/debian/patches/series
654index 5638063..e69de29 100644
655--- a/debian/patches/series
656+++ b/debian/patches/series
657@@ -1,5 +0,0 @@
658-0001-Fix-LP-1874377-Not-connect-to-WiFi-after-netplan-app.patch
659-0002-Fix-process_link_changes-handling-up-interfaces.patch
660-0003-Call-daemon-reload-after-we-touched-systemd-unit-fil.patch
661-0004-Fix-autopkgtest-on-arm64-with-NM-1.24-146.patch
662-0005-Fix-GCC-10-fno-common-linker-errors-LP-1875412-155.patch
663diff --git a/debian/tests/control b/debian/tests/control
664index 727e3da..bb557aa 100644
665--- a/debian/tests/control
666+++ b/debian/tests/control
667@@ -1,3 +1,17 @@
668+Test-Command: python3 tests/integration/run.py --test=ovs
669+Tests-Directory: tests/integration
670+Depends: @,
671+ systemd,
672+ network-manager,
673+ hostapd,
674+ dnsmasq-base,
675+ libnm0,
676+ python3-gi,
677+ gir1.2-nm-1.0,
678+ openvswitch-switch,
679+Restrictions: allow-stderr, needs-root, isolation-machine
680+Features: test-name=ovs
681+
682 Test-Command: python3 tests/integration/run.py --test=ethernets
683 Tests-Directory: tests/integration
684 Depends: @,
685@@ -86,6 +100,7 @@ Depends: @,
686 libnm0,
687 python3-gi,
688 gir1.2-nm-1.0,
689+ wireguard-tools,
690 Restrictions: allow-stderr, needs-root, isolation-machine
691 Features: test-name=tunnels
692
693diff --git a/doc/netplan.md b/doc/netplan.md
694index c4b0ba3..97dc945 100644
695--- a/doc/netplan.md
696+++ b/doc/netplan.md
697@@ -43,8 +43,8 @@ configuration files. Their primary purpose is to serve as anchor names for
698 composite devices, for example to enumerate the members of a bridge that is
699 currently being defined.
700
701-If an interface is defined with an ID in a configuration file; it will be
702-brought up by the applicable renderer. To not have netplan touch an interface
703+(Since 0.97) If an interface is defined with an ID in a configuration file; it will
704+be brought up by the applicable renderer. To not have netplan touch an interface
705 at all, it should be completely omitted from the netplan configuration files.
706
707 There are two physically/structurally different classes of device definitions,
708@@ -89,8 +89,8 @@ Virtual devices
709 : Current interface name. Globs are supported, and the primary use case
710 for matching on names, as selecting one fixed name can be more easily
711 achieved with having no ``match:`` at all and just using the ID (see
712- above). Note that currently only networkd supports globbing,
713- NetworkManager does not.
714+ above).
715+ (``NetworkManager``: as of v1.14.0)
716
717 ``macaddress`` (scalar)
718 : Device's MAC address in the form "XX:XX:XX:XX:XX:XX". Globs are not
719@@ -133,10 +133,80 @@ Virtual devices
720
721 : Enable wake on LAN. Off by default.
722
723-``emit-lldp`` (bool)
724+``emit-lldp`` (bool) – since **0.99**
725
726 : (networkd backend only) Whether to emit LLDP packets. Off by default.
727
728+``openvswitch`` (mapping) – since **0.100**
729+
730+: This provides additional configuration for the network device for openvswitch.
731+ If openvswitch is not available on the system, netplan treats the presence of
732+ openvswitch configuration as an error.
733+
734+ Any supported network device that is declared with the ``openvswitch`` mapping
735+ (or any bond/bridge that includes an interface with an openvswitch configuration)
736+ will be created in openvswitch instead of the defined renderer.
737+ In the case of a ``vlan`` definition declared the same way, netplan will create
738+ a fake VLAN bridge in openvswitch with the requested vlan properties.
739+
740+ ``external-ids`` (mapping) – since **0.100**
741+ : Passed-through directly to OpenVSwitch
742+
743+ ``other-config`` (mapping) – since **0.100**
744+ : Passed-through directly to OpenVSwitch
745+
746+ ``lacp`` (scalar) – since **0.100**
747+ : Valid for bond interfaces. Accepts ``active``, ``passive`` or ``off`` (the default).
748+
749+ ``fail-mode`` (scalar) – since **0.100**
750+ : Valid for bridge interfaces. Accepts ``secure`` or ``standalone`` (the default).
751+
752+ ``mcast-snooping`` (bool) – since **0.100**
753+ : Valid for bridge interfaces. False by default.
754+
755+ ``protocols`` (sequence of scalars) – since **0.100**
756+ : Valid for bridge interfaces or the network section. List of protocols to be used when
757+ negotiating a connection with the controller. Accepts ``OpenFlow10``, ``OpenFlow11``,
758+ ``OpenFlow12``, ``OpenFlow13``, ``OpenFlow14``, ``OpenFlow15`` and ``OpenFlow16``.
759+
760+ ``rstp`` (bool) – since **0.100**
761+ : Valid for bridge interfaces. False by default.
762+
763+ ``controller`` (mapping) – since **0.100**
764+ : Valid for bridge interfaces. Specify an external OpenFlow controller.
765+
766+ ``addresses`` (sequence of scalars)
767+ : Set the list of addresses to use for the controller targets. The
768+ syntax of these addresses is as defined in ovs-vsctl(8). Example:
769+ addresses: ``[tcp:127.0.0.1:6653, "ssl:[fe80::1234%eth0]:6653"]``
770+
771+ ``connection-mode`` (scalar)
772+ : Set the connection mode for the controller. Supported options are
773+ ``in-band`` and ``out-of-band``. The default is ``in-band``.
774+
775+ ``ports`` (sequence of sequence of scalars) – since **0.100**
776+ : OpenvSwitch patch ports. Each port is declared as a pair of names
777+ which can be referenced as interfaces in dependent virtual devices
778+ (bonds, bridges).
779+
780+ Example:
781+
782+ openvswitch:
783+ ports:
784+ - [patch0-1, patch1-0]
785+
786+ ``ssl`` (mapping) – since **0.100**
787+ : Valid for global ``openvswitch`` settings. Options for configuring SSL
788+ server endpoint for the switch.
789+
790+ ``ca-cert`` (scalar)
791+ : Path to a file containing the CA certificate to be used.
792+
793+ ``certificate`` (scalar)
794+ : Path to a file containing the server certificate.
795+
796+ ``private-key`` (scalar)
797+ : Path to a file containing the private key for the server.
798
799 ## Common properties for all device types
800
801@@ -144,13 +214,13 @@ Virtual devices
802
803 : Use the given networking backend for this definition. Currently supported are
804 ``networkd`` and ``NetworkManager``. This property can be specified globally
805- in ``networks:``, for a device type (in e. g. ``ethernets:``) or
806+ in ``network:``, for a device type (in e. g. ``ethernets:``) or
807 for a particular device definition. Default is ``networkd``.
808
809- The ``renderer`` property has one additional acceptable value for vlan objects
810- (i. e. defined in ``vlans:``): ``sriov``. If a vlan is defined with the ``sriov``
811- renderer for an SR-IOV Virtual Function interface, this causes netplan to set
812- up a hardware VLAN filter for it. There can be only one defined per VF.
813+ (Since 0.99) The ``renderer`` property has one additional acceptable value for vlan
814+ objects (i. e. defined in ``vlans:``): ``sriov``. If a vlan is defined with the
815+ ``sriov`` renderer for an SR-IOV Virtual Function interface, this causes netplan to
816+ set up a hardware VLAN filter for it. There can be only one defined per VF.
817
818 ``dhcp4`` (bool)
819
820@@ -172,7 +242,7 @@ Virtual devices
821 Note that **``rdnssd``**(8) is required to use RDNSS with networkd. No extra
822 software is required for NetworkManager.
823
824-``ipv6-mtu`` (scalar)
825+``ipv6-mtu`` (scalar) – since **0.98**
826 : Set the IPv6 MTU (only supported with `networkd` backend). Note
827 that needing to set this is an unusual requirement.
828
829@@ -231,7 +301,7 @@ Virtual devices
830 When enabled, accept Router Advertisements. When disabled, do not respond to
831 Router Advertisements. If unset use the host kernel default setting.
832
833-``addresses`` (sequence of scalars)
834+``addresses`` (sequence of scalars and mappings)
835
836 : Add static addresses to the interface in addition to the ones received
837 through DHCP or RA. Each sequence entry is in CIDR notation, i. e. of the
838@@ -242,13 +312,41 @@ Virtual devices
839 configured and DHCP is disabled, the interface may still be brought online,
840 but will not be addressable from the network.
841
842+ In addition to the addresses themselves one can specify configuration
843+ parameters as mappings. Current supported options are:
844+
845+ ``lifetime`` (scalar) – since **0.100**
846+ : Default: ``forever``. This can be ``forever`` or ``0`` and corresponds
847+ to the ``PreferredLifetime`` option in ``systemd-networkd``'s Address
848+ section. Currently supported on the ``networkd`` backend only.
849+
850+ ``label`` (scalar) – since **0.100**
851+ : An IP address label, equivalent to the ``ip address label``
852+ command. Currently supported on the ``networkd`` backend only.
853+
854 Example: ``addresses: [192.168.14.2/24, "2001:1::1/64"]``
855
856-``ipv6-address-generation`` (scalar)
857+ Example:
858+
859+ ethernets:
860+ eth0:
861+ addresses:
862+ - 10.0.0.15/24:
863+ lifetime: 0
864+ label: "maas"
865+ - "2001:1::1/64"
866+
867+``ipv6-address-generation`` (scalar) – since **0.99**
868
869 : Configure method for creating the address for use with RFC4862 IPv6
870- Stateless Address Autoconfiguration. Possible values are ``eui64``
871- or ``stable-privacy``.
872+ Stateless Address Autoconfiguration (only supported with `NetworkManager`
873+ backend). Possible values are ``eui64`` or ``stable-privacy``.
874+
875+``ipv6-address-token`` (scalar) – since **0.100**
876+
877+: Define an IPv6 address token for creating a static interface identifier for
878+ IPv6 Stateless Address Autoconfiguration. This is mutually exclusive with
879+ ``ipv6-address-generation``.
880
881 ``gateway4``, ``gateway6`` (scalar)
882
883@@ -333,11 +431,11 @@ similar to ``gateway*``, and ``search:`` is a list of search domains.
884 dhcp6: true
885 optional-addresses: [ ipv4-ll, dhcp6 ]
886
887-``routes`` (mapping)
888+``routes`` (sequence of mappings)
889
890 : Configure static routing for the device; see the ``Routing`` section below.
891
892-``routing-policy`` (mapping)
893+``routing-policy`` (sequence of mappings)
894
895 : Configure policy routing for the device; see the ``Routing`` section below.
896
897@@ -358,6 +456,8 @@ When using the NetworkManager backend, different values may be specified for
898 ``dhcp4-overrides`` and ``dhcp6-overrides``, and will be applied to the DHCP
899 client processes as specified in the netplan YAML.
900
901+``dhcp4-overrides``, ``dhcp6-overrides`` (mapping)
902+
903 : The ``dhcp4-overrides`` and ``dhcp6-overrides`` mappings override the
904 default DHCP behavior.
905
906@@ -405,15 +505,15 @@ client processes as specified in the netplan YAML.
907
908 ``route-metric`` (scalar)
909 : Use this value for default metric for automatically-added routes.
910- Use this to prioritize routes for devices by setting a higher metric
911+ Use this to prioritize routes for devices by setting a lower metric
912 on a preferred interface. Available for both the ``networkd`` and
913 ``NetworkManager`` backends.
914
915- ``use-domains`` (scalar)
916- : Takes a boolean, or the special value "route". When true, the domain
917+ ``use-domains`` (scalar) – since **0.98**
918+ : Takes a boolean, or the special value "route". When true, the domain
919 name received from the DHCP server will be used as DNS search domain
920 over this link, similar to the effect of the Domains= setting. If set
921- to "route", the domain name received from the DHCP server will be
922+ to "route", the domain name received from the DHCP server will be
923 used for routing DNS queries only, but not for searching, similar to
924 the effect of the Domains= setting when the argument is prefixed with
925 "~".
926@@ -438,6 +538,7 @@ These options are available for all types of interfaces.
927
928 ``from`` (scalar)
929 : Set a source IP address for traffic going through the route.
930+ (``NetworkManager``: as of v1.8.0)
931
932 ``to`` (scalar)
933 : Destination address for the route.
934@@ -448,6 +549,7 @@ These options are available for all types of interfaces.
935 ``on-link`` (bool)
936 : When set to "true", specifies that the route is directly connected
937 to the interface.
938+ (``NetworkManager``: as of v1.12.0 for IPv4 and v1.18.0 for IPv6)
939
940 ``metric`` (scalar)
941 : The relative priority of the route. Must be a positive integer value.
942@@ -458,7 +560,8 @@ These options are available for all types of interfaces.
943
944 ``scope`` (scalar)
945 : The route scope, how wide-ranging it is to the network. Possible
946- values are "global", "link", or "host".
947+ values are "global", "link", or "host". ``NetworkManager`` does
948+ not support setting a scope.
949
950 ``table`` (scalar)
951 : The table number to use for the route. In some scenarios, it may be
952@@ -467,6 +570,7 @@ These options are available for all types of interfaces.
953 parameter. Allowed values are positive integers starting from 1.
954 Some values are already in use to refer to specific routing tables:
955 see ``/etc/iproute2/rt_tables``.
956+ (``NetworkManager``: as of v1.10.0)
957
958 ``routing-policy`` (mapping)
959
960@@ -555,7 +659,7 @@ interfaces, as well as individual wifi networks, by means of the ``auth`` block.
961 : Password to use to decrypt the private key specified in
962 ``client-key`` if it is encrypted.
963
964- ``phase2-auth`` (scalar)
965+ ``phase2-auth`` (scalar) – since **0.99**
966 : Phase 2 authentication mechanism.
967
968
969@@ -563,7 +667,7 @@ interfaces, as well as individual wifi networks, by means of the ``auth`` block.
970 Ethernet device definitions, beyond common ones described above, also support
971 some additional properties that can be used for SR-IOV devices.
972
973-``link`` (scalar)
974+``link`` (scalar) – since **0.99**
975
976 : (SR-IOV devices only) The ``link`` property declares the device as a
977 Virtual Function of the selected Physical Function device, as identified
978@@ -576,7 +680,7 @@ Example:
979 enp1s16f1:
980 link: enp1
981
982-``virtual-function-count`` (scalar)
983+``virtual-function-count`` (scalar) – since **0.99**
984
985 : (SR-IOV devices only) In certain special cases VFs might need to be
986 configured outside of netplan. For such configurations ``virtual-function-count``
987@@ -585,47 +689,67 @@ Example:
988 VFs as are defined in the netplan configuration. This should be used for special
989 cases only.
990
991+ **Requires feature: sriov**
992+
993 ## Properties for device type ``modems:``
994-GSM/CDMA modem configuration is only supported for the ``NetworkManager`` backend. ``systemd-networkd`` does
995-not support modems.
996+GSM/CDMA modem configuration is only supported for the ``NetworkManager``
997+backend. ``systemd-networkd`` does not support modems.
998
999-``apn`` (scalar)
1000-: Set the carrier APN (Access Point Name). This can be omitted if ``auto-config`` is enabled.
1001+**Requires feature: modems**
1002
1003-``auto-config`` (bool)
1004-: Specify whether to try and autoconfigure the modem by doing a lookup of the carrier
1005- against the Mobile Broadband Provider database. This may not work for all carriers.
1006+``apn`` (scalar) – since **0.99**
1007
1008-``device-id`` (scalar)
1009-: Specify the device ID (as given by the WWAN management service) of the modem to match.
1010- This can be found using ``mmcli``.
1011+: Set the carrier APN (Access Point Name). This can be omitted if
1012+ ``auto-config`` is enabled.
1013+
1014+``auto-config`` (bool) – since **0.99**
1015+
1016+: Specify whether to try and autoconfigure the modem by doing a lookup of
1017+ the carrier against the Mobile Broadband Provider database. This may not
1018+ work for all carriers.
1019+
1020+``device-id`` (scalar) – since **0.99**
1021+
1022+: Specify the device ID (as given by the WWAN management service) of the
1023+ modem to match. This can be found using ``mmcli``.
1024+
1025+``network-id`` (scalar) – since **0.99**
1026+
1027+: Specify the Network ID (GSM LAI format). If this is specified, the device
1028+ will not roam networks.
1029+
1030+``number`` (scalar) – since **0.99**
1031+
1032+: The number to dial to establish the connection to the mobile broadband
1033+ network. (Deprecated for GSM)
1034
1035-``network-id`` (scalar)
1036-: Specify the Network ID (GSM LAI format). If this is specified, the device will not roam networks.
1037+``password`` (scalar) – since **0.99**
1038
1039-``number`` (scalar)
1040-: The number to dial to establish the connection to the mobile broadband network. (Deprecated for GSM)
1041+: Specify the password used to authenticate with the carrier network. This
1042+ can be omitted if ``auto-config`` is enabled.
1043
1044-``password`` (scalar)
1045-: Specify the password used to authenticate with the carrier network. This can be omitted
1046- if ``auto-config`` is enabled.
1047+``pin`` (scalar) – since **0.99**
1048
1049-``pin`` (scalar)
1050 : Specify the SIM PIN to allow it to operate if a PIN is set.
1051
1052-``sim-id`` (scalar)
1053-: Specify the SIM unique identifier (as given by the WWAN management service) which this
1054- connection applies to. If given, the connection will apply to any device also allowed by
1055- ``device-id`` which contains a SIM card matching the given identifier.
1056+``sim-id`` (scalar) – since **0.99**
1057
1058-``sim-operator-id`` (scalar)
1059-: Specify the MCC/MNC string (such as "310260" or "21601") which identifies the carrier that
1060- this connection should apply to. If given, the connection will apply to any device also
1061- allowed by ``device-id`` and ``sim-id`` which contains a SIM card provisioned by the given operator.
1062+: Specify the SIM unique identifier (as given by the WWAN management service)
1063+ which this connection applies to. If given, the connection will apply to
1064+ any device also allowed by ``device-id`` which contains a SIM card matching
1065+ the given identifier.
1066
1067-``username`` (scalar)
1068-: Specify the username used to authentiate with the carrier network. This can be omitted if
1069- ``auto-config`` is enabled.
1070+``sim-operator-id`` (scalar) – since **0.99**
1071+
1072+: Specify the MCC/MNC string (such as "310260" or "21601") which identifies
1073+ the carrier that this connection should apply to. If given, the connection
1074+ will apply to any device also allowed by ``device-id`` and ``sim-id``
1075+ which contains a SIM card provisioned by the given operator.
1076+
1077+``username`` (scalar) – since **0.99**
1078+
1079+: Specify the username used to authentiate with the carrier network. This
1080+ can be omitted if ``auto-config`` is enabled.
1081
1082 ## Properties for device type ``wifis:``
1083 Note that ``systemd-networkd`` does not natively support wifi, so you need
1084@@ -657,21 +781,27 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi.
1085 and ``adhoc`` (peer to peer networks without a central access point).
1086 ``ap`` is only supported with NetworkManager.
1087
1088- ``bssid`` (scalar)
1089+ ``bssid`` (scalar) – since **0.99**
1090 : If specified, directs the device to only associate with the given
1091 access point.
1092
1093- ``band`` (scalar)
1094+ ``band`` (scalar) – since **0.99**
1095 : Possible bands are ``5GHz`` (for 5GHz 802.11a) and ``2.4GHz``
1096 (for 2.4GHz 802.11), do not restrict the 802.11 frequency band of the
1097 network if unset (the default).
1098
1099- ``channel`` (scalar)
1100+ ``channel`` (scalar) – since **0.99**
1101 : Wireless channel to use for the Wi-Fi connection. Because channel
1102 numbers overlap between bands, this property takes effect only if
1103 the ``band`` property is also set.
1104
1105-``wakeonwlan`` (sequence of scalars)
1106+ ``hidden`` (bool) – since **0.100**
1107+ : Set to ``true`` to change the SSID scan technique for connecting to
1108+ hidden WiFi networks. Note this may have slower performance compared
1109+ to ``false`` (the default) when connecting to publicly broadcast
1110+ SSIDs.
1111+
1112+``wakeonwlan`` (sequence of scalars) – since **0.99**
1113
1114 : This enables WakeOnWLan on supported devices. Not all drivers support all
1115 options. May be any combination of ``any``, ``disconnect``, ``magic_pkt``,
1116@@ -783,6 +913,8 @@ wpasupplicant installed if you let the ``networkd`` renderer handle wifi.
1117 ``balance-rr`` (round robin). Possible values are ``balance-rr``,
1118 ``active-backup``, ``balance-xor``, ``broadcast``, ``802.3ad``,
1119 ``balance-tlb``, and ``balance-alb``.
1120+ For OpenVSwitch ``active-backup`` and the additional modes
1121+ ``balance-tcp`` and ``balance-slb`` are supported.
1122
1123 ``lacp-rate`` (scalar)
1124 : Set the rate at which LACPDUs are transmitted. This is only useful
1125@@ -923,8 +1055,9 @@ more general information about tunnels.
1126 ``mode`` (scalar)
1127
1128 : Defines the tunnel mode. Valid options are ``sit``, ``gre``, ``ip6gre``,
1129- ``ipip``, ``ipip6``, ``ip6ip6``, ``vti``, and ``vti6``. Additionally,
1130- the ``networkd`` backend also supports ``gretap`` and ``ip6gretap`` modes.
1131+ ``ipip``, ``ipip6``, ``ip6ip6``, ``vti``, ``vti6`` and ``wireguard``.
1132+ Additionally, the ``networkd`` backend also supports ``gretap`` and
1133+ ``ip6gretap`` modes.
1134 In addition, the ``NetworkManager`` backend supports ``isatap`` tunnels.
1135
1136 ``local`` (scalar)
1137@@ -938,14 +1071,16 @@ more general information about tunnels.
1138 ``key`` (scalar or mapping)
1139
1140 : Define keys to use for the tunnel. The key can be a number or a dotted
1141- quad (an IPv4 address). It is used for identification of IP transforms.
1142- This is only required for ``vti`` and ``vti6`` when using the networkd
1143- backend, and for ``gre`` or ``ip6gre`` tunnels when using the
1144- NetworkManager backend.
1145+ quad (an IPv4 address). For ``wireguard`` it can be a base64-encoded
1146+ private key or (as of ``networkd`` v242+) an absolute path to a file,
1147+ containing the private key (since 0.100).
1148+ It is used for identification of IP transforms. This is only required
1149+ for ``vti`` and ``vti6`` when using the networkd backend, and for
1150+ ``gre`` or ``ip6gre`` tunnels when using the NetworkManager backend.
1151
1152 This field may be used as a scalar (meaning that a single key is
1153- specified and to be used for both input and output key), or as a mapping,
1154- where you can then further specify ``input`` and ``output``.
1155+ specified and to be used for input, output and private key), or as a
1156+ mapping, where you can further specify ``input``/``output``/``private``.
1157
1158 ``input`` (scalar)
1159 : The input key for the tunnel
1160@@ -953,6 +1088,15 @@ more general information about tunnels.
1161 ``output`` (scalar)
1162 : The output key for the tunnel
1163
1164+ ``private`` (scalar) – since **0.100**
1165+ : A base64-encoded private key required for Wireguard tunnels. When the
1166+ ``systemd-networkd`` backend (v242+) is used, this can also be an
1167+ absolute path to a file containing the private key.
1168+
1169+``keys`` (scalar or mapping)
1170+
1171+: Alternate name for the ``key`` field. See above.
1172+
1173 Examples:
1174
1175 tunnels:
1176@@ -971,10 +1115,91 @@ Examples:
1177 remote: ...
1178 key: 59568549
1179
1180-``keys`` (scalar or mapping)
1181+ tunnels:
1182+ wg0:
1183+ mode: wireguard
1184+ addresses: [...]
1185+ peers:
1186+ - keys:
1187+ public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=
1188+ shared: /path/to/shared.key
1189+ ...
1190+ key: mNb7OIIXTdgW4khM7OFlzJ+UPs7lmcWHV7xjPgakMkQ=
1191+
1192+ tunnels:
1193+ wg0:
1194+ mode: wireguard
1195+ addresses: [...]
1196+ peers:
1197+ - keys:
1198+ public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=
1199+ ...
1200+ keys:
1201+ private: /path/to/priv.key
1202
1203-: Alternate name for the ``key`` field. See above.
1204
1205+Wireguard specific keys:
1206+
1207+ ``mark`` (scalar) – since **0.100**
1208+ : Firewall mark for outgoing WireGuard packets from this interface,
1209+ optional.
1210+
1211+ ``port`` (scalar) – since **0.100**
1212+ : UDP port to listen at or ``auto``. Optional, defaults to ``auto``.
1213+
1214+ ``peers`` (sequence of mappings) – since **0.100**
1215+ : A list of peers, each having keys documented below.
1216+
1217+ Example:
1218+
1219+ tunnels:
1220+ wg0:
1221+ mode: wireguard
1222+ key: /path/to/private.key
1223+ mark: 42
1224+ port: 5182
1225+ peers:
1226+ - keys:
1227+ public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=
1228+ allowed-ips: [0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]
1229+ keepalive: 23
1230+ endpoint: 1.2.3.4:5
1231+ - keys:
1232+ public: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=
1233+ shared: /some/shared.key
1234+ allowed-ips: [10.10.10.20/24]
1235+ keepalive: 22
1236+ endpoint: 5.4.3.2:1
1237+
1238+ ``endpoint`` (scalar) – since **0.100**
1239+ : Remote endpoint IPv4/IPv6 address or a hostname, followed by a colon
1240+ and a port number.
1241+
1242+ ``allowed-ips`` (sequence of scalars) – since **0.100**
1243+ : A list of IP (v4 or v6) addresses with CIDR masks from which this peer
1244+ is allowed to send incoming traffic and to which outgoing traffic for
1245+ this peer is directed. The catch-all 0.0.0.0/0 may be specified for
1246+ matching all IPv4 addresses, and ::/0 may be specified for matching
1247+ all IPv6 addresses.
1248+
1249+ ``keepalive`` (scalar) – since **0.100**
1250+ : An interval in seconds, between 1 and 65535 inclusive, of how often to
1251+ send an authenticated empty packet to the peer for the purpose of
1252+ keeping a stateful firewall or NAT mapping valid persistently. Optional.
1253+
1254+ ``keys`` (mapping) – since **0.100**
1255+ : Define keys to use for the Wireguard peers.
1256+
1257+ This field can be used as a mapping, where you can further specify the
1258+ ``public`` and ``shared`` keys.
1259+
1260+ ``public`` (scalar) – since **0.100**
1261+ : A base64-encoded public key, requried for Wireguard peers.
1262+
1263+ ``shared`` (scalar) – since **0.100**
1264+ : A base64-encoded preshared key. Optional for Wireguard peers.
1265+ When the ``systemd-networkd`` backend (v242+) is used, this can
1266+ also be an absolute path to a file containing the preshared key.
1267
1268 ## Properties for device type ``vlans:``
1269
1270@@ -1009,24 +1234,24 @@ backends may require to record some of their own parameters in netplan,
1271 especially if the netplan definitions are generated automatically by the
1272 consumer of that backend. Currently, this is only used with ``NetworkManager``.
1273
1274-``networkmanager`` (mapping)
1275+``networkmanager`` (mapping) – since **0.99**
1276
1277 : Keeps the NetworkManager-specific configuration parameters used by the
1278 daemon to recognize connections.
1279
1280- ``name`` (scalar)
1281+ ``name`` (scalar) – since **0.99**
1282 : Set the display name for the connection.
1283
1284- ``uuid`` (scalar)
1285+ ``uuid`` (scalar) – since **0.99**
1286 : Defines the UUID (unique identifier) for this connection, as
1287 generated by NetworkManager itself.
1288
1289- ``stable-id`` (scalar)
1290+ ``stable-id`` (scalar) – since **0.99**
1291 : Defines the stable ID (a different form of a connection name) used
1292 by NetworkManager in case the name of the connection might otherwise
1293 change, such as when sharing connections between users.
1294
1295- ``device`` (scalar)
1296+ ``device`` (scalar) – since **0.99**
1297 : Defines the interface name for which this connection applies.
1298
1299
1300@@ -1114,7 +1339,6 @@ This is a complex example which shows most available features:
1301 switchports:
1302 # all cards on second PCI bus unconfigured by
1303 # themselves, will be added to br0 below
1304- # note: globbing is not supported by NetworkManager
1305 match:
1306 name: enp2*
1307 mtu: 1280
1308diff --git a/examples/sriov.yaml b/examples/sriov.yaml
1309new file mode 100644
1310index 0000000..67de132
1311--- /dev/null
1312+++ b/examples/sriov.yaml
1313@@ -0,0 +1,14 @@
1314+network:
1315+ version: 2
1316+ renderer: networkd
1317+ ethernets:
1318+ eno1:
1319+ mtu: 9000
1320+ enp1s16f1:
1321+ link: eno1
1322+ addresses : [ "10.15.98.25/24" ]
1323+ vf1:
1324+ match:
1325+ name: enp1s16f[2-3]
1326+ link: eno1
1327+ addresses : [ "10.15.99.25/24" ]
1328diff --git a/examples/sriov_vlan.yaml b/examples/sriov_vlan.yaml
1329new file mode 100644
1330index 0000000..2c664d7
1331--- /dev/null
1332+++ b/examples/sriov_vlan.yaml
1333@@ -0,0 +1,18 @@
1334+network:
1335+ version: 2
1336+ renderer: networkd
1337+ ethernets:
1338+ eno1:
1339+ mtu: 9000
1340+ enp1s16f1:
1341+ link: eno1
1342+ addresses : [ "10.15.98.25/24" ]
1343+ vlans:
1344+ vlan1:
1345+ id: 15
1346+ link: enp1s16f1
1347+ addresses: [ "10.3.99.5/24" ]
1348+ vlan2_hw:
1349+ id: 10
1350+ link: enp1s16f1
1351+ renderer: sriov
1352diff --git a/examples/static.yaml b/examples/static.yaml
1353index e618909..977710b 100644
1354--- a/examples/static.yaml
1355+++ b/examples/static.yaml
1356@@ -7,5 +7,5 @@ network:
1357 - 10.10.10.2/24
1358 gateway4: 10.10.10.1
1359 nameservers:
1360- search: [mydomain,otherdomain]
1361- addresses: [10.10.10.1, 1.1.1.1]
1362+ search: [mydomain, otherdomain]
1363+ addresses: [10.10.10.1, 1.1.1.1]
1364diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py
1365index 0ec95f5..fb5a6ad 100644
1366--- a/netplan/cli/commands/apply.py
1367+++ b/netplan/cli/commands/apply.py
1368@@ -3,6 +3,7 @@
1369 # Copyright (C) 2018-2020 Canonical, Ltd.
1370 # Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com>
1371 # Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@canonical.com>
1372+# Author: Lukas 'slyon' Märdian <lukas.maerdian@canonical.com>
1373 #
1374 # This program is free software; you can redistribute it and/or modify
1375 # it under the terms of the GNU General Public License as published by
1376@@ -28,6 +29,7 @@ import shutil
1377 import netplan.cli.utils as utils
1378 from netplan.configmanager import ConfigManager, ConfigurationError
1379 from netplan.cli.sriov import apply_sriov_config
1380+from netplan.cli.ovs import apply_ovs_cleanup
1381
1382 import netifaces
1383
1384@@ -38,15 +40,33 @@ class NetplanApply(utils.NetplanCommand):
1385 super().__init__(command_id='apply',
1386 description='Apply current netplan config to running system',
1387 leaf=True)
1388+ self.sriov_only = False
1389+ self.only_ovs_cleanup = False
1390
1391 def run(self): # pragma: nocover (covered in autopkgtest)
1392- self.func = NetplanApply.command_apply
1393+ self.parser.add_argument('--sriov-only', action='store_true',
1394+ help='Only apply SR-IOV related configuration and exit')
1395+ self.parser.add_argument('--only-ovs-cleanup', action='store_true',
1396+ help='Only clean up old OpenVSwitch interfaces and exit')
1397+
1398+ self.func = self.command_apply
1399
1400 self.parse_args()
1401 self.run_command()
1402
1403- @staticmethod
1404- def command_apply(run_generate=True, sync=False, exit_on_error=True): # pragma: nocover (covered in autopkgtest)
1405+ def command_apply(self, run_generate=True, sync=False, exit_on_error=True): # pragma: nocover (covered in autopkgtest)
1406+ config_manager = ConfigManager()
1407+
1408+ # For certain use-cases, we might want to only apply specific configuration.
1409+ # If we only need SR-IOV configuration, do that and exit early.
1410+ if self.sriov_only:
1411+ NetplanApply.process_sriov_config(config_manager, exit_on_error)
1412+ return
1413+ # If we only need OpenVSwitch cleanup, do that and exit early.
1414+ elif self.only_ovs_cleanup:
1415+ NetplanApply.process_ovs_cleanup(config_manager, False, False, exit_on_error)
1416+ return
1417+
1418 # if we are inside a snap, then call dbus to run netplan apply instead
1419 if "SNAP" in os.environ:
1420 # TODO: maybe check if we are inside a classic snap and don't do
1421@@ -73,8 +93,16 @@ class NetplanApply(utils.NetplanCommand):
1422 else:
1423 return
1424
1425+ ovs_cleanup_service = '/run/systemd/system/netplan-ovs-cleanup.service'
1426 old_files_networkd = bool(glob.glob('/run/systemd/network/*netplan-*'))
1427- old_files_nm = bool(glob.glob('/run/NetworkManager/system-connections/netplan-*'))
1428+ old_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*')
1429+ # Ignore netplan-ovs-cleanup.service, as it can always be there
1430+ if ovs_cleanup_service in old_ovs_glob:
1431+ old_ovs_glob.remove(ovs_cleanup_service)
1432+ old_files_ovs = bool(old_ovs_glob)
1433+ old_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*')
1434+ nm_ifaces = utils.nm_interfaces(old_nm_glob, netifaces.interfaces())
1435+ old_files_nm = bool(old_nm_glob)
1436
1437 generator_call = []
1438 generate_out = None
1439@@ -89,7 +117,6 @@ class NetplanApply(utils.NetplanCommand):
1440 else:
1441 raise ConfigurationError("the configuration could not be generated")
1442
1443- config_manager = ConfigManager()
1444 devices = netifaces.interfaces()
1445
1446 # Re-start service when
1447@@ -101,14 +128,36 @@ class NetplanApply(utils.NetplanCommand):
1448 restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*'))
1449 if not restart_networkd and old_files_networkd:
1450 restart_networkd = True
1451- restart_nm = bool(glob.glob('/run/NetworkManager/system-connections/netplan-*'))
1452+ restart_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*')
1453+ # Ignore netplan-ovs-cleanup.service, as it can always be there
1454+ if ovs_cleanup_service in restart_ovs_glob:
1455+ restart_ovs_glob.remove(ovs_cleanup_service)
1456+ restart_ovs = bool(restart_ovs_glob)
1457+ if not restart_ovs and old_files_ovs:
1458+ # OVS is managed via systemd units
1459+ restart_networkd = True
1460+
1461+ restart_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*')
1462+ nm_ifaces.update(utils.nm_interfaces(restart_nm_glob, devices))
1463+ restart_nm = bool(restart_nm_glob)
1464 if not restart_nm and old_files_nm:
1465 restart_nm = True
1466
1467 # stop backends
1468 if restart_networkd:
1469 logging.debug('netplan generated networkd configuration changed, restarting networkd')
1470- utils.systemctl_networkd('stop', sync=sync, extra_services=['netplan-wpa@*.service'])
1471+ # Running 'systemctl daemon-reload' will re-run the netplan systemd generator,
1472+ # so let's make sure we only run it iff we're willing to run 'netplan generate'
1473+ if run_generate:
1474+ utils.systemctl_daemon_reload()
1475+ # Clean up any old netplan related OVS ports/bonds/bridges, if applicable
1476+ NetplanApply.process_ovs_cleanup(config_manager, old_files_ovs, restart_ovs, exit_on_error)
1477+ wpa_services = ['netplan-wpa-*.service']
1478+ # Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an
1479+ # upgraded system, we need to make sure to stop those.
1480+ if utils.systemctl_is_active('netplan-wpa@*.service'):
1481+ wpa_services.insert(0, 'netplan-wpa@*.service')
1482+ utils.systemctl_networkd('stop', sync=sync, extra_services=wpa_services)
1483 else:
1484 logging.debug('no netplan generated networkd configuration exists')
1485
1486@@ -117,6 +166,8 @@ class NetplanApply(utils.NetplanCommand):
1487 if utils.nm_running():
1488 # restarting NM does not cause new config to be applied, need to shut down devices first
1489 for device in devices:
1490+ if device not in nm_ifaces:
1491+ continue # do not touch this interface
1492 # ignore failures here -- some/many devices might not be managed by NM
1493 try:
1494 utils.nmcli(['device', 'disconnect', device])
1495@@ -135,14 +186,6 @@ class NetplanApply(utils.NetplanCommand):
1496 config_manager.parse()
1497 changes = NetplanApply.process_link_changes(devices, config_manager)
1498
1499- # apply any SR-IOV related changes, if applicable
1500- try:
1501- apply_sriov_config(devices, config_manager)
1502- except (ConfigurationError, RuntimeError) as e:
1503- logging.error(str(e))
1504- if exit_on_error:
1505- sys.exit(1)
1506-
1507 # if the interface is up, we can still apply some .link file changes
1508 devices = netifaces.interfaces()
1509 for device in devices:
1510@@ -167,11 +210,21 @@ class NetplanApply(utils.NetplanCommand):
1511
1512 subprocess.check_call(['udevadm', 'settle'])
1513
1514+ # apply any SR-IOV related changes, if applicable
1515+ NetplanApply.process_sriov_config(config_manager, exit_on_error)
1516+
1517 # (re)start backends
1518 if restart_networkd:
1519- netplan_wpa = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-wpa@*.service')]
1520- utils.systemctl_networkd('start', sync=sync, extra_services=netplan_wpa)
1521+ netplan_wpa = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-wpa-*.service')]
1522+ netplan_ovs = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-ovs-*.service')]
1523+ # Run 'systemctl start' command synchronously, to avoid race conditions
1524+ # with 'oneshot' systemd service units, e.g. netplan-ovs-*.service.
1525+ utils.systemctl_networkd('start', sync=True, extra_services=netplan_wpa + netplan_ovs)
1526 if restart_nm:
1527+ # Flush all IP addresses of NM managed interfaces, to avoid NM creating
1528+ # new, non netplan-* connection profiles, using the existing IPs.
1529+ for iface in utils.nm_interfaces(restart_nm_glob, devices):
1530+ utils.ip_addr_flush(iface)
1531 utils.systemctl_network_manager('start', sync=sync)
1532
1533 @staticmethod
1534@@ -238,6 +291,9 @@ class NetplanApply(utils.NetplanCommand):
1535 continue
1536
1537 driver_name = utils.get_interface_driver_name(interface, only_down=True)
1538+ if not driver_name:
1539+ # don't allow up interfaces to match by mac
1540+ continue
1541 macaddress = utils.get_interface_macaddress(interface)
1542 if driver_name in matches['by-driver']:
1543 new_name = matches['by-driver'][driver_name]
1544@@ -252,3 +308,21 @@ class NetplanApply(utils.NetplanCommand):
1545
1546 logging.debug(changes)
1547 return changes
1548+
1549+ @staticmethod
1550+ def process_sriov_config(config_manager, exit_on_error=True): # pragma: nocover (covered in autopkgtest)
1551+ try:
1552+ apply_sriov_config(config_manager)
1553+ except (ConfigurationError, RuntimeError) as e:
1554+ logging.error(str(e))
1555+ if exit_on_error:
1556+ sys.exit(1)
1557+
1558+ @staticmethod
1559+ def process_ovs_cleanup(config_manager, ovs_old, ovs_current, exit_on_error=True): # pragma: nocover (autopkgtest)
1560+ try:
1561+ apply_ovs_cleanup(config_manager, ovs_old, ovs_current)
1562+ except (OSError, RuntimeError) as e:
1563+ logging.error(str(e))
1564+ if exit_on_error:
1565+ sys.exit(1)
1566diff --git a/netplan/cli/commands/try_command.py b/netplan/cli/commands/try_command.py
1567index 0f70566..756fa49 100644
1568--- a/netplan/cli/commands/try_command.py
1569+++ b/netplan/cli/commands/try_command.py
1570@@ -80,7 +80,7 @@ class NetplanTry(utils.NetplanCommand):
1571 self.backup()
1572 self.setup()
1573
1574- NetplanApply.command_apply(run_generate=True, sync=True, exit_on_error=False)
1575+ NetplanApply().command_apply(run_generate=True, sync=True, exit_on_error=False)
1576
1577 self.t.get_confirmation_input(timeout=self.timeout)
1578 except netplan.terminal.InputRejected:
1579@@ -114,7 +114,7 @@ class NetplanTry(utils.NetplanCommand):
1580
1581 def revert(self): # pragma: nocover (requires user input)
1582 self.config_manager.revert()
1583- NetplanApply.command_apply(run_generate=False, sync=True, exit_on_error=False)
1584+ NetplanApply().command_apply(run_generate=False, sync=True, exit_on_error=False)
1585 for ifname in self.new_interfaces:
1586 if ifname not in self.config_manager.bonds and \
1587 ifname not in self.config_manager.bridges and \
1588diff --git a/netplan/cli/ovs.py b/netplan/cli/ovs.py
1589new file mode 100644
1590index 0000000..a7ae6c8
1591--- /dev/null
1592+++ b/netplan/cli/ovs.py
1593@@ -0,0 +1,168 @@
1594+#!/usr/bin/python3
1595+#
1596+# Copyright (C) 2020 Canonical, Ltd.
1597+# Author: Lukas 'slyon' Märdian <lukas.maerdian@canonical.com>
1598+#
1599+# This program is free software; you can redistribute it and/or modify
1600+# it under the terms of the GNU General Public License as published by
1601+# the Free Software Foundation; version 3.
1602+#
1603+# This program is distributed in the hope that it will be useful,
1604+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1605+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1606+# GNU General Public License for more details.
1607+#
1608+# You should have received a copy of the GNU General Public License
1609+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1610+
1611+import logging
1612+import os
1613+import subprocess
1614+
1615+OPENVSWITCH_OVS_VSCTL = '/usr/bin/ovs-vsctl'
1616+# Defaults for non-optional settings, as defined here:
1617+# http://www.openvswitch.org/ovs-vswitchd.conf.db.5.pdf
1618+DEFAULTS = {
1619+ # Mandatory columns:
1620+ 'mcast_snooping_enable': 'false',
1621+ 'rstp_enable': 'false',
1622+}
1623+GLOBALS = {
1624+ # Global commands:
1625+ 'set-ssl': ('del-ssl', 'get-ssl'),
1626+ 'set-fail-mode': ('del-fail-mode', 'get-fail-mode'),
1627+ 'set-controller': ('del-controller', 'get-controller'),
1628+}
1629+
1630+
1631+def _del_col(type, iface, column, value):
1632+ """Cleanup values from a column (i.e. "column=value")"""
1633+ default = DEFAULTS.get(column)
1634+ if default is None:
1635+ # removes the exact value only if it was set by netplan
1636+ subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, value])
1637+ elif default and default != value:
1638+ # reset to default, if its not the default already
1639+ subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'set', type, iface, '%s=%s' % (column, default)])
1640+
1641+
1642+def _del_dict(type, iface, column, key, value):
1643+ """Cleanup values from a dictionary (i.e. "column:key=value")"""
1644+ # removes the exact value only if it was set by netplan
1645+ subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, key, value])
1646+
1647+
1648+def _del_global(type, iface, key, value):
1649+ """Cleanup commands from the global namespace"""
1650+ del_cmd, get_cmd = GLOBALS.get(key, (None, None))
1651+ if del_cmd == 'del-ssl':
1652+ iface = None
1653+
1654+ if del_cmd:
1655+ args_get = [OPENVSWITCH_OVS_VSCTL, get_cmd]
1656+ args_del = [OPENVSWITCH_OVS_VSCTL, del_cmd]
1657+ if iface:
1658+ args_get.append(iface)
1659+ args_del.append(iface)
1660+ # Check the current value of a global command and compare it to the tag-value, e.g.:
1661+ # * get-ssl: netplan/global/set-ssl=/private/key.pem,/another/cert.pem,/some/ca-cert.pem
1662+ # Private key: /private/key.pem
1663+ # Certificate: /another/cert.pem
1664+ # CA Certificate: /some/ca-cert.pem
1665+ # Bootstrap: false
1666+ # * get-fail-mode: netplan/global/set-fail-mode=secure
1667+ # secure
1668+ # * get-controller: netplan/global/set-controller=tcp:127.0.0.1:1337,unix:/some/socket
1669+ # tcp:127.0.0.1:1337
1670+ # unix:/some/socket
1671+ out = subprocess.check_output(args_get, universal_newlines=True)
1672+ # Clean it only if the exact same value(s) were set by netplan.
1673+ # Don't touch it if other values were set by another integration.
1674+ if all(item in out for item in value.split(',')):
1675+ subprocess.check_call(args_del)
1676+ else:
1677+ raise Exception('Reset command unkown for:', key)
1678+
1679+
1680+def clear_setting(type, iface, setting, value):
1681+ """Check if this setting is in a dict or a colum and delete accordingly"""
1682+ split = setting.split('/', 2)
1683+ col = split[1]
1684+ if col == 'global' and len(split) > 2:
1685+ _del_global(type, iface, split[2], value)
1686+ elif len(split) > 2:
1687+ _del_dict(type, iface, split[1], split[2], value)
1688+ else:
1689+ _del_col(type, iface, split[1], value)
1690+ # Cleanup the tag itself (i.e. "netplan/column[/key]")
1691+ subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, 'external-ids', setting])
1692+
1693+
1694+def is_ovs_interface(iface, interfaces):
1695+ if interfaces[iface].get('openvswitch') is not None:
1696+ return True
1697+ else:
1698+ return any(is_ovs_interface(i, interfaces) for i in interfaces[iface].get('interfaces', []))
1699+
1700+
1701+def apply_ovs_cleanup(config_manager, ovs_old, ovs_current): # pragma: nocover (covered in autopkgtest)
1702+ """
1703+ Query OpenVSwitch state through 'ovs-vsctl' and filter for netplan=true
1704+ tagged ports/bonds and bridges. Delete interfaces which are not defined
1705+ in the current configuration.
1706+ Also filter for individual settings tagged netplan/<column>[/<key]=value
1707+ in external-ids and clear them if they have been set by netplan.
1708+ """
1709+ config_manager.parse()
1710+ ovs_ifaces = set()
1711+ for i in config_manager.interfaces.keys():
1712+ if (is_ovs_interface(i, config_manager.interfaces)):
1713+ ovs_ifaces.add(i)
1714+
1715+ # Tear down old OVS interfaces, not defined in the current config.
1716+ # Use 'del-br' on the Interface table, to delete any netplan created VLAN fake bridges.
1717+ # Use 'del-bond-iface' on the Interface table, to delete netplan created patch port interfaces
1718+ if os.path.isfile(OPENVSWITCH_OVS_VSCTL):
1719+ # Step 1: Delete all interfaces, which are not part of the current OVS config
1720+ for t in (('Port', 'del-port'), ('Bridge', 'del-br'), ('Interface', 'del-br')):
1721+ out = subprocess.check_output([OPENVSWITCH_OVS_VSCTL, '--columns=name,external-ids',
1722+ '-f', 'csv', '-d', 'bare', '--no-headings', 'list', t[0]],
1723+ universal_newlines=True)
1724+ for line in out.splitlines():
1725+ if 'netplan=true' in line:
1726+ iface = line.split(',')[0]
1727+ # Skip cleanup if this OVS interface is part of the current netplan OVS config
1728+ if iface in ovs_ifaces:
1729+ continue
1730+ if t[0] == 'Interface' and subprocess.run([OPENVSWITCH_OVS_VSCTL, 'iface-to-br', iface]).returncode > 0:
1731+ subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', 'del-bond-iface', iface])
1732+ else:
1733+ subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', t[1], iface])
1734+
1735+ # Step 2: Clean up the settings of the remaining interfaces
1736+ for t in ('Port', 'Bridge', 'Interface', 'Open_vSwitch', 'Controller'):
1737+ cols = 'name,external-ids'
1738+ if t == 'Open_vSwitch':
1739+ cols = 'external-ids'
1740+ elif t == 'Controller':
1741+ cols = '_uuid,external-ids' # handle _uuid as if it would be the iface 'name'
1742+ out = subprocess.check_output([OPENVSWITCH_OVS_VSCTL, '--columns=%s' % cols,
1743+ '-f', 'csv', '-d', 'bare', '--no-headings', 'list', t],
1744+ universal_newlines=True)
1745+ for line in out.splitlines():
1746+ if 'netplan/' in line:
1747+ iface = '.'
1748+ extids = line
1749+ if t != 'Open_vSwitch':
1750+ iface, extids = line.split(',', 1)
1751+ # Check each line (interface) if it contains any netplan tagged settings, e.g.:
1752+ # ovs0,"iface-id=myhostname netplan=true netplan/external-ids/iface-id=myhostname"
1753+ # ovs1,"netplan=true netplan/global/set-fail-mode=standalone netplan/mcast_snooping_enable=false"
1754+ for entry in extids.strip('"').split(' '):
1755+ if entry.startswith('netplan/') and '=' in entry:
1756+ setting, val = entry.split('=', 1)
1757+ clear_setting(t, iface, setting, val)
1758+
1759+ # Show the warning only if we are or have been working with OVS definitions
1760+ elif ovs_old or ovs_current:
1761+ logging.warning('ovs-vsctl is missing, cannot tear down old OpenVSwitch interfaces')
1762diff --git a/netplan/cli/sriov.py b/netplan/cli/sriov.py
1763index 8feacf1..43e0259 100644
1764--- a/netplan/cli/sriov.py
1765+++ b/netplan/cli/sriov.py
1766@@ -30,25 +30,36 @@ import netifaces
1767 def _get_target_interface(interfaces, config_manager, pf_link, pfs):
1768 if pf_link not in pfs:
1769 # handle the match: syntax, get the actual device name
1770- pf_match = config_manager.ethernets[pf_link].get('match')
1771+ pf_dev = config_manager.ethernets[pf_link]
1772+ pf_match = pf_dev.get('match')
1773 if pf_match:
1774- by_name = pf_match.get('name')
1775- by_mac = pf_match.get('macaddress')
1776- by_driver = pf_match.get('driver')
1777-
1778- for interface in interfaces:
1779- if ((by_name and not utils.is_interface_matching_name(interface, by_name)) or
1780- (by_mac and not utils.is_interface_matching_macaddress(interface, by_mac)) or
1781- (by_driver and not utils.is_interface_matching_driver_name(interface, by_driver))):
1782- continue
1783- # we have a matching PF
1784- # store the matching interface in the dictionary of
1785- # active PFs, but error out if we matched more than one
1786- if pf_link in pfs:
1787- raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link)
1788- pfs[pf_link] = interface
1789+ # now here it's a bit tricky
1790+ set_name = pf_dev.get('set-name')
1791+ if set_name and set_name in interfaces:
1792+ # if we had a match: stanza and set-name: this means we should
1793+ # assume that, if found, the interface has already been
1794+ # renamed - use the new name
1795+ pfs[pf_link] = set_name
1796+ else:
1797+ # no set-name (or interfaces not yet renamed) so we need to do
1798+ # the matching ourselves
1799+ by_name = pf_match.get('name')
1800+ by_mac = pf_match.get('macaddress')
1801+ by_driver = pf_match.get('driver')
1802+
1803+ for interface in interfaces:
1804+ if ((by_name and not utils.is_interface_matching_name(interface, by_name)) or
1805+ (by_mac and not utils.is_interface_matching_macaddress(interface, by_mac)) or
1806+ (by_driver and not utils.is_interface_matching_driver_name(interface, by_driver))):
1807+ continue
1808+ # we have a matching PF
1809+ # store the matching interface in the dictionary of
1810+ # active PFs, but error out if we matched more than one
1811+ if pf_link in pfs:
1812+ raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link)
1813+ pfs[pf_link] = interface
1814 else:
1815- # no match field, assume entry name is interface name
1816+ # no match field, assume entry name is the interface name
1817 if pf_link in interfaces:
1818 pfs[pf_link] = pf_link
1819
1820@@ -219,11 +230,13 @@ def apply_vlan_filter_for_vf(pf, vf, vlan_name, vlan_id, prefix='/'):
1821 'failed setting SR-IOV VLAN filter for vlan %s (ip link set command failed)' % vlan_name)
1822
1823
1824-def apply_sriov_config(interfaces, config_manager):
1825+def apply_sriov_config(config_manager):
1826 """
1827 Go through all interfaces, identify which ones are SR-IOV VFs, create
1828 them and perform all other necessary setup.
1829 """
1830+ config_manager.parse()
1831+ interfaces = netifaces.interfaces()
1832
1833 # for sr-iov devices, we identify VFs by them having a link: field
1834 # pointing to an PF. So let's browse through all ethernet devices,
1835@@ -316,5 +329,6 @@ def apply_sriov_config(interfaces, config_manager):
1836 raise ConfigurationError(
1837 'interface %s for netplan device %s (%s) already has an SR-IOV vlan defined' % (vf, link, vlan))
1838
1839+ # TODO: make sure that we don't apply the filter twice
1840 apply_vlan_filter_for_vf(pf, vf, vlan, vlan_id)
1841 filtered_vlans_set.add(vf)
1842diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py
1843index 5f54b1a..f076c25 100644
1844--- a/netplan/cli/utils.py
1845+++ b/netplan/cli/utils.py
1846@@ -23,6 +23,7 @@ import fnmatch
1847 import argparse
1848 import subprocess
1849 import netifaces
1850+import re
1851
1852 NM_SERVICE_NAME = 'NetworkManager.service'
1853 NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service'
1854@@ -55,6 +56,20 @@ def nm_running(): # pragma: nocover (covered in autopkgtest)
1855 return False
1856
1857
1858+def nm_interfaces(paths, devices):
1859+ pat = re.compile('^interface-name=(.*)$')
1860+ interfaces = set()
1861+ for path in paths:
1862+ with open(path, 'r') as f:
1863+ for line in f:
1864+ m = pat.match(line)
1865+ if m:
1866+ # Expand/match globbing of interface names, to real devices
1867+ interfaces.update(set(fnmatch.filter(devices, m.group(1))))
1868+ break # skip to next file
1869+ return interfaces
1870+
1871+
1872 def systemctl_network_manager(action, sync=False): # pragma: nocover (covered in autopkgtest)
1873 service_name = NM_SERVICE_NAME
1874
1875@@ -86,6 +101,23 @@ def systemctl_networkd(action, sync=False, extra_services=[]): # pragma: nocove
1876 subprocess.check_call(command)
1877
1878
1879+def systemctl_is_active(unit_pattern): # pragma: nocover (covered in autopkgtest)
1880+ '''Return True if at least one matching unit is running'''
1881+ if subprocess.call(['systemctl', '--quiet', 'is-active', unit_pattern]) == 0:
1882+ return True
1883+ return False
1884+
1885+
1886+def systemctl_daemon_reload(): # pragma: nocover (covered in autopkgtest)
1887+ '''Reload systemd unit files from disk and re-calculate its dependencies'''
1888+ subprocess.check_call(['systemctl', 'daemon-reload'])
1889+
1890+
1891+def ip_addr_flush(iface): # pragma: nocover (covered in autopkgtest)
1892+ '''Flush all IP addresses of a given interface via iproute2'''
1893+ subprocess.check_call(['ip', 'addr', 'flush', iface], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1894+
1895+
1896 def get_interface_driver_name(interface, only_down=False): # pragma: nocover (covered in autopkgtest)
1897 devdir = os.path.join('/sys/class/net', interface)
1898 if only_down:
1899diff --git a/netplan/configmanager.py b/netplan/configmanager.py
1900index 0f9e28b..ce050a2 100644
1901--- a/netplan/configmanager.py
1902+++ b/netplan/configmanager.py
1903@@ -44,6 +44,7 @@ class ConfigManager(object):
1904 @property
1905 def interfaces(self):
1906 interfaces = {}
1907+ interfaces.update(self.ovs_ports)
1908 interfaces.update(self.ethernets)
1909 interfaces.update(self.wifis)
1910 interfaces.update(self.bridges)
1911@@ -59,6 +60,10 @@ class ConfigManager(object):
1912 return interfaces
1913
1914 @property
1915+ def ovs_ports(self):
1916+ return self.network['ovs_ports']
1917+
1918+ @property
1919 def ethernets(self):
1920 return self.network['ethernets']
1921
1922@@ -101,6 +106,7 @@ class ConfigManager(object):
1923 files = [names_to_paths[name] for name in sorted(names_to_paths.keys())]
1924
1925 self.config['network'] = {
1926+ 'ovs_ports': {},
1927 'ethernets': {},
1928 'wifis': {},
1929 'bridges': {},
1930@@ -172,6 +178,29 @@ class ConfigManager(object):
1931 else:
1932 raise
1933
1934+ def _merge_ovs_ports_config(self, orig, new):
1935+ new_interfaces = set()
1936+ ports = dict()
1937+ if 'ports' in new:
1938+ for p1, p2 in new.get('ports'):
1939+ # Spoof an interface config for patch ports, which are usually
1940+ # just strings. Add 'peer' and mark it via 'openvswitch' key.
1941+ ports[p1] = {'peer': p2, 'openvswitch': {}}
1942+ ports[p2] = {'peer': p1, 'openvswitch': {}}
1943+ changed_ifaces = list(ports.keys())
1944+
1945+ for ifname in changed_ifaces:
1946+ iface = ports.pop(ifname)
1947+ if ifname in orig:
1948+ logging.debug("{} exists in {}".format(ifname, orig))
1949+ orig[ifname].update(iface)
1950+ else:
1951+ logging.debug("{} not found in {}".format(ifname, orig))
1952+ orig[ifname] = iface
1953+ new_interfaces.add(ifname)
1954+
1955+ return new_interfaces
1956+
1957 def _merge_interface_config(self, orig, new):
1958 new_interfaces = set()
1959 changed_ifaces = list(new.keys())
1960@@ -198,6 +227,9 @@ class ConfigManager(object):
1961 if yaml_data is not None:
1962 network = yaml_data.get('network')
1963 if network:
1964+ if 'openvswitch' in network:
1965+ new = self._merge_ovs_ports_config(self.ovs_ports, network.get('openvswitch'))
1966+ new_interfaces |= new
1967 if 'ethernets' in network:
1968 new = self._merge_interface_config(self.ethernets, network.get('ethernets'))
1969 new_interfaces |= new
1970diff --git a/src/generate.c b/src/generate.c
1971index c020d8f..e88dd7f 100644
1972--- a/src/generate.c
1973+++ b/src/generate.c
1974@@ -30,10 +30,13 @@
1975 #include "parse.h"
1976 #include "networkd.h"
1977 #include "nm.h"
1978+#include "openvswitch.h"
1979+#include "sriov.h"
1980
1981 static gchar* rootdir;
1982 static gchar** files;
1983 static gboolean any_networkd;
1984+static gboolean any_sriov;
1985 static gchar* mapping_iface;
1986
1987 static GOptionEntry options[] = {
1988@@ -53,9 +56,14 @@ reload_udevd(void)
1989 static void
1990 nd_iterator_list(gpointer value, gpointer user_data)
1991 {
1992- if (write_networkd_conf((NetplanNetDefinition*) value, (const char*) user_data))
1993+ NetplanNetDefinition* def = (NetplanNetDefinition*) value;
1994+ if (write_networkd_conf(def, (const char*) user_data))
1995 any_networkd = TRUE;
1996- write_nm_conf((NetplanNetDefinition*) value, (const char*) user_data);
1997+
1998+ write_ovs_conf(def, (const char*) user_data);
1999+ write_nm_conf(def, (const char*) user_data);
2000+ if (def->sriov_explicit_vf_count < G_MAXUINT || def->sriov_link)
2001+ any_sriov = TRUE;
2002 }
2003
2004
2005@@ -246,6 +254,8 @@ int main(int argc, char** argv)
2006 /* Clean up generated config from previous runs */
2007 cleanup_networkd_conf(rootdir);
2008 cleanup_nm_conf(rootdir);
2009+ cleanup_ovs_conf(rootdir);
2010+ cleanup_sriov_conf(rootdir);
2011
2012 if (mapping_iface && netdefs) {
2013 return find_interface(mapping_iface);
2014@@ -256,6 +266,8 @@ int main(int argc, char** argv)
2015 g_debug("Generating output files..");
2016 g_list_foreach (netdefs_ordered, nd_iterator_list, rootdir);
2017 write_nm_conf_finish(rootdir);
2018+ write_ovs_conf_finish(rootdir);
2019+ if (any_sriov) write_sriov_conf_finish(rootdir);
2020 /* We may have written .rules & .link files, thus we must
2021 * invalidate udevd cache of its config as by default it only
2022 * invalidates cache at most every 3 seconds. Not sure if this
2023diff --git a/src/networkd.c b/src/networkd.c
2024index e2bb111..a1b31d8 100644
2025--- a/src/networkd.c
2026+++ b/src/networkd.c
2027@@ -28,6 +28,7 @@
2028 #include "networkd.h"
2029 #include "parse.h"
2030 #include "util.h"
2031+#include "validation.h"
2032
2033 /**
2034 * Append WiFi frequencies to wpa_supplicant's freq_list=
2035@@ -152,6 +153,69 @@ write_tunnel_params(GString* s, const NetplanNetDefinition* def)
2036 }
2037
2038 static void
2039+write_wireguard_params(GString* s, const NetplanNetDefinition* def)
2040+{
2041+ GString *params = NULL;
2042+ params = g_string_sized_new(200);
2043+
2044+ g_assert(def->tunnel.private_key);
2045+ /* The "PrivateKeyFile=" setting is available as of systemd-netwokrd v242+
2046+ * Base64 encoded PrivateKey= or absolute PrivateKeyFile= fields are mandatory.
2047+ *
2048+ * The key was already validated via validate_tunnel_grammar(), but we need
2049+ * to differentiate between base64 key VS absolute path key-file. And a base64
2050+ * string could (theoretically) start with '/', so we use is_wireguard_key()
2051+ * as well to check for more specific characteristics (if needed). */
2052+ if (def->tunnel.private_key[0] == '/' && !is_wireguard_key(def->tunnel.private_key))
2053+ g_string_append_printf(params, "PrivateKeyFile=%s\n", def->tunnel.private_key);
2054+ else
2055+ g_string_append_printf(params, "PrivateKey=%s\n", def->tunnel.private_key);
2056+
2057+ if (def->tunnel.port)
2058+ g_string_append_printf(params, "ListenPort=%u\n", def->tunnel.port);
2059+ /* This is called FirewallMark= as of systemd v243, but we keep calling it FwMark= for
2060+ backwards compatibility. FwMark= is still supported, but deprecated:
2061+ https://github.com/systemd/systemd/pull/12478 */
2062+ if (def->tunnel.fwmark)
2063+ g_string_append_printf(params, "FwMark=%u\n", def->tunnel.fwmark);
2064+
2065+ g_string_append_printf(s, "\n[WireGuard]\n%s", params->str);
2066+ g_string_free(params, TRUE);
2067+
2068+ for (guint i = 0; i < def->wireguard_peers->len; i++) {
2069+ NetplanWireguardPeer *peer = g_array_index (def->wireguard_peers, NetplanWireguardPeer*, i);
2070+ GString *peer_s = g_string_sized_new(200);
2071+
2072+ g_string_append_printf(peer_s, "PublicKey=%s\n", peer->public_key);
2073+ g_string_append(peer_s, "AllowedIPs=");
2074+ for (guint i = 0; i < peer->allowed_ips->len; ++i) {
2075+ if (i > 0 )
2076+ g_string_append_c(peer_s, ',');
2077+ g_string_append_printf(peer_s, "%s", g_array_index(peer->allowed_ips, char*, i));
2078+ }
2079+ g_string_append_c(peer_s, '\n');
2080+
2081+ if (peer->keepalive)
2082+ g_string_append_printf(peer_s, "PersistentKeepalive=%d\n", peer->keepalive);
2083+ if (peer->endpoint)
2084+ g_string_append_printf(peer_s, "Endpoint=%s\n", peer->endpoint);
2085+ /* The key was already validated via validate_tunnel_grammar(), but we need
2086+ * to differentiate between base64 key VS absolute path key-file. And a base64
2087+ * string could (theoretically) start with '/', so we use is_wireguard_key()
2088+ * as well to check for more specific characteristics (if needed). */
2089+ if (peer->preshared_key) {
2090+ if (peer->preshared_key[0] == '/' && !is_wireguard_key(peer->preshared_key))
2091+ g_string_append_printf(peer_s, "PresharedKeyFile=%s\n", peer->preshared_key);
2092+ else
2093+ g_string_append_printf(peer_s, "PresharedKey=%s\n", peer->preshared_key);
2094+ }
2095+
2096+ g_string_append_printf(s, "\n[WireGuardPeer]\n%s", peer_s->str);
2097+ g_string_free(peer_s, TRUE);
2098+ }
2099+}
2100+
2101+static void
2102 write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
2103 {
2104 GString* s = NULL;
2105@@ -326,9 +390,9 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch
2106 case NETPLAN_TUNNEL_MODE_SIT:
2107 case NETPLAN_TUNNEL_MODE_VTI:
2108 case NETPLAN_TUNNEL_MODE_VTI6:
2109- g_string_append_printf(s,
2110- "Kind=%s\n",
2111- tunnel_mode_to_string(def->tunnel.mode));
2112+ case NETPLAN_TUNNEL_MODE_WIREGUARD:
2113+ g_string_append_printf(s, "Kind=%s\n",
2114+ tunnel_mode_to_string(def->tunnel.mode));
2115 break;
2116
2117 case NETPLAN_TUNNEL_MODE_IP6IP6:
2118@@ -341,14 +405,13 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch
2119 g_assert_not_reached();
2120 // LCOV_EXCL_STOP
2121 }
2122-
2123- write_tunnel_params(s, def);
2124+ if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD)
2125+ write_wireguard_params(s, def);
2126+ else
2127+ write_tunnel_params(s, def);
2128 break;
2129
2130- // LCOV_EXCL_START
2131- default:
2132- g_assert_not_reached();
2133- // LCOV_EXCL_STOP
2134+ default: g_assert_not_reached(); // LCOV_EXCL_LINE
2135 }
2136
2137 /* these do not contain secrets and need to be readable by
2138@@ -402,6 +465,19 @@ write_ip_rule(NetplanIPRule* r, GString* s)
2139 g_string_append_printf(s, "TypeOfService=%d\n", r->tos);
2140 }
2141
2142+static void
2143+write_addr_option(NetplanAddressOptions* o, GString* s)
2144+{
2145+ g_string_append_printf(s, "\n[Address]\n");
2146+ g_assert(o->address);
2147+ g_string_append_printf(s, "Address=%s\n", o->address);
2148+
2149+ if (o->lifetime)
2150+ g_string_append_printf(s, "PreferredLifetime=%s\n", o->lifetime);
2151+ if (o->label)
2152+ g_string_append_printf(s, "Label=%s\n", o->label);
2153+}
2154+
2155 #define DHCP_OVERRIDES_ERROR \
2156 "ERROR: %s: networkd requires that %s has the same value in both " \
2157 "dhcp4_overrides and dhcp6_overrides\n"
2158@@ -459,7 +535,10 @@ combine_dhcp_overrides(const NetplanNetDefinition* def, NetplanDHCPOverrides* co
2159 }
2160 }
2161
2162-static void
2163+/**
2164+ * Write the needed networkd .network configuration for the selected netplan definition.
2165+ */
2166+void
2167 write_network_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
2168 {
2169 GString* network = NULL;
2170@@ -524,13 +603,13 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c
2171 if (def->ip6_addresses)
2172 for (unsigned i = 0; i < def->ip6_addresses->len; ++i)
2173 g_string_append_printf(network, "Address=%s\n", g_array_index(def->ip6_addresses, char*, i));
2174- if (def->ip6_addr_gen_mode) {
2175- /* TODO: Figure out how we can configure ipv6-address-generation for networkd.
2176- * IPv6Token= seems to be the corresponding option, but it doesn't do
2177- * exactly what we need and has quite some restrictions, c.f.:
2178- * https://github.com/systemd/systemd/issues/4625
2179- * https://github.com/systemd/systemd/pull/14415 */
2180- g_fprintf(stderr, "ERROR: %s: ipv6-address-generation is not supported by networkd\n", def->id);
2181+ if (def->ip6_addr_gen_token) {
2182+ g_string_append_printf(network, "IPv6Token=static:%s\n", def->ip6_addr_gen_token);
2183+ } else if (def->ip6_addr_gen_mode > NETPLAN_ADDRGEN_EUI64) {
2184+ /* EUI-64 mode is enabled by default, if no IPv6Token= is specified */
2185+ /* TODO: Enable stable-privacy mode for networkd, once PR#16618 has been released:
2186+ * https://github.com/systemd/systemd/pull/16618 */
2187+ g_fprintf(stderr, "ERROR: %s: ipv6-address-generation mode is not supported by networkd\n", def->id);
2188 exit(1);
2189 }
2190 if (def->accept_ra == NETPLAN_RA_MODE_ENABLED)
2191@@ -563,7 +642,7 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c
2192 if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL)
2193 g_string_append(network, "ConfigureWithoutCarrier=yes\n");
2194
2195- if (def->bridge) {
2196+ if (def->bridge && def->backend != NETPLAN_BACKEND_OVS) {
2197 g_string_append_printf(network, "Bridge=%s\n", def->bridge);
2198
2199 if (def->bridge_params.path_cost || def->bridge_params.port_priority)
2200@@ -573,14 +652,14 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c
2201 if (def->bridge_params.port_priority)
2202 g_string_append_printf(network, "Priority=%u\n", def->bridge_params.port_priority);
2203 }
2204- if (def->bond) {
2205+ if (def->bond && def->backend != NETPLAN_BACKEND_OVS) {
2206 g_string_append_printf(network, "Bond=%s\n", def->bond);
2207
2208 if (def->bond_params.primary_slave)
2209 g_string_append_printf(network, "PrimarySlave=true\n");
2210 }
2211
2212- if (def->has_vlans) {
2213+ if (def->has_vlans && def->backend != NETPLAN_BACKEND_OVS) {
2214 /* iterate over all netdefs to find VLANs attached to us */
2215 GList *l = netdefs_ordered;
2216 const NetplanNetDefinition* nd;
2217@@ -604,6 +683,13 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c
2218 }
2219 }
2220
2221+ if (def->address_options) {
2222+ for (unsigned i = 0; i < def->address_options->len; ++i) {
2223+ NetplanAddressOptions* opts = g_array_index(def->address_options, NetplanAddressOptions*, i);
2224+ write_addr_option(opts, network);
2225+ }
2226+ }
2227+
2228 if (def->dhcp4 || def->dhcp6 || def->critical) {
2229 /* NetworkManager compatible route metrics */
2230 g_string_append(network, "\n[DHCP]\n");
2231@@ -812,19 +898,8 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
2232 {
2233 g_autoptr(GError) err = NULL;
2234 g_autofree gchar *stdouth = NULL;
2235- g_autofree gchar *stderrh = NULL;
2236- gint exit_status = 0;
2237
2238- gchar *argv[] = {"bin" "/" "systemd-escape", def->id, NULL};
2239- g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdouth, &stderrh, &exit_status, &err);
2240- g_spawn_check_exit_status(exit_status, &err);
2241- if (err != NULL) {
2242- // LCOV_EXCL_START
2243- g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", def->id, exit_status, stdouth, stderrh);
2244- exit(1);
2245- // LCOV_EXCL_STOP
2246- }
2247- g_strstrip(stdouth);
2248+ stdouth = systemd_escape(def->id);
2249
2250 GString* s = g_string_new("[Unit]\n");
2251 g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", stdouth, ".service", NULL);
2252@@ -863,6 +938,9 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir)
2253 if (ap->bssid) {
2254 g_string_append_printf(s, " bssid=%s\n", ap->bssid);
2255 }
2256+ if (ap->hidden) {
2257+ g_string_append(s, " scan_ssid=1\n");
2258+ }
2259 if (ap->band == NETPLAN_WIFI_BAND_24) {
2260 // initialize 2.4GHz frequency hashtable
2261 if(!wifi_frequency_24)
2262@@ -990,8 +1068,12 @@ cleanup_networkd_conf(const char* rootdir)
2263 {
2264 unlink_glob(rootdir, "/run/systemd/network/10-netplan-*");
2265 unlink_glob(rootdir, "/run/netplan/wpa-*.conf");
2266+ unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-*.service");
2267 unlink_glob(rootdir, "/run/systemd/system/netplan-wpa-*.service");
2268 unlink_glob(rootdir, "/run/udev/rules.d/99-netplan-*");
2269+ /* Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an
2270+ * upgraded system, we need to make sure to clean those up. */
2271+ unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa@*.service");
2272 }
2273
2274 /**
2275diff --git a/src/networkd.h b/src/networkd.h
2276index 5629a04..41ab125 100644
2277--- a/src/networkd.h
2278+++ b/src/networkd.h
2279@@ -22,3 +22,5 @@
2280 gboolean write_networkd_conf(const NetplanNetDefinition* def, const char* rootdir);
2281 void cleanup_networkd_conf(const char* rootdir);
2282 void enable_networkd(const char* generator_dir);
2283+
2284+void write_network_file(const NetplanNetDefinition* def, const char* rootdir, const char* path);
2285diff --git a/src/nm.c b/src/nm.c
2286index 2cd1d52..9a377d0 100644
2287--- a/src/nm.c
2288+++ b/src/nm.c
2289@@ -28,6 +28,7 @@
2290 #include "nm.h"
2291 #include "parse.h"
2292 #include "util.h"
2293+#include "validation.h"
2294
2295 GString* udev_rules;
2296
2297@@ -111,6 +112,8 @@ type_str(const NetplanNetDefinition* def)
2298 case NETPLAN_DEF_TYPE_VLAN:
2299 return "vlan";
2300 case NETPLAN_DEF_TYPE_TUNNEL:
2301+ if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD)
2302+ return "wireguard";
2303 return "ip-tunnel";
2304 // LCOV_EXCL_START
2305 default:
2306@@ -201,23 +204,14 @@ write_routes(const NetplanNetDefinition* def, GString *s, int family)
2307 exit(1);
2308 }
2309
2310- if (cur_route->scope && g_ascii_strcasecmp(cur_route->scope, "global") != 0) {
2311- g_fprintf(stderr, "ERROR: %s: NetworkManager only supports global scoped routes\n", def->id);
2312- exit(1);
2313- }
2314-
2315- if (cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC) {
2316- g_fprintf(stderr, "ERROR: %s: NetworkManager does not support non-default routing tables\n", def->id);
2317- exit(1);
2318- }
2319-
2320- if (cur_route->from) {
2321- g_fprintf(stderr, "ERROR: %s: NetworkManager does not support routes with 'from'\n", def->id);
2322- exit(1);
2323- }
2324-
2325- if (cur_route->onlink) {
2326- g_fprintf(stderr, "ERROR: %s: NetworkManager does not support on-link routes\n", def->id);
2327+ if (!g_strcmp0(cur_route->scope, "global")) {
2328+ /* For IPv6 addresses, kernel and NetworkManager don't support a scope.
2329+ * For IPv4 addresses, NetworkManager determines the scope of addresses on its own
2330+ * ("link" for addresses without gateway, "global" for addresses with next-hop). */
2331+ g_debug("%s: NetworkManager does not support setting a scope for routes, it will auto-detect them.", def->id);
2332+ } else if (cur_route->scope) {
2333+ /* Error out if scope is not set to its default value of 'global' */
2334+ g_fprintf(stderr, "ERROR: %s: NetworkManager does not support setting a scope for routes\n", def->id);
2335 exit(1);
2336 }
2337
2338@@ -226,6 +220,21 @@ write_routes(const NetplanNetDefinition* def, GString *s, int family)
2339 if (cur_route->metric != NETPLAN_METRIC_UNSPEC)
2340 g_string_append_printf(s, ",%d", cur_route->metric);
2341 g_string_append(s, "\n");
2342+
2343+ if ( cur_route->onlink
2344+ || cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC
2345+ || cur_route->from) {
2346+ g_string_append_printf(s, "route%d_options=", j);
2347+ if (cur_route->onlink) {
2348+ /* onlink for IPv6 addresses is only supported since nm-1.18.0. */
2349+ g_string_append_printf(s, "onlink=true,");
2350+ }
2351+ if (cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC)
2352+ g_string_append_printf(s, "table=%u,", cur_route->table);
2353+ if (cur_route->from)
2354+ g_string_append_printf(s, "src=%s,", cur_route->from);
2355+ s->str[s->len - 1] = '\n';
2356+ }
2357 j++;
2358 }
2359 }
2360@@ -322,6 +331,60 @@ write_bridge_params(const NetplanNetDefinition* def, GString *s)
2361 }
2362
2363 static void
2364+write_wireguard_params(const NetplanNetDefinition* def, GString *s)
2365+{
2366+ g_assert(def->tunnel.private_key);
2367+ g_string_append(s, "\n[wireguard]\n");
2368+
2369+ /* The key was already validated via validate_tunnel_grammar(), but we need
2370+ * to differentiate between base64 key VS absolute path key-file. And a base64
2371+ * string could (theoretically) start with '/', so we use is_wireguard_key()
2372+ * as well to check for more specific characteristics (if needed). */
2373+ if (def->tunnel.private_key[0] == '/' && !is_wireguard_key(def->tunnel.private_key)) {
2374+ g_fprintf(stderr, "%s: private key needs to be base64 encoded when using the NM backend\n", def->id);
2375+ exit(1);
2376+ } else
2377+ g_string_append_printf(s, "private-key=%s\n", def->tunnel.private_key);
2378+
2379+ if (def->tunnel.port)
2380+ g_string_append_printf(s, "listen-port=%u\n", def->tunnel.port);
2381+ if (def->tunnel.fwmark)
2382+ g_string_append_printf(s, "fwmark=%u\n", def->tunnel.fwmark);
2383+
2384+ for (guint i = 0; i < def->wireguard_peers->len; i++) {
2385+ NetplanWireguardPeer *peer = g_array_index (def->wireguard_peers, NetplanWireguardPeer*, i);
2386+ g_assert(peer->public_key);
2387+ g_string_append_printf(s, "\n[wireguard-peer.%s]\n", peer->public_key);
2388+
2389+ if (peer->keepalive)
2390+ g_string_append_printf(s, "persistent-keepalive=%d\n", peer->keepalive);
2391+ if (peer->endpoint)
2392+ g_string_append_printf(s, "endpoint=%s\n", peer->endpoint);
2393+ /* The key was already validated via validate_tunnel_grammar(), but we need
2394+ * to differentiate between base64 key VS absolute path key-file. And a base64
2395+ * string could (theoretically) start with '/', so we use is_wireguard_key()
2396+ * as well to check for more specific characteristics (if needed). */
2397+ if (peer->preshared_key) {
2398+ if (peer->preshared_key[0] == '/' && !is_wireguard_key(peer->preshared_key)) {
2399+ g_fprintf(stderr, "%s: shared key needs to be base64 encoded when using the NM backend\n", def->id);
2400+ exit(1);
2401+ } else {
2402+ g_string_append_printf(s, "preshared-key=%s\n", peer->preshared_key);
2403+ g_string_append(s, "preshared-key-flags=0\n");
2404+ }
2405+ }
2406+ if (peer->allowed_ips && peer->allowed_ips->len > 0) {
2407+ g_string_append(s, "allowed-ips=");
2408+ for (guint i = 0; i < peer->allowed_ips->len; ++i) {
2409+ if (i > 0 ) g_string_append_c(s, ';');
2410+ g_string_append_printf(s, "%s", g_array_index(peer->allowed_ips, char*, i));
2411+ }
2412+ g_string_append_c(s, '\n');
2413+ }
2414+ }
2415+}
2416+
2417+static void
2418 write_tunnel_params(const NetplanNetDefinition* def, GString *s)
2419 {
2420 g_string_append(s, "\n[ip-tunnel]\n");
2421@@ -437,6 +500,7 @@ write_nm_conf_access_point(NetplanNetDefinition* def, const char* rootdir, const
2422 g_autofree char* conf_path = NULL;
2423 mode_t orig_umask;
2424 char uuidstr[37];
2425+ const char *match_interface_name = NULL;
2426
2427 if (def->type == NETPLAN_DEF_TYPE_WIFI)
2428 g_assert(ap);
2429@@ -472,12 +536,10 @@ write_nm_conf_access_point(NetplanNetDefinition* def, const char* rootdir, const
2430 else if (!def->has_match)
2431 g_string_append_printf(s, "interface-name=%s\n", def->id);
2432 else if (def->match.original_name) {
2433- /* NM does not support interface name globbing */
2434- if (strpbrk(def->match.original_name, "*[]?")) {
2435- g_fprintf(stderr, "ERROR: %s: NetworkManager definitions do not support name globbing\n", def->id);
2436- exit(1);
2437- }
2438- g_string_append_printf(s, "interface-name=%s\n", def->match.original_name);
2439+ if (strpbrk(def->match.original_name, "*[]?"))
2440+ match_interface_name = def->match.original_name;
2441+ else
2442+ g_string_append_printf(s, "interface-name=%s\n", def->match.original_name);
2443 }
2444 /* else matches on something other than the name, do not restrict interface-name */
2445 } else {
2446@@ -609,8 +671,17 @@ write_nm_conf_access_point(NetplanNetDefinition* def, const char* rootdir, const
2447 if (def->type == NETPLAN_DEF_TYPE_BOND)
2448 write_bond_parameters(def, s);
2449
2450- if (def->type == NETPLAN_DEF_TYPE_TUNNEL)
2451- write_tunnel_params(def, s);
2452+ if (def->type == NETPLAN_DEF_TYPE_TUNNEL) {
2453+ if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD)
2454+ write_wireguard_params(def, s);
2455+ else
2456+ write_tunnel_params(def, s);
2457+ }
2458+
2459+ if (match_interface_name) {
2460+ g_string_append(s, "\n[match]\n");
2461+ g_string_append_printf(s, "interface-name=%s;\n", match_interface_name);
2462+ }
2463
2464 g_string_append(s, "\n[ipv4]\n");
2465
2466@@ -660,7 +731,11 @@ write_nm_conf_access_point(NetplanNetDefinition* def, const char* rootdir, const
2467 if (def->ip6_addresses)
2468 for (unsigned i = 0; i < def->ip6_addresses->len; ++i)
2469 g_string_append_printf(s, "address%i=%s\n", i+1, g_array_index(def->ip6_addresses, char*, i));
2470- if (def->ip6_addr_gen_mode) {
2471+ if (def->ip6_addr_gen_token) {
2472+ /* Token implies EUI-64, i.e mode=0 */
2473+ g_string_append(s, "addr-gen-mode=0\n");
2474+ g_string_append_printf(s, "token=%s\n", def->ip6_addr_gen_token);
2475+ } else if (def->ip6_addr_gen_mode) {
2476 g_string_append_printf(s, "addr-gen-mode=%s\n", addr_gen_mode_str(def->ip6_addr_gen_mode));
2477 }
2478 if (def->ip6_privacy)
2479@@ -700,6 +775,9 @@ write_nm_conf_access_point(NetplanNetDefinition* def, const char* rootdir, const
2480 if (ap->bssid) {
2481 g_string_append_printf(s, "bssid=%s\n", ap->bssid);
2482 }
2483+ if (ap->hidden) {
2484+ g_string_append(s, "hidden=true\n");
2485+ }
2486 if (ap->band == NETPLAN_WIFI_BAND_5 || ap->band == NETPLAN_WIFI_BAND_24) {
2487 g_string_append_printf(s, "band=%s\n", wifi_band_str(ap->band));
2488 /* Channel is only unambiguous, if band is set. */
2489@@ -747,6 +825,11 @@ write_nm_conf(NetplanNetDefinition* def, const char* rootdir)
2490 exit(1);
2491 }
2492
2493+ if (def->address_options) {
2494+ g_fprintf(stderr, "ERROR: %s: NetworkManager does not support address options\n", def->id);
2495+ exit(1);
2496+ }
2497+
2498 /* for wifi we need to create a separate connection file for every SSID */
2499 if (def->type == NETPLAN_DEF_TYPE_WIFI) {
2500 GHashTableIter iter;
2501diff --git a/src/openvswitch.c b/src/openvswitch.c
2502new file mode 100644
2503index 0000000..79068d1
2504--- /dev/null
2505+++ b/src/openvswitch.c
2506@@ -0,0 +1,484 @@
2507+/*
2508+ * Copyright (C) 2020 Canonical, Ltd.
2509+ * Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
2510+ * Lukas 'slyon' Märdian <lukas.maerdian@canonical.com>
2511+ *
2512+ * This program is free software; you can redistribute it and/or modify
2513+ * it under the terms of the GNU General Public License as published by
2514+ * the Free Software Foundation; version 3.
2515+ *
2516+ * This program is distributed in the hope that it will be useful,
2517+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2518+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2519+ * GNU General Public License for more details.
2520+ *
2521+ * You should have received a copy of the GNU General Public License
2522+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2523+ */
2524+
2525+#include <unistd.h>
2526+#include <errno.h>
2527+
2528+#include <glib.h>
2529+#include <glib/gprintf.h>
2530+
2531+#include "openvswitch.h"
2532+#include "networkd.h"
2533+#include "parse.h"
2534+#include "util.h"
2535+
2536+static void
2537+write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, gboolean physical, gboolean cleanup, const char* dependency)
2538+{
2539+ g_autofree gchar* id_escaped = NULL;
2540+ g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", id, ".service", NULL);
2541+ g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", id, ".service", NULL);
2542+
2543+ GString* s = g_string_new("[Unit]\n");
2544+ g_string_append_printf(s, "Description=OpenVSwitch configuration for %s\n", id);
2545+ g_string_append(s, "DefaultDependencies=no\n");
2546+ /* run any ovs-netplan unit only after openvswitch-switch.service is ready */
2547+ g_string_append_printf(s, "Wants=openvswitch-switch.service\n");
2548+ g_string_append_printf(s, "After=openvswitch-switch.service\n");
2549+ if (physical) {
2550+ id_escaped = systemd_escape((char*) id);
2551+ g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", id_escaped);
2552+ g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", id_escaped);
2553+ }
2554+ if (!cleanup) {
2555+ g_string_append_printf(s, "After=netplan-ovs-cleanup.service\n");
2556+ } else {
2557+ /* The netplan-ovs-cleanup unit shall not run on systems where openvswitch is not installed. */
2558+ g_string_append(s, "ConditionFileIsExecutable=" OPENVSWITCH_OVS_VSCTL "\n");
2559+ }
2560+ g_string_append(s, "Before=network.target\nWants=network.target\n");
2561+ if (dependency) {
2562+ g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", dependency);
2563+ g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency);
2564+ }
2565+
2566+ g_string_append(s, "\n[Service]\nType=oneshot\n");
2567+ g_string_append(s, cmds->str);
2568+
2569+ g_string_free_to_file(s, rootdir, path, NULL);
2570+
2571+ safe_mkdir_p_dir(link);
2572+ if (symlink(path, link) < 0 && errno != EEXIST) {
2573+ // LCOV_EXCL_START
2574+ g_fprintf(stderr, "failed to create enablement symlink: %m\n");
2575+ exit(1);
2576+ // LCOV_EXCL_STOP
2577+ }
2578+}
2579+
2580+#define append_systemd_cmd(s, command, ...) \
2581+{ \
2582+ g_string_append(s, "ExecStart="); \
2583+ g_string_append_printf(s, command, __VA_ARGS__); \
2584+ g_string_append(s, "\n"); \
2585+}
2586+
2587+static char*
2588+netplan_type_to_table_name(const NetplanDefType type)
2589+{
2590+ switch (type) {
2591+ case NETPLAN_DEF_TYPE_BRIDGE:
2592+ return "Bridge";
2593+ case NETPLAN_DEF_TYPE_BOND:
2594+ case NETPLAN_DEF_TYPE_PORT:
2595+ return "Port";
2596+ default: /* For regular interfaces and others */
2597+ return "Interface";
2598+ }
2599+}
2600+
2601+static gboolean
2602+netplan_type_is_physical(const NetplanDefType type)
2603+{
2604+ switch (type) {
2605+ case NETPLAN_DEF_TYPE_ETHERNET:
2606+ // case NETPLAN_DEF_TYPE_WIFI:
2607+ // case NETPLAN_DEF_TYPE_MODEM:
2608+ return TRUE;
2609+ default:
2610+ return FALSE;
2611+ }
2612+}
2613+
2614+static void
2615+write_ovs_tag_setting(const gchar* id, const char* type, const char* col, const char* key, const char* value, GString* cmds)
2616+{
2617+ g_assert(col);
2618+ g_assert(value);
2619+ g_autofree char *clean_value = g_strdup(value);
2620+ /* Replace " " -> "," if value contains spaces */
2621+ if (strchr(value, ' ')) {
2622+ char **split = g_strsplit(value, " ", -1);
2623+ g_free(clean_value);
2624+ clean_value = g_strjoinv(",", split);
2625+ g_strfreev(split);
2626+ }
2627+
2628+ GString* s = g_string_new("external-ids:netplan/");
2629+ g_string_append_printf(s, "%s", col);
2630+ if (key)
2631+ g_string_append_printf(s, "/%s", key);
2632+ g_string_append_printf(s, "=%s", clean_value);
2633+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s %s", type, id, s->str);
2634+ g_string_free(s, TRUE);
2635+}
2636+
2637+static void
2638+write_ovs_additional_data(GHashTable *data, const char* type, const gchar* id, GString* cmds, const char* setting)
2639+{
2640+ GHashTableIter iter;
2641+ gchar* key;
2642+ gchar* value;
2643+
2644+ g_hash_table_iter_init(&iter, data);
2645+ while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &value)) {
2646+ /* XXX: we need to check what happens when an invalid key=value pair
2647+ gets supplied here. We might want to handle this somehow. */
2648+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s %s:%s=%s",
2649+ type, id, setting, key, value);
2650+ write_ovs_tag_setting(id, type, setting, key, value, cmds);
2651+ }
2652+}
2653+
2654+static void
2655+setup_patch_port(GString* s, const NetplanNetDefinition* def)
2656+{
2657+ /* Execute the setup commands to create an OVS patch port atomically within
2658+ * the same command where this virtual interface is created. Either as a
2659+ * Port+Interface of an OVS bridge or as a Interface of an OVS bond. This
2660+ * avoids delays in the PatchPort creation and thus potential races. */
2661+ g_assert(def->type == NETPLAN_DEF_TYPE_PORT);
2662+ g_string_append_printf(s, " -- set Interface %s type=patch options:peer=%s",
2663+ def->id, def->peer);
2664+}
2665+
2666+static char*
2667+write_ovs_bond_interfaces(const NetplanNetDefinition* def, GString* cmds)
2668+{
2669+ NetplanNetDefinition* tmp_nd;
2670+ GHashTableIter iter;
2671+ gchar* key;
2672+ guint i = 0;
2673+ GString* s = NULL;
2674+ GString* patch_ports = g_string_new("");
2675+
2676+ if (!def->bridge) {
2677+ g_fprintf(stderr, "Bond %s needs to be a slave of an OpenVSwitch bridge\n", def->id);
2678+ exit(1);
2679+ }
2680+
2681+ s = g_string_new(OPENVSWITCH_OVS_VSCTL " --may-exist add-bond");
2682+ g_string_append_printf(s, " %s %s", def->bridge, def->id);
2683+
2684+ g_hash_table_iter_init(&iter, netdefs);
2685+ while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &tmp_nd)) {
2686+ if (!g_strcmp0(def->id, tmp_nd->bond)) {
2687+ /* Append and count bond interfaces */
2688+ g_string_append_printf(s, " %s", tmp_nd->id);
2689+ i++;
2690+ if (tmp_nd->type == NETPLAN_DEF_TYPE_PORT)
2691+ setup_patch_port(patch_ports, tmp_nd);
2692+ }
2693+ }
2694+ if (i < 2) {
2695+ g_fprintf(stderr, "Bond %s needs to have at least 2 slave interfaces\n", def->id);
2696+ exit(1);
2697+ }
2698+
2699+ g_string_append(s, patch_ports->str);
2700+ g_string_free(patch_ports, TRUE);
2701+ append_systemd_cmd(cmds, s->str, def->bridge, def->id);
2702+ g_string_free(s, TRUE);
2703+ return def->bridge;
2704+}
2705+
2706+static void
2707+write_ovs_tag_netplan(const gchar* id, const char* type, GString* cmds)
2708+{
2709+ /* Mark this bridge/port/interface as created by netplan */
2710+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s external-ids:netplan=true",
2711+ type, id);
2712+}
2713+
2714+static void
2715+write_ovs_bond_mode(const NetplanNetDefinition* def, GString* cmds)
2716+{
2717+ char* value = NULL;
2718+ /* OVS supports only "active-backup", "balance-tcp" and "balance-slb":
2719+ * http://www.openvswitch.org/support/dist-docs/ovs-vswitchd.conf.db.5.txt */
2720+ if (!strcmp(def->bond_params.mode, "active-backup") ||
2721+ !strcmp(def->bond_params.mode, "balance-tcp") ||
2722+ !strcmp(def->bond_params.mode, "balance-slb")) {
2723+ value = def->bond_params.mode;
2724+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Port %s bond_mode=%s", def->id, value);
2725+ write_ovs_tag_setting(def->id, "Port", "bond_mode", NULL, value, cmds);
2726+ } else {
2727+ g_fprintf(stderr, "%s: bond mode '%s' not supported by openvswitch\n",
2728+ def->id, def->bond_params.mode);
2729+ exit(1);
2730+ }
2731+}
2732+
2733+static void
2734+write_ovs_bridge_interfaces(const NetplanNetDefinition* def, GString* cmds)
2735+{
2736+ NetplanNetDefinition* tmp_nd;
2737+ GHashTableIter iter;
2738+ gchar* key;
2739+
2740+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-br %s", def->id);
2741+
2742+ g_hash_table_iter_init(&iter, netdefs);
2743+ while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &tmp_nd)) {
2744+ /* OVS bonds will connect to their OVS bridge and create the interface/port themselves */
2745+ if ((tmp_nd->type != NETPLAN_DEF_TYPE_BOND || tmp_nd->backend != NETPLAN_BACKEND_OVS)
2746+ && !g_strcmp0(def->id, tmp_nd->bridge)) {
2747+ GString * patch_ports = g_string_new("");
2748+ if (tmp_nd->type == NETPLAN_DEF_TYPE_PORT)
2749+ setup_patch_port(patch_ports, tmp_nd);
2750+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-port %s %s%s",
2751+ def->id, tmp_nd->id, patch_ports->str);
2752+ g_string_free(patch_ports, TRUE);
2753+ }
2754+ }
2755+}
2756+
2757+static void
2758+write_ovs_protocols(const NetplanOVSSettings* ovs_settings, const gchar* bridge, GString* cmds)
2759+{
2760+ g_assert(bridge);
2761+ GString* s = g_string_new(g_array_index(ovs_settings->protocols, char*, 0));
2762+
2763+ for (unsigned i = 1; i < ovs_settings->protocols->len; ++i)
2764+ g_string_append_printf(s, ",%s", g_array_index(ovs_settings->protocols, char*, i));
2765+
2766+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s protocols=%s", bridge, s->str);
2767+ write_ovs_tag_setting(bridge, "Bridge", "protocols", NULL, s->str, cmds);
2768+ g_string_free(s, TRUE);
2769+}
2770+
2771+static gboolean
2772+check_ovs_ssl(gchar* target)
2773+{
2774+ /* Check if target needs ssl */
2775+ if (g_str_has_prefix(target, "ssl:") || g_str_has_prefix(target, "pssl:")) {
2776+ /* Check if SSL is configured in ovs_settings_global.ssl */
2777+ if (!ovs_settings_global.ssl.ca_certificate || !ovs_settings_global.ssl.client_certificate ||
2778+ !ovs_settings_global.ssl.client_key) {
2779+ g_fprintf(stderr, "ERROR: openvswitch bridge controller target '%s' needs SSL configuration, but global 'openvswitch.ssl' settings are not set\n", target);
2780+ exit(1);
2781+ }
2782+ return TRUE;
2783+ }
2784+ return FALSE;
2785+}
2786+
2787+static void
2788+write_ovs_bridge_controller_targets(const NetplanOVSController* controller, const gchar* bridge, GString* cmds)
2789+{
2790+ gchar* target = g_array_index(controller->addresses, char*, 0);
2791+ gboolean needs_ssl = check_ovs_ssl(target);
2792+ GString* s = g_string_new(target);
2793+
2794+ for (unsigned i = 1; i < controller->addresses->len; ++i) {
2795+ target = g_array_index(controller->addresses, char*, i);
2796+ if (!needs_ssl)
2797+ needs_ssl = check_ovs_ssl(target);
2798+ g_string_append_printf(s, " %s", target);
2799+ }
2800+
2801+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-controller %s %s", bridge, s->str);
2802+ write_ovs_tag_setting(bridge, "Bridge", "global", "set-controller", s->str, cmds);
2803+ g_string_free(s, TRUE);
2804+}
2805+
2806+/**
2807+ * Generate the OpenVSwitch systemd units for configuration of the selected netdef
2808+ * @rootdir: If not %NULL, generate configuration in this root directory
2809+ * (useful for testing).
2810+ */
2811+void
2812+write_ovs_conf(const NetplanNetDefinition* def, const char* rootdir)
2813+{
2814+ GString* cmds = g_string_new(NULL);
2815+ gchar* dependency = NULL;
2816+ const char* type = netplan_type_to_table_name(def->type);
2817+ g_autofree char* base_config_path = NULL;
2818+ char* value = NULL;
2819+
2820+ /* TODO: maybe dynamically query the ovs-vsctl tool path? */
2821+
2822+ /* For OVS specific settings, we expect the backend to be set to OVS.
2823+ * The OVS backend is implicitly set, if an interface contains an empty "openvswitch: {}"
2824+ * key, or an "openvswitch:" key, containing more than "external-ids" and/or "other-config". */
2825+ if (def->backend == NETPLAN_BACKEND_OVS) {
2826+ switch (def->type) {
2827+ case NETPLAN_DEF_TYPE_BOND:
2828+ dependency = write_ovs_bond_interfaces(def, cmds);
2829+ write_ovs_tag_netplan(def->id, type, cmds);
2830+ /* Set LACP mode, default to "off" */
2831+ value = def->ovs_settings.lacp? def->ovs_settings.lacp : "off";
2832+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Port %s lacp=%s", def->id, value);
2833+ write_ovs_tag_setting(def->id, type, "lacp", NULL, value, cmds);
2834+ if (def->bond_params.mode) {
2835+ write_ovs_bond_mode(def, cmds);
2836+ }
2837+ break;
2838+
2839+ case NETPLAN_DEF_TYPE_BRIDGE:
2840+ write_ovs_bridge_interfaces(def, cmds);
2841+ write_ovs_tag_netplan(def->id, type, cmds);
2842+ /* Set fail-mode, default to "standalone" */
2843+ value = def->ovs_settings.fail_mode? def->ovs_settings.fail_mode : "standalone";
2844+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-fail-mode %s %s", def->id, value);
2845+ write_ovs_tag_setting(def->id, type, "global", "set-fail-mode", value, cmds);
2846+ /* Enable/disable mcast-snooping */
2847+ value = def->ovs_settings.mcast_snooping? "true" : "false";
2848+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s mcast_snooping_enable=%s", def->id, value);
2849+ write_ovs_tag_setting(def->id, type, "mcast_snooping_enable", NULL, value, cmds);
2850+ /* Enable/disable rstp */
2851+ value = def->ovs_settings.rstp? "true" : "false";
2852+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s rstp_enable=%s", def->id, value);
2853+ write_ovs_tag_setting(def->id, type, "rstp_enable", NULL, value, cmds);
2854+ /* Set protocols */
2855+ if (def->ovs_settings.protocols && def->ovs_settings.protocols->len > 0) {
2856+ write_ovs_protocols(&(def->ovs_settings), def->id, cmds);
2857+ } else if (ovs_settings_global.protocols && ovs_settings_global.protocols->len > 0) {
2858+ write_ovs_protocols(&(ovs_settings_global), def->id, cmds);
2859+ }
2860+ /* Set controller target addresses */
2861+ if (def->ovs_settings.controller.addresses && def->ovs_settings.controller.addresses->len > 0) {
2862+ write_ovs_bridge_controller_targets(&(def->ovs_settings.controller), def->id, cmds);
2863+ /* Set controller connection mode, only applicable if at least one controller target address was set */
2864+ if (def->ovs_settings.controller.connection_mode) {
2865+ value = def->ovs_settings.controller.connection_mode;
2866+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Controller %s connection-mode=%s", def->id, value);
2867+ write_ovs_tag_setting(def->id, "Controller", "connection-mode", NULL, value, cmds);
2868+ }
2869+ }
2870+ break;
2871+
2872+ case NETPLAN_DEF_TYPE_PORT:
2873+ g_assert(def->peer);
2874+ dependency = def->bridge?: def->bond;
2875+ if (!dependency) {
2876+ g_fprintf(stderr, "%s: OpenVSwitch patch port needs to be assigned to a bridge/bond\n", def->id);
2877+ exit(1);
2878+ }
2879+ /* There is no OVS Port which we could tag netplan=true if this
2880+ * patch port is assigned as an OVS bond interface. Tag the
2881+ * Interface instead, to clean it up from a bond. */
2882+ if (def->bond)
2883+ write_ovs_tag_netplan(def->id, "Interface", cmds);
2884+ else
2885+ write_ovs_tag_netplan(def->id, type, cmds);
2886+ break;
2887+
2888+ case NETPLAN_DEF_TYPE_VLAN:
2889+ g_assert(def->vlan_link);
2890+ dependency = def->vlan_link->id;
2891+ /* Create a fake VLAN bridge */
2892+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-br %s %s %i", def->id, def->vlan_link->id, def->vlan_id)
2893+ write_ovs_tag_netplan(def->id, type, cmds);
2894+ break;
2895+
2896+ default:
2897+ g_fprintf(stderr, "%s: This device type is not supported with the OpenVSwitch backend\n", def->id);
2898+ exit(1);
2899+ break;
2900+ }
2901+
2902+ /* Try writing out a base config */
2903+ base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL);
2904+ write_network_file(def, rootdir, base_config_path);
2905+ } else {
2906+ /* Other interfaces must be part of an OVS bridge or bond to carry additional data */
2907+ if ( (def->ovs_settings.external_ids && g_hash_table_size(def->ovs_settings.external_ids) > 0)
2908+ || (def->ovs_settings.other_config && g_hash_table_size(def->ovs_settings.other_config) > 0)) {
2909+ dependency = def->bridge?: def->bond;
2910+ if (!dependency) {
2911+ g_fprintf(stderr, "%s: Interface needs to be assigned to an OVS bridge/bond to carry external-ids/other-config\n", def->id);
2912+ exit(1);
2913+ }
2914+ } else {
2915+ g_debug("openvswitch: definition %s is not for us (backend %i)", def->id, def->backend);
2916+ return;
2917+ }
2918+ }
2919+
2920+ /* Set "external-ids" and "other-config" after NETPLAN_BACKEND_OVS interfaces, as bonds,
2921+ * bridges, etc. might just be created before.*/
2922+
2923+ /* Common OVS settings can be specified even for non-OVS interfaces */
2924+ if (def->ovs_settings.external_ids && g_hash_table_size(def->ovs_settings.external_ids) > 0) {
2925+ write_ovs_additional_data(def->ovs_settings.external_ids, type,
2926+ def->id, cmds, "external-ids");
2927+ }
2928+
2929+ if (def->ovs_settings.other_config && g_hash_table_size(def->ovs_settings.other_config) > 0) {
2930+ write_ovs_additional_data(def->ovs_settings.other_config, type,
2931+ def->id, cmds, "other-config");
2932+ }
2933+
2934+ /* If we need to configure anything for this netdef, write the required systemd unit */
2935+ if (cmds->len > 0)
2936+ write_ovs_systemd_unit(def->id, cmds, rootdir, netplan_type_is_physical(def->type), FALSE, dependency);
2937+ g_string_free(cmds, TRUE);
2938+}
2939+
2940+/**
2941+ * Finalize the OpenVSwitch configuration (global config)
2942+ */
2943+void
2944+write_ovs_conf_finish(const char* rootdir)
2945+{
2946+ GString* cmds = g_string_new(NULL);
2947+
2948+ /* Global external-ids and other-config settings */
2949+ if (ovs_settings_global.external_ids && g_hash_table_size(ovs_settings_global.external_ids) > 0) {
2950+ write_ovs_additional_data(ovs_settings_global.external_ids, "open_vswitch",
2951+ ".", cmds, "external-ids");
2952+ }
2953+
2954+ if (ovs_settings_global.other_config && g_hash_table_size(ovs_settings_global.other_config) > 0) {
2955+ write_ovs_additional_data(ovs_settings_global.other_config, "open_vswitch",
2956+ ".", cmds, "other-config");
2957+ }
2958+
2959+ if (ovs_settings_global.ssl.client_key && ovs_settings_global.ssl.client_certificate &&
2960+ ovs_settings_global.ssl.ca_certificate) {
2961+ GString* value = g_string_new(NULL);
2962+ g_string_printf(value, "%s %s %s",
2963+ ovs_settings_global.ssl.client_key,
2964+ ovs_settings_global.ssl.client_certificate,
2965+ ovs_settings_global.ssl.ca_certificate);
2966+ append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-ssl %s", value->str);
2967+ write_ovs_tag_setting(".", "open_vswitch", "global", "set-ssl", value->str, cmds);
2968+ g_string_free(value, TRUE);
2969+ }
2970+
2971+ if (cmds->len > 0)
2972+ write_ovs_systemd_unit("global", cmds, rootdir, FALSE, FALSE, NULL);
2973+ g_string_free(cmds, TRUE);
2974+
2975+ /* Clear all netplan=true tagged ports/bonds and bridges, via 'netplan apply --only-ovs-cleanup' */
2976+ cmds = g_string_new(NULL);
2977+ append_systemd_cmd(cmds, "/usr/sbin/netplan apply %s", "--only-ovs-cleanup");
2978+ write_ovs_systemd_unit("cleanup", cmds, rootdir, FALSE, TRUE, NULL);
2979+ g_string_free(cmds, TRUE);
2980+}
2981+
2982+/**
2983+ * Clean up all generated configurations in @rootdir from previous runs.
2984+ */
2985+void
2986+cleanup_ovs_conf(const char* rootdir)
2987+{
2988+ unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-*.service");
2989+ unlink_glob(rootdir, "/run/systemd/system/netplan-ovs-*.service");
2990+}
2991diff --git a/src/openvswitch.h b/src/openvswitch.h
2992new file mode 100644
2993index 0000000..69bd6ee
2994--- /dev/null
2995+++ b/src/openvswitch.h
2996@@ -0,0 +1,24 @@
2997+/*
2998+ * Copyright (C) 2020 Canonical, Ltd.
2999+ * Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
3000+ *
3001+ * This program is free software; you can redistribute it and/or modify
3002+ * it under the terms of the GNU General Public License as published by
3003+ * the Free Software Foundation; version 3.
3004+ *
3005+ * This program is distributed in the hope that it will be useful,
3006+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3007+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3008+ * GNU General Public License for more details.
3009+ *
3010+ * You should have received a copy of the GNU General Public License
3011+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
3012+ */
3013+
3014+#pragma once
3015+
3016+#include "parse.h"
3017+
3018+void write_ovs_conf(const NetplanNetDefinition* def, const char* rootdir);
3019+void write_ovs_conf_finish(const char* rootdir);
3020+void cleanup_ovs_conf(const char* rootdir);
3021diff --git a/src/parse.c b/src/parse.c
3022index c1dc175..2732f78 100644
3023--- a/src/parse.c
3024+++ b/src/parse.c
3025@@ -1,6 +1,7 @@
3026 /*
3027 * Copyright (C) 2016 Canonical, Ltd.
3028 * Author: Martin Pitt <martin.pitt@ubuntu.com>
3029+ * Lukas Märdian <lukas.maerdian@canonical.com>
3030 *
3031 * This program is free software; you can redistribute it and/or modify
3032 * it under the terms of the GNU General Public License as published by
3033@@ -31,11 +32,14 @@
3034 #include "validation.h"
3035
3036 /* convenience macro to put the offset of a NetplanNetDefinition field into "void* data" */
3037+#define access_point_offset(field) GUINT_TO_POINTER(offsetof(NetplanWifiAccessPoint, field))
3038+#define addr_option_offset(field) GUINT_TO_POINTER(offsetof(NetplanAddressOptions, field))
3039+#define auth_offset(field) GUINT_TO_POINTER(offsetof(NetplanAuthenticationSettings, field))
3040+#define ip_rule_offset(field) GUINT_TO_POINTER(offsetof(NetplanIPRule, field))
3041 #define netdef_offset(field) GUINT_TO_POINTER(offsetof(NetplanNetDefinition, field))
3042+#define ovs_settings_offset(field) GUINT_TO_POINTER(offsetof(NetplanOVSSettings, field))
3043 #define route_offset(field) GUINT_TO_POINTER(offsetof(NetplanIPRoute, field))
3044-#define ip_rule_offset(field) GUINT_TO_POINTER(offsetof(NetplanIPRule, field))
3045-#define auth_offset(field) GUINT_TO_POINTER(offsetof(NetplanAuthenticationSettings, field))
3046-#define access_point_offset(field) GUINT_TO_POINTER(offsetof(NetplanWifiAccessPoint, field))
3047+#define wireguard_peer_offset(field) GUINT_TO_POINTER(offsetof(NetplanWireguardPeer, field))
3048
3049 /* NetplanNetDefinition that is currently being processed */
3050 static NetplanNetDefinition* cur_netdef;
3051@@ -43,14 +47,22 @@ static NetplanNetDefinition* cur_netdef;
3052 /* NetplanWifiAccessPoint that is currently being processed */
3053 static NetplanWifiAccessPoint* cur_access_point;
3054
3055-/* authentication options that are currently being processed */
3056+/* NetplanAuthenticationSettings that are currently being processed */
3057 static NetplanAuthenticationSettings* cur_auth;
3058
3059+/* NetplanWireguardPeer that is currently being processed */
3060+static NetplanWireguardPeer* cur_wireguard_peer;
3061+
3062+static NetplanAddressOptions* cur_addr_option;
3063+
3064 static NetplanIPRoute* cur_route;
3065 static NetplanIPRule* cur_ip_rule;
3066
3067 static NetplanBackend backend_global, backend_cur_type;
3068
3069+/* global OpenVSwitch settings */
3070+NetplanOVSSettings ovs_settings_global;
3071+
3072 /* Global ID → NetplanNetDefinition* map for all parsed config files */
3073 GHashTable* netdefs;
3074
3075@@ -62,6 +74,11 @@ GList* netdefs_ordered;
3076 * existing definition */
3077 static GHashTable* ids_in_file;
3078
3079+/* Global variables, defined in this file */
3080+int missing_ids_found;
3081+const char* current_file;
3082+GHashTable* missing_id;
3083+
3084 /**
3085 * Load YAML file name into a yaml_document_t.
3086 *
3087@@ -173,6 +190,57 @@ assert_valid_id(yaml_node_t* node, GError** error)
3088 return TRUE;
3089 }
3090
3091+static void
3092+initialize_dhcp_overrides(NetplanDHCPOverrides* overrides)
3093+{
3094+ overrides->use_dns = TRUE;
3095+ overrides->use_domains = NULL;
3096+ overrides->use_ntp = TRUE;
3097+ overrides->send_hostname = TRUE;
3098+ overrides->use_hostname = TRUE;
3099+ overrides->use_mtu = TRUE;
3100+ overrides->use_routes = TRUE;
3101+ overrides->hostname = NULL;
3102+ overrides->metric = NETPLAN_METRIC_UNSPEC;
3103+}
3104+
3105+static void
3106+initialize_ovs_settings(NetplanOVSSettings* ovs_settings)
3107+{
3108+ ovs_settings->mcast_snooping = FALSE;
3109+ ovs_settings->rstp = FALSE;
3110+}
3111+
3112+static NetplanNetDefinition*
3113+netplan_netdef_new(const char* id, NetplanDefType type, NetplanBackend backend)
3114+{
3115+ /* create new network definition */
3116+ cur_netdef = g_new0(NetplanNetDefinition, 1);
3117+ cur_netdef->type = type;
3118+ cur_netdef->backend = backend ?: NETPLAN_BACKEND_NONE;
3119+ cur_netdef->id = g_strdup(id);
3120+
3121+ /* Set some default values */
3122+ cur_netdef->vlan_id = G_MAXUINT; /* 0 is a valid ID */
3123+ cur_netdef->tunnel.mode = NETPLAN_TUNNEL_MODE_UNKNOWN;
3124+ cur_netdef->dhcp_identifier = g_strdup("duid"); /* keep networkd's default */
3125+ /* systemd-networkd defaults to IPv6 LL enabled; keep that default */
3126+ cur_netdef->linklocal.ipv6 = TRUE;
3127+ cur_netdef->sriov_vlan_filter = FALSE;
3128+ cur_netdef->sriov_explicit_vf_count = G_MAXUINT; /* 0 is a valid number of VFs */
3129+
3130+ /* DHCP override defaults */
3131+ initialize_dhcp_overrides(&cur_netdef->dhcp4_overrides);
3132+ initialize_dhcp_overrides(&cur_netdef->dhcp6_overrides);
3133+
3134+ /* OpenVSwitch defaults */
3135+ initialize_ovs_settings(&cur_netdef->ovs_settings);
3136+
3137+ g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef);
3138+ netdefs_ordered = g_list_append(netdefs_ordered, cur_netdef);
3139+ return cur_netdef;
3140+}
3141+
3142 /****************************************************
3143 * Data types and functions for interpreting YAML nodes
3144 ****************************************************/
3145@@ -216,7 +284,7 @@ get_handler(const mapping_entry_handler* handlers, const char* key)
3146 * Returns: TRUE on success, FALSE on error (@error gets set then).
3147 */
3148 static gboolean
3149-process_mapping(yaml_document_t* doc, yaml_node_t* node, const mapping_entry_handler* handlers, GError** error)
3150+process_mapping(yaml_document_t* doc, yaml_node_t* node, const mapping_entry_handler* handlers, GList** out_values, GError** error)
3151 {
3152 yaml_node_pair_t* entry;
3153
3154@@ -235,10 +303,12 @@ process_mapping(yaml_document_t* doc, yaml_node_t* node, const mapping_entry_han
3155 if (!h)
3156 return yaml_error(key, error, "unknown key '%s'", scalar(key));
3157 assert_type(value, h->type);
3158+ if (out_values)
3159+ *out_values = g_list_prepend(*out_values, g_strdup(scalar(key)));
3160 if (h->map_handlers) {
3161 g_assert(h->handler == NULL);
3162 g_assert(h->type == YAML_MAPPING_NODE);
3163- if (!process_mapping(doc, value, h->map_handlers, error))
3164+ if (!process_mapping(doc, value, h->map_handlers, NULL, error))
3165 return FALSE;
3166 } else {
3167 if (!h->handler(doc, value, h->data, error))
3168@@ -317,6 +387,65 @@ handle_generic_mac(yaml_document_t* doc, yaml_node_t* node, void* entryptr, cons
3169 return handle_generic_str(doc, node, entryptr, data, error);
3170 }
3171
3172+/*
3173+ * Handler for setting a boolean field from a scalar node, inside a given struct
3174+ * @entryptr: pointer to the beginning of the to-be-modified data structure
3175+ * @data: offset into entryptr struct where the boolean field to write is located
3176+ */
3177+static gboolean
3178+handle_generic_bool(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error)
3179+{
3180+ g_assert(entryptr);
3181+ guint offset = GPOINTER_TO_UINT(data);
3182+ gboolean v;
3183+
3184+ if (g_ascii_strcasecmp(scalar(node), "true") == 0 ||
3185+ g_ascii_strcasecmp(scalar(node), "on") == 0 ||
3186+ g_ascii_strcasecmp(scalar(node), "yes") == 0 ||
3187+ g_ascii_strcasecmp(scalar(node), "y") == 0)
3188+ v = TRUE;
3189+ else if (g_ascii_strcasecmp(scalar(node), "false") == 0 ||
3190+ g_ascii_strcasecmp(scalar(node), "off") == 0 ||
3191+ g_ascii_strcasecmp(scalar(node), "no") == 0 ||
3192+ g_ascii_strcasecmp(scalar(node), "n") == 0)
3193+ v = FALSE;
3194+ else
3195+ return yaml_error(node, error, "invalid boolean value '%s'", scalar(node));
3196+
3197+ *((gboolean*) ((void*) entryptr + offset)) = v;
3198+ return TRUE;
3199+}
3200+
3201+/*
3202+ * Handler for setting a HashTable field from a mapping node, inside a given struct
3203+ * @entryptr: pointer to the beginning of the to-be-modified data structure
3204+ * @data: offset into entryptr struct where the boolean field to write is located
3205+*/
3206+static gboolean
3207+handle_generic_map(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error)
3208+{
3209+ guint offset = GPOINTER_TO_UINT(data);
3210+ GHashTable** map = (GHashTable**) ((void*) entryptr + offset);
3211+ if (!*map)
3212+ *map = g_hash_table_new(g_str_hash, g_str_equal);
3213+
3214+ for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
3215+ yaml_node_t* key, *value;
3216+
3217+ key = yaml_document_get_node(doc, entry->key);
3218+ value = yaml_document_get_node(doc, entry->value);
3219+
3220+ assert_type(key, YAML_SCALAR_NODE);
3221+ assert_type(value, YAML_SCALAR_NODE);
3222+
3223+ /* TODO: make sure we free all the memory here */
3224+ if (!g_hash_table_insert(*map, g_strdup(scalar(key)), g_strdup(scalar(value))))
3225+ return yaml_error(node, error, "duplicate map entry '%s'", scalar(key));
3226+ }
3227+
3228+ return TRUE;
3229+}
3230+
3231 /**
3232 * Generic handler for setting a cur_netdef string field from a scalar node
3233 * @data: offset into NetplanNetDefinition where the const char* field to write is
3234@@ -343,7 +472,9 @@ handle_netdef_id(yaml_document_t* doc, yaml_node_t* node, const void* data, GErr
3235
3236 /**
3237 * Generic handler for setting a cur_netdef ID/iface name field referring to an
3238- * existing ID from a scalar node
3239+ * existing ID from a scalar node. This handler also includes a special case
3240+ * handler for OVS VLANs, switching the backend implicitly to OVS for such
3241+ * interfaces
3242 * @data: offset into NetplanNetDefinition where the NetplanNetDefinition* field to write is
3243 * located
3244 */
3245@@ -358,6 +489,11 @@ handle_netdef_id_ref(yaml_document_t* doc, yaml_node_t* node, const void* data,
3246 add_missing_node(node);
3247 } else {
3248 *((NetplanNetDefinition**) ((void*) cur_netdef + offset)) = ref;
3249+
3250+ if (cur_netdef->type == NETPLAN_DEF_TYPE_VLAN && ref->backend == NETPLAN_BACKEND_OVS) {
3251+ g_debug("%s: VLAN defined for openvswitch interface, choosing OVS backend", cur_netdef->id);
3252+ cur_netdef->backend = NETPLAN_BACKEND_OVS;
3253+ }
3254 }
3255 return TRUE;
3256 }
3257@@ -381,24 +517,7 @@ handle_netdef_mac(yaml_document_t* doc, yaml_node_t* node, const void* data, GEr
3258 static gboolean
3259 handle_netdef_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3260 {
3261- guint offset = GPOINTER_TO_UINT(data);
3262- gboolean v;
3263-
3264- if (g_ascii_strcasecmp(scalar(node), "true") == 0 ||
3265- g_ascii_strcasecmp(scalar(node), "on") == 0 ||
3266- g_ascii_strcasecmp(scalar(node), "yes") == 0 ||
3267- g_ascii_strcasecmp(scalar(node), "y") == 0)
3268- v = TRUE;
3269- else if (g_ascii_strcasecmp(scalar(node), "false") == 0 ||
3270- g_ascii_strcasecmp(scalar(node), "off") == 0 ||
3271- g_ascii_strcasecmp(scalar(node), "no") == 0 ||
3272- g_ascii_strcasecmp(scalar(node), "n") == 0)
3273- v = FALSE;
3274- else
3275- return yaml_error(node, error, "invalid boolean value '%s'", scalar(node));
3276-
3277- *((gboolean*) ((void*) cur_netdef + offset)) = v;
3278- return TRUE;
3279+ return handle_generic_bool(doc, node, cur_netdef, data, error);
3280 }
3281
3282 /**
3283@@ -484,6 +603,22 @@ handle_netdef_addrgen(yaml_document_t* doc, yaml_node_t* node, const void* _, GE
3284 return TRUE;
3285 }
3286
3287+static gboolean
3288+handle_netdef_addrtok(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3289+{
3290+ g_assert(cur_netdef);
3291+ gboolean ret = handle_netdef_str(doc, node, data, error);
3292+ if (!is_ip6_address(cur_netdef->ip6_addr_gen_token))
3293+ return yaml_error(node, error, "invalid ipv6-address-token '%s'", scalar(node));
3294+ return ret;
3295+}
3296+
3297+static gboolean
3298+handle_netdef_map(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3299+{
3300+ g_assert(cur_netdef);
3301+ return handle_generic_map(doc, node, cur_netdef, data, error);
3302+}
3303
3304 /****************************************************
3305 * Grammar and handlers for network config "match" entry
3306@@ -585,6 +720,12 @@ handle_access_point_mac(yaml_document_t* doc, yaml_node_t* node, const void* dat
3307 }
3308
3309 static gboolean
3310+handle_access_point_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3311+{
3312+ return handle_generic_bool(doc, node, cur_access_point, data, error);
3313+}
3314+
3315+static gboolean
3316 handle_access_point_password(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3317 {
3318 g_assert(cur_access_point);
3319@@ -605,7 +746,7 @@ handle_access_point_auth(yaml_document_t* doc, yaml_node_t* node, const void* _,
3320 cur_access_point->has_auth = TRUE;
3321
3322 cur_auth = &cur_access_point->auth;
3323- ret = process_mapping(doc, node, auth_handlers, error);
3324+ ret = process_mapping(doc, node, auth_handlers, NULL, error);
3325 cur_auth = NULL;
3326
3327 return ret;
3328@@ -642,6 +783,7 @@ handle_access_point_band(yaml_document_t* doc, yaml_node_t* node, const void* _,
3329 static const mapping_entry_handler wifi_access_point_handlers[] = {
3330 {"band", YAML_SCALAR_NODE, handle_access_point_band},
3331 {"bssid", YAML_SCALAR_NODE, handle_access_point_mac, NULL, access_point_offset(bssid)},
3332+ {"hidden", YAML_SCALAR_NODE, handle_access_point_bool, NULL, access_point_offset(hidden)},
3333 {"channel", YAML_SCALAR_NODE, handle_access_point_guint, NULL, access_point_offset(channel)},
3334 {"mode", YAML_SCALAR_NODE, handle_access_point_mode},
3335 {"password", YAML_SCALAR_NODE, handle_access_point_password},
3336@@ -680,27 +822,19 @@ handle_netdef_renderer(yaml_document_t* doc, yaml_node_t* node, const void* _, G
3337 static gboolean
3338 handle_accept_ra(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3339 {
3340- if (g_ascii_strcasecmp(scalar(node), "true") == 0 ||
3341- g_ascii_strcasecmp(scalar(node), "on") == 0 ||
3342- g_ascii_strcasecmp(scalar(node), "yes") == 0 ||
3343- g_ascii_strcasecmp(scalar(node), "y") == 0)
3344+ gboolean ret = handle_generic_bool(doc, node, cur_netdef, data, error);
3345+ if (cur_netdef->accept_ra)
3346 cur_netdef->accept_ra = NETPLAN_RA_MODE_ENABLED;
3347- else if (g_ascii_strcasecmp(scalar(node), "false") == 0 ||
3348- g_ascii_strcasecmp(scalar(node), "off") == 0 ||
3349- g_ascii_strcasecmp(scalar(node), "no") == 0 ||
3350- g_ascii_strcasecmp(scalar(node), "n") == 0)
3351- cur_netdef->accept_ra = NETPLAN_RA_MODE_DISABLED;
3352 else
3353- return yaml_error(node, error, "invalid boolean value '%s'", scalar(node));
3354-
3355- return TRUE;
3356+ cur_netdef->accept_ra = NETPLAN_RA_MODE_DISABLED;
3357+ return ret;
3358 }
3359
3360 static gboolean
3361 handle_match(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3362 {
3363 cur_netdef->has_match = TRUE;
3364- return process_mapping(doc, node, match_handlers, error);
3365+ return process_mapping(doc, node, match_handlers, NULL, error);
3366 }
3367
3368 struct NetplanWifiWowlanType NETPLAN_WIFI_WOWLAN_TYPES[] = {
3369@@ -747,20 +881,61 @@ handle_auth(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** err
3370 cur_netdef->has_auth = TRUE;
3371
3372 cur_auth = &cur_netdef->auth;
3373- ret = process_mapping(doc, node, auth_handlers, error);
3374+ ret = process_mapping(doc, node, auth_handlers, NULL, error);
3375 cur_auth = NULL;
3376
3377 return ret;
3378 }
3379
3380 static gboolean
3381-handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3382+handle_address_option_lifetime(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3383 {
3384+ if (g_ascii_strcasecmp(scalar(node), "0") != 0 &&
3385+ g_ascii_strcasecmp(scalar(node), "forever") != 0) {
3386+ return yaml_error(node, error, "invalid lifetime value '%s'", scalar(node));
3387+ }
3388+ return handle_generic_str(doc, node, cur_addr_option, data, error);
3389+}
3390+
3391+static gboolean
3392+handle_address_option_label(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3393+{
3394+ return handle_generic_str(doc, node, cur_addr_option, data, error);
3395+}
3396+
3397+const mapping_entry_handler address_option_handlers[] = {
3398+ {"lifetime", YAML_SCALAR_NODE, handle_address_option_lifetime, NULL, addr_option_offset(lifetime)},
3399+ {"label", YAML_SCALAR_NODE, handle_address_option_label, NULL, addr_option_offset(label)},
3400+ {NULL}
3401+};
3402+
3403+/*
3404+ * Handler for setting an array of IP addresses from a sequence node, inside a given struct
3405+ * @entryptr: pointer to the beginning of the do-be-modified data structure
3406+ * @data: offset into entryptr struct where the array to write is located
3407+ */
3408+static gboolean
3409+handle_generic_addresses(yaml_document_t* doc, yaml_node_t* node, gboolean check_zero_prefix, GArray** ip4, GArray** ip6, GError** error)
3410+{
3411+ g_assert(ip4);
3412+ g_assert(ip6);
3413 for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
3414 g_autofree char* addr = NULL;
3415 char* prefix_len;
3416 guint64 prefix_len_num;
3417 yaml_node_t *entry = yaml_document_get_node(doc, *i);
3418+ yaml_node_t *key = NULL;
3419+ yaml_node_t *value = NULL;
3420+
3421+ if (entry->type != YAML_SCALAR_NODE && entry->type != YAML_MAPPING_NODE) {
3422+ return yaml_error(entry, error, "expected either scalar or mapping (check indentation)");
3423+ }
3424+
3425+ if (entry->type == YAML_MAPPING_NODE) {
3426+ key = yaml_document_get_node(doc, entry->data.mapping.pairs.start->key);
3427+ value = yaml_document_get_node(doc, entry->data.mapping.pairs.start->value);
3428+ entry = key;
3429+ }
3430 assert_type(entry, YAML_SCALAR_NODE);
3431
3432 /* split off /prefix_len */
3433@@ -772,26 +947,62 @@ handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError*
3434 prefix_len++; /* skip former '/' into first char of prefix */
3435 prefix_len_num = g_ascii_strtoull(prefix_len, NULL, 10);
3436
3437+ if (value) {
3438+ if (!is_ip4_address(addr) && !is_ip6_address(addr))
3439+ return yaml_error(node, error, "malformed address '%s', must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", scalar(entry));
3440+
3441+ if (!cur_netdef->address_options)
3442+ cur_netdef->address_options = g_array_new(FALSE, FALSE, sizeof(NetplanAddressOptions*));
3443+
3444+ for (unsigned i = 0; i < cur_netdef->address_options->len; ++i) {
3445+ NetplanAddressOptions* opts = g_array_index(cur_netdef->address_options, NetplanAddressOptions*, i);
3446+ /* check for multi-pass parsing, return early if options for this address already exist */
3447+ if (!g_strcmp0(scalar(key), opts->address))
3448+ return TRUE;
3449+ }
3450+
3451+ cur_addr_option = g_new0(NetplanAddressOptions, 1);
3452+ cur_addr_option->address = g_strdup(scalar(key));
3453+
3454+ if (!process_mapping(doc, value, address_option_handlers, NULL, error))
3455+ return FALSE;
3456+
3457+ g_array_append_val(cur_netdef->address_options, cur_addr_option);
3458+ continue;
3459+ }
3460+
3461 /* is it an IPv4 address? */
3462 if (is_ip4_address(addr)) {
3463- if (prefix_len_num == 0 || prefix_len_num > 32)
3464+ if ((check_zero_prefix && prefix_len_num == 0) || prefix_len_num > 32)
3465 return yaml_error(node, error, "invalid prefix length in address '%s'", scalar(entry));
3466
3467- if (!cur_netdef->ip4_addresses)
3468- cur_netdef->ip4_addresses = g_array_new(FALSE, FALSE, sizeof(char*));
3469+ if (!*ip4)
3470+ *ip4 = g_array_new(FALSE, FALSE, sizeof(char*));
3471+
3472+ /* Do not append the same IP (on multiple passes), if it is already contained */
3473+ for (unsigned i = 0; i < (*ip4)->len; ++i)
3474+ if (!g_strcmp0(scalar(entry), g_array_index(*ip4, char*, i)))
3475+ goto skip_ip4;
3476 char* s = g_strdup(scalar(entry));
3477- g_array_append_val(cur_netdef->ip4_addresses, s);
3478+ g_array_append_val(*ip4, s);
3479+skip_ip4:
3480 continue;
3481 }
3482
3483 /* is it an IPv6 address? */
3484 if (is_ip6_address(addr)) {
3485- if (prefix_len_num == 0 || prefix_len_num > 128)
3486+ if ((check_zero_prefix && prefix_len_num == 0) || prefix_len_num > 128)
3487 return yaml_error(node, error, "invalid prefix length in address '%s'", scalar(entry));
3488- if (!cur_netdef->ip6_addresses)
3489- cur_netdef->ip6_addresses = g_array_new(FALSE, FALSE, sizeof(char*));
3490+ if (!*ip6)
3491+ *ip6 = g_array_new(FALSE, FALSE, sizeof(char*));
3492+
3493+ /* Do not append the same IP (on multiple passes), if it is already contained */
3494+ for (unsigned i = 0; i < (*ip6)->len; ++i)
3495+ if (!g_strcmp0(scalar(entry), g_array_index(*ip6, char*, i)))
3496+ goto skip_ip6;
3497 char* s = g_strdup(scalar(entry));
3498- g_array_append_val(cur_netdef->ip6_addresses, s);
3499+ g_array_append_val(*ip6, s);
3500+skip_ip6:
3501 continue;
3502 }
3503
3504@@ -802,6 +1013,12 @@ handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError*
3505 }
3506
3507 static gboolean
3508+handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3509+{
3510+ return handle_generic_addresses(doc, node, TRUE, &(cur_netdef->ip4_addresses), &(cur_netdef->ip6_addresses), error);
3511+}
3512+
3513+static gboolean
3514 handle_gateway4(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3515 {
3516 if (!is_ip4_address(scalar(node)))
3517@@ -847,7 +1064,7 @@ handle_wifi_access_points(yaml_document_t* doc, yaml_node_t* node, const void* d
3518 return ret;
3519 }
3520
3521- if (!process_mapping(doc, value, wifi_access_point_handlers, error)) {
3522+ if (!process_mapping(doc, value, wifi_access_point_handlers, NULL, error)) {
3523 cur_access_point = NULL;
3524 return FALSE;
3525 }
3526@@ -881,7 +1098,11 @@ handle_bridge_interfaces(yaml_document_t* doc, yaml_node_t* node, const void* da
3527 if (component->bond)
3528 return yaml_error(node, error, "%s: interface '%s' is already assigned to bond %s",
3529 cur_netdef->id, scalar(entry), component->bond);
3530- component->bridge = g_strdup(cur_netdef->id);
3531+ component->bridge = g_strdup(cur_netdef->id);
3532+ if (component->backend == NETPLAN_BACKEND_OVS) {
3533+ g_debug("%s: Bridge contains openvswitch interface, choosing OVS backend", cur_netdef->id);
3534+ cur_netdef->backend = NETPLAN_BACKEND_OVS;
3535+ }
3536 }
3537 }
3538
3539@@ -902,9 +1123,19 @@ handle_bond_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GErr
3540 strcmp(scalar(node), "broadcast") == 0 ||
3541 strcmp(scalar(node), "802.3ad") == 0 ||
3542 strcmp(scalar(node), "balance-tlb") == 0 ||
3543- strcmp(scalar(node), "balance-alb") == 0))
3544+ strcmp(scalar(node), "balance-alb") == 0 ||
3545+ strcmp(scalar(node), "balance-tcp") == 0 || // only supported for OVS
3546+ strcmp(scalar(node), "balance-slb") == 0)) // only supported for OVS
3547 return yaml_error(node, error, "unknown bond mode '%s'", scalar(node));
3548
3549+ /* Implicitly set NETPLAN_BACKEND_OVS if ovs-only mode selected */
3550+ if (!strcmp(scalar(node), "balance-tcp") ||
3551+ !strcmp(scalar(node), "balance-slb")) {
3552+ g_debug("%s: mode '%s' only supported with openvswitch, choosing this backend",
3553+ cur_netdef->id, scalar(node));
3554+ cur_netdef->backend = NETPLAN_BACKEND_OVS;
3555+ }
3556+
3557 return handle_netdef_str(doc, node, data, error);
3558 }
3559
3560@@ -932,6 +1163,10 @@ handle_bond_interfaces(yaml_document_t* doc, yaml_node_t* node, const void* data
3561 return yaml_error(node, error, "%s: interface '%s' is already assigned to bond %s",
3562 cur_netdef->id, scalar(entry), component->bond);
3563 component->bond = g_strdup(cur_netdef->id);
3564+ if (component->backend == NETPLAN_BACKEND_OVS) {
3565+ g_debug("%s: Bond contains openvswitch interface, choosing OVS backend", cur_netdef->id);
3566+ cur_netdef->backend = NETPLAN_BACKEND_OVS;
3567+ }
3568 }
3569 }
3570
3571@@ -1076,24 +1311,8 @@ check_and_set_family(int family, guint* dest)
3572 static gboolean
3573 handle_routes_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3574 {
3575- guint offset = GPOINTER_TO_UINT(data);
3576- gboolean v;
3577-
3578- if (g_ascii_strcasecmp(scalar(node), "true") == 0 ||
3579- g_ascii_strcasecmp(scalar(node), "on") == 0 ||
3580- g_ascii_strcasecmp(scalar(node), "yes") == 0 ||
3581- g_ascii_strcasecmp(scalar(node), "y") == 0)
3582- v = TRUE;
3583- else if (g_ascii_strcasecmp(scalar(node), "false") == 0 ||
3584- g_ascii_strcasecmp(scalar(node), "off") == 0 ||
3585- g_ascii_strcasecmp(scalar(node), "no") == 0 ||
3586- g_ascii_strcasecmp(scalar(node), "n") == 0)
3587- v = FALSE;
3588- else
3589- return yaml_error(node, error, "invalid boolean value '%s'", scalar(node));
3590-
3591- *((gboolean*) ((void*) cur_route + offset)) = v;
3592- return TRUE;
3593+ g_assert(cur_route);
3594+ return handle_generic_bool(doc, node, cur_route, data, error);
3595 }
3596
3597 static gboolean
3598@@ -1166,87 +1385,26 @@ handle_ip_rule_ip(yaml_document_t* doc, yaml_node_t* node, const void* data, GEr
3599 }
3600
3601 static gboolean
3602-handle_ip_rule_prio(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3603+handle_ip_rule_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3604 {
3605- guint64 v;
3606- gchar* endptr;
3607-
3608- v = g_ascii_strtoull(scalar(node), &endptr, 10);
3609- if (*endptr != '\0' || v > G_MAXUINT)
3610- return yaml_error(node, error, "invalid priority value '%s'", scalar(node));
3611-
3612- cur_ip_rule->priority = (guint) v;
3613- return TRUE;
3614+ g_assert(cur_ip_rule);
3615+ return handle_generic_guint(doc, node, cur_ip_rule, data, error);
3616 }
3617
3618 static gboolean
3619-handle_ip_rule_tos(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3620+handle_routes_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3621 {
3622- guint64 v;
3623- gchar* endptr;
3624-
3625- v = g_ascii_strtoull(scalar(node), &endptr, 10);
3626- if (*endptr != '\0' || v > 255)
3627- return yaml_error(node, error, "invalid ToS (must be between 0 and 255): %s", scalar(node));
3628-
3629- cur_ip_rule->tos = (guint) v;
3630- return TRUE;
3631-}
3632-
3633-static gboolean
3634-handle_routes_table(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3635-{
3636- guint64 v;
3637- gchar* endptr;
3638-
3639- v = g_ascii_strtoull(scalar(node), &endptr, 10);
3640- if (*endptr != '\0' || v > G_MAXUINT)
3641- return yaml_error(node, error, "invalid routing table '%s'", scalar(node));
3642-
3643- cur_route->table = (guint) v;
3644- return TRUE;
3645-}
3646-
3647-static gboolean
3648-handle_ip_rule_table(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3649-{
3650- guint64 v;
3651- gchar* endptr;
3652-
3653- v = g_ascii_strtoull(scalar(node), &endptr, 10);
3654- if (*endptr != '\0' || v > G_MAXUINT)
3655- return yaml_error(node, error, "invalid routing table '%s'", scalar(node));
3656-
3657- cur_ip_rule->table = (guint) v;
3658- return TRUE;
3659-}
3660-
3661-static gboolean
3662-handle_ip_rule_fwmark(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3663-{
3664- guint64 v;
3665- gchar* endptr;
3666-
3667- v = g_ascii_strtoull(scalar(node), &endptr, 10);
3668- if (*endptr != '\0' || v > G_MAXUINT)
3669- return yaml_error(node, error, "invalid fwmark value '%s'", scalar(node));
3670-
3671- cur_ip_rule->fwmark = (guint) v;
3672- return TRUE;
3673+ g_assert(cur_route);
3674+ return handle_generic_guint(doc, node, cur_route, data, error);
3675 }
3676
3677 static gboolean
3678-handle_routes_metric(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3679+handle_ip_rule_tos(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3680 {
3681- guint64 v;
3682- gchar* endptr;
3683-
3684- v = g_ascii_strtoull(scalar(node), &endptr, 10);
3685- if (*endptr != '\0' || v > G_MAXUINT)
3686- return yaml_error(node, error, "invalid unsigned int value '%s'", scalar(node));
3687-
3688- cur_route->metric = (guint) v;
3689- return TRUE;
3690+ gboolean ret = handle_generic_guint(doc, node, cur_ip_rule, data, error);
3691+ if (cur_ip_rule->tos > 255)
3692+ return yaml_error(node, error, "invalid ToS (must be between 0 and 255): %s", scalar(node));
3693+ return ret;
3694 }
3695
3696 /****************************************************
3697@@ -1343,7 +1501,7 @@ handle_bridge(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** e
3698 {
3699 cur_netdef->custom_bridging = TRUE;
3700 cur_netdef->bridge_params.stp = TRUE;
3701- return process_mapping(doc, node, bridge_params_handlers, error);
3702+ return process_mapping(doc, node, bridge_params_handlers, NULL, error);
3703 }
3704
3705 /****************************************************
3706@@ -1354,44 +1512,57 @@ static const mapping_entry_handler routes_handlers[] = {
3707 {"from", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(from)},
3708 {"on-link", YAML_SCALAR_NODE, handle_routes_bool, NULL, route_offset(onlink)},
3709 {"scope", YAML_SCALAR_NODE, handle_routes_scope},
3710- {"table", YAML_SCALAR_NODE, handle_routes_table},
3711+ {"table", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(table)},
3712 {"to", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(to)},
3713 {"type", YAML_SCALAR_NODE, handle_routes_type},
3714 {"via", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(via)},
3715- {"metric", YAML_SCALAR_NODE, handle_routes_metric},
3716+ {"metric", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(metric)},
3717 {NULL}
3718 };
3719
3720 static gboolean
3721 handle_routes(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3722 {
3723+ if (!cur_netdef->routes)
3724+ cur_netdef->routes = g_array_new(FALSE, TRUE, sizeof(NetplanIPRoute*));
3725+
3726+ /* Avoid adding the same routes in a 2nd parsing pass by comparing
3727+ * the array size to the YAML sequence size. Skip if they are equal. */
3728+ guint item_count = node->data.sequence.items.top - node->data.sequence.items.start;
3729+ if (cur_netdef->routes->len == item_count) {
3730+ g_debug("%s: all routes have already been added", cur_netdef->id);
3731+ return TRUE;
3732+ }
3733+
3734 for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
3735 yaml_node_t *entry = yaml_document_get_node(doc, *i);
3736+ assert_type(entry, YAML_MAPPING_NODE);
3737
3738+ g_assert(cur_route == NULL);
3739 cur_route = g_new0(NetplanIPRoute, 1);
3740 cur_route->type = g_strdup("unicast");
3741 cur_route->scope = g_strdup("global");
3742 cur_route->family = G_MAXUINT; /* 0 is a valid family ID */
3743 cur_route->metric = NETPLAN_METRIC_UNSPEC; /* 0 is a valid metric */
3744+ g_debug("%s: adding new route", cur_netdef->id);
3745
3746- if (process_mapping(doc, entry, routes_handlers, error)) {
3747- if (!cur_netdef->routes) {
3748- cur_netdef->routes = g_array_new(FALSE, FALSE, sizeof(NetplanIPRoute*));
3749- }
3750-
3751+ if (process_mapping(doc, entry, routes_handlers, NULL, error))
3752 g_array_append_val(cur_netdef->routes, cur_route);
3753- }
3754
3755 if ( ( g_ascii_strcasecmp(cur_route->scope, "link") == 0
3756 || g_ascii_strcasecmp(cur_route->scope, "host") == 0)
3757- && !cur_route->to)
3758+ && !cur_route->to) {
3759+ cur_route = NULL;
3760 return yaml_error(node, error, "link and host routes must specify a 'to' IP");
3761- else if ( g_ascii_strcasecmp(cur_route->type, "unicast") == 0
3762+ } else if ( g_ascii_strcasecmp(cur_route->type, "unicast") == 0
3763 && g_ascii_strcasecmp(cur_route->scope, "global") == 0
3764- && (!cur_route->to || !cur_route->via))
3765+ && (!cur_route->to || !cur_route->via)) {
3766+ cur_route = NULL;
3767 return yaml_error(node, error, "unicast route must include both a 'to' and 'via' IP");
3768- else if (g_ascii_strcasecmp(cur_route->type, "unicast") != 0 && !cur_route->to)
3769+ } else if (g_ascii_strcasecmp(cur_route->type, "unicast") != 0 && !cur_route->to) {
3770+ cur_route = NULL;
3771 return yaml_error(node, error, "non-unicast routes must specify a 'to' IP");
3772+ }
3773
3774 cur_route = NULL;
3775
3776@@ -1403,11 +1574,11 @@ handle_routes(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** e
3777
3778 static const mapping_entry_handler ip_rules_handlers[] = {
3779 {"from", YAML_SCALAR_NODE, handle_ip_rule_ip, NULL, ip_rule_offset(from)},
3780- {"mark", YAML_SCALAR_NODE, handle_ip_rule_fwmark},
3781- {"priority", YAML_SCALAR_NODE, handle_ip_rule_prio},
3782- {"table", YAML_SCALAR_NODE, handle_ip_rule_table},
3783+ {"mark", YAML_SCALAR_NODE, handle_ip_rule_guint, NULL, ip_rule_offset(fwmark)},
3784+ {"priority", YAML_SCALAR_NODE, handle_ip_rule_guint, NULL, ip_rule_offset(priority)},
3785+ {"table", YAML_SCALAR_NODE, handle_ip_rule_guint, NULL, ip_rule_offset(table)},
3786 {"to", YAML_SCALAR_NODE, handle_ip_rule_ip, NULL, ip_rule_offset(to)},
3787- {"type-of-service", YAML_SCALAR_NODE, handle_ip_rule_tos},
3788+ {"type-of-service", YAML_SCALAR_NODE, handle_ip_rule_tos, NULL, ip_rule_offset(tos)},
3789 {NULL}
3790 };
3791
3792@@ -1424,7 +1595,7 @@ handle_ip_rules(yaml_document_t* doc, yaml_node_t* node, const void* _, GError**
3793 cur_ip_rule->tos = NETPLAN_IP_RULE_TOS_UNSPEC;
3794 cur_ip_rule->fwmark = NETPLAN_IP_RULE_FW_MARK_UNSPEC;
3795
3796- if (process_mapping(doc, entry, ip_rules_handlers, error)) {
3797+ if (process_mapping(doc, entry, ip_rules_handlers, NULL, error)) {
3798 if (!cur_netdef->ip_rules) {
3799 cur_netdef->ip_rules = g_array_new(FALSE, FALSE, sizeof(NetplanIPRule*));
3800 }
3801@@ -1482,7 +1653,10 @@ handle_bond_primary_slave(yaml_document_t* doc, yaml_node_t* node, const void* d
3802 if (!component) {
3803 add_missing_node(node);
3804 } else {
3805- if (cur_netdef->bond_params.primary_slave)
3806+ /* If this is not the primary pass, the primary slave might already be equally set. */
3807+ if (!g_strcmp0(cur_netdef->bond_params.primary_slave, scalar(node))) {
3808+ return TRUE;
3809+ } else if (cur_netdef->bond_params.primary_slave)
3810 return yaml_error(node, error, "%s: bond already has a primary slave: %s",
3811 cur_netdef->id, cur_netdef->bond_params.primary_slave);
3812
3813@@ -1525,7 +1699,7 @@ static const mapping_entry_handler bond_params_handlers[] = {
3814 static gboolean
3815 handle_bonding(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3816 {
3817- return process_mapping(doc, node, bond_params_handlers, error);
3818+ return process_mapping(doc, node, bond_params_handlers, NULL, error);
3819 }
3820
3821 static gboolean
3822@@ -1592,56 +1766,155 @@ handle_tunnel_mode(yaml_document_t* doc, yaml_node_t* node, const void* _, GErro
3823 return yaml_error(node, error, "%s: tunnel mode '%s' is not supported", cur_netdef->id, key);
3824 }
3825
3826+static const mapping_entry_handler tunnel_keys_handlers[] = {
3827+ {"input", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(tunnel.input_key)},
3828+ {"output", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(tunnel.output_key)},
3829+ {"private", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(tunnel.private_key)},
3830+ {NULL}
3831+};
3832+
3833 static gboolean
3834-handle_tunnel_key(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3835+handle_tunnel_key_mapping(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3836 {
3837- /* Tunnel key should be a number or dotted quad. */
3838- guint offset = GPOINTER_TO_UINT(data);
3839- char** dest = (char**) ((void*) cur_netdef + offset);
3840- guint64 v;
3841- gchar* endptr;
3842+ gboolean ret = FALSE;
3843
3844- v = g_ascii_strtoull(scalar(node), &endptr, 10);
3845- if (*endptr != '\0' || v > G_MAXUINT) {
3846- /* Not a simple uint, try for a dotted quad */
3847- if (!is_ip4_address(scalar(node)))
3848- return yaml_error(node, error, "invalid tunnel key '%s'", scalar(node));
3849+ /* We overload the 'key[s]' setting for tunnels; such that it can either be a
3850+ * single scalar with the same key to use for both input, output and private
3851+ * keys, or a mapping where one can specify each. */
3852+ if (node->type == YAML_SCALAR_NODE) {
3853+ ret = handle_netdef_str(doc, node, netdef_offset(tunnel.input_key), error);
3854+ if (ret)
3855+ ret = handle_netdef_str(doc, node, netdef_offset(tunnel.output_key), error);
3856+ if (ret)
3857+ ret = handle_netdef_str(doc, node, netdef_offset(tunnel.private_key), error);
3858+ } else if (node->type == YAML_MAPPING_NODE)
3859+ ret = process_mapping(doc, node, tunnel_keys_handlers, NULL, error);
3860+ else
3861+ return yaml_error(node, error, "invalid type for 'key[s]': must be a scalar or mapping");
3862+
3863+ return ret;
3864+}
3865+
3866+/**
3867+ * Handler for setting a NetplanWireguardPeer string field from a scalar node
3868+ * @data: pointer to the const char* field to write
3869+ */
3870+static gboolean
3871+handle_wireguard_peer_str(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3872+{
3873+ g_assert(cur_wireguard_peer);
3874+ return handle_generic_str(doc, node, cur_wireguard_peer, data, error);
3875+}
3876+
3877+/**
3878+ * Handler for setting a NetplanWireguardPeer string field from a scalar node
3879+ * @data: pointer to the guint field to write
3880+ */
3881+static gboolean
3882+handle_wireguard_peer_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
3883+{
3884+ g_assert(cur_wireguard_peer);
3885+ return handle_generic_guint(doc, node, cur_wireguard_peer, data, error);
3886+}
3887+
3888+static gboolean
3889+handle_wireguard_allowed_ips(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3890+{
3891+ return handle_generic_addresses(doc, node, FALSE, &(cur_wireguard_peer->allowed_ips),
3892+ &(cur_wireguard_peer->allowed_ips), error);
3893+}
3894+
3895+static gboolean
3896+handle_wireguard_endpoint(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3897+{
3898+ g_autofree char* endpoint = NULL;
3899+ char* port;
3900+ char* address;
3901+ guint64 port_num;
3902+
3903+ endpoint = g_strdup(scalar(node));
3904+ /* absolute minimal length of endpoint is 3 chars: 'h:8' */
3905+ if (strlen(endpoint) < 3) {
3906+ return yaml_error(node, error, "invalid endpoint address or hostname '%s'", scalar(node));
3907+ }
3908+ if (endpoint[0] == '[') {
3909+ /* this is an ipv6 endpoint in [ad:rr:ee::ss]:port form */
3910+ char *endbrace = strrchr(endpoint, ']');
3911+ if (!endbrace)
3912+ return yaml_error(node, error, "invalid address in endpoint '%s'", scalar(node));
3913+ address = endpoint + 1;
3914+ *endbrace = '\0';
3915+ port = strrchr(endbrace + 1, ':');
3916+ } else {
3917+ address = endpoint;
3918+ port = strrchr(endpoint, ':');
3919+ }
3920+ /* split off :port */
3921+ if (!port)
3922+ return yaml_error(node, error, "endpoint '%s' is missing :port", scalar(node));
3923+ *port = '\0';
3924+ port++; /* skip former ':' into first char of port */
3925+ port_num = g_ascii_strtoull(port, NULL, 10);
3926+ if (port_num > 65535)
3927+ return yaml_error(node, error, "invalid port in endpoint '%s'", scalar(node));
3928+ if (is_ip4_address(address) || is_ip6_address(address) || is_hostname(address)) {
3929+ return handle_wireguard_peer_str(doc, node, wireguard_peer_offset(endpoint), error);
3930 }
3931+ return yaml_error(node, error, "invalid endpoint address or hostname '%s'", scalar(node));
3932+}
3933
3934- g_free(*dest);
3935- *dest = g_strdup(scalar(node));
3936+static const mapping_entry_handler wireguard_peer_keys_handlers[] = {
3937+ {"public", YAML_SCALAR_NODE, handle_wireguard_peer_str, NULL, wireguard_peer_offset(public_key)},
3938+ {"shared", YAML_SCALAR_NODE, handle_wireguard_peer_str, NULL, wireguard_peer_offset(preshared_key)},
3939+ {NULL}
3940+};
3941
3942- return TRUE;
3943+static gboolean
3944+handle_wireguard_peer_key_mapping(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3945+{
3946+ return process_mapping(doc, node, wireguard_peer_keys_handlers, NULL, error);
3947 }
3948
3949-static const mapping_entry_handler tunnel_keys_handlers[] = {
3950- {"input", YAML_SCALAR_NODE, handle_tunnel_key, NULL, netdef_offset(tunnel.input_key)},
3951- {"output", YAML_SCALAR_NODE, handle_tunnel_key, NULL, netdef_offset(tunnel.output_key)},
3952+const mapping_entry_handler wireguard_peer_handlers[] = {
3953+ {"keys", YAML_MAPPING_NODE, handle_wireguard_peer_key_mapping},
3954+ {"keepalive", YAML_SCALAR_NODE, handle_wireguard_peer_guint, NULL, wireguard_peer_offset(keepalive)},
3955+ {"endpoint", YAML_SCALAR_NODE, handle_wireguard_endpoint},
3956+ {"allowed-ips", YAML_SEQUENCE_NODE, handle_wireguard_allowed_ips},
3957 {NULL}
3958 };
3959
3960 static gboolean
3961-handle_tunnel_key_mapping(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3962+handle_wireguard_peers(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
3963 {
3964- gboolean ret = FALSE;
3965+ if (!cur_netdef->wireguard_peers)
3966+ cur_netdef->wireguard_peers = g_array_new(FALSE, TRUE, sizeof(NetplanWireguardPeer*));
3967
3968- /* We overload the key 'key' for tunnels; such that it can either be a
3969- * single scalar with the same key to use for both input and output keys,
3970- * or a mapping where one can specify each.
3971- */
3972- if (node->type == YAML_SCALAR_NODE) {
3973- ret = handle_tunnel_key(doc, node, netdef_offset(tunnel.input_key), error);
3974- if (ret)
3975- ret = handle_tunnel_key(doc, node, netdef_offset(tunnel.output_key), error);
3976- }
3977- else if (node->type == YAML_MAPPING_NODE) {
3978- ret = process_mapping(doc, node, tunnel_keys_handlers, error);
3979- }
3980- else {
3981- return yaml_error(node, error, "invalid type for 'keys': must be a scalar or mapping");
3982+ /* Avoid adding the same peers in a 2nd parsing pass by comparing
3983+ * the array size to the YAML sequence size. Skip if they are equal. */
3984+ guint item_count = node->data.sequence.items.top - node->data.sequence.items.start;
3985+ if (cur_netdef->wireguard_peers->len == item_count) {
3986+ g_debug("%s: all wireguard peers have already been added", cur_netdef->id);
3987+ return TRUE;
3988 }
3989
3990- return ret;
3991+ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
3992+ g_autofree char* addr = NULL;
3993+ yaml_node_t *entry = yaml_document_get_node(doc, *i);
3994+ assert_type(entry, YAML_MAPPING_NODE);
3995+
3996+ g_assert(cur_wireguard_peer == NULL);
3997+ cur_wireguard_peer = g_new0(NetplanWireguardPeer, 1);
3998+ cur_wireguard_peer->allowed_ips = g_array_new(FALSE, FALSE, sizeof(char*));
3999+ g_debug("%s: adding new wireguard peer", cur_netdef->id);
4000+
4001+ g_array_append_val(cur_netdef->wireguard_peers, cur_wireguard_peer);
4002+ if (!process_mapping(doc, entry, wireguard_peer_handlers, NULL, error)) {
4003+ cur_wireguard_peer = NULL;
4004+ return FALSE;
4005+ }
4006+ cur_wireguard_peer = NULL;
4007+ }
4008+ return TRUE;
4009 }
4010
4011 /****************************************************
4012@@ -1656,6 +1929,187 @@ static const mapping_entry_handler nm_backend_settings_handlers[] = {
4013 {NULL}
4014 };
4015
4016+static gboolean
4017+handle_ovs_bond_lacp(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4018+{
4019+ if (cur_netdef->type != NETPLAN_DEF_TYPE_BOND)
4020+ return yaml_error(node, error, "Key 'lacp' is only valid for iterface type 'openvswitch bond'");
4021+
4022+ if (g_strcmp0(scalar(node), "active") && g_strcmp0(scalar(node), "passive") && g_strcmp0(scalar(node), "off"))
4023+ return yaml_error(node, error, "Value of 'lacp' needs to be 'active', 'passive' or 'off");
4024+
4025+ return handle_netdef_str(doc, node, data, error);
4026+}
4027+
4028+static gboolean
4029+handle_ovs_bridge_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4030+{
4031+ if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE)
4032+ return yaml_error(node, error, "Key is only valid for iterface type 'openvswitch bridge'");
4033+
4034+ return handle_netdef_bool(doc, node, data, error);
4035+}
4036+
4037+static gboolean
4038+handle_ovs_bridge_fail_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4039+{
4040+ if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE)
4041+ return yaml_error(node, error, "Key 'fail-mode' is only valid for iterface type 'openvswitch bridge'");
4042+
4043+ if (g_strcmp0(scalar(node), "standalone") && g_strcmp0(scalar(node), "secure"))
4044+ return yaml_error(node, error, "Value of 'fail-mode' needs to be 'standalone' or 'secure'");
4045+
4046+ return handle_netdef_str(doc, node, data, error);
4047+}
4048+
4049+static gboolean
4050+handle_ovs_protocol(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error)
4051+{
4052+ const char* supported[] = {
4053+ "OpenFlow10", "OpenFlow11", "OpenFlow12", "OpenFlow13", "OpenFlow14", "OpenFlow15", "OpenFlow16", NULL
4054+ };
4055+ unsigned i = 0;
4056+ guint offset = GPOINTER_TO_UINT(data);
4057+ GArray** protocols = (GArray**) ((void*) entryptr + offset);
4058+
4059+ for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
4060+ yaml_node_t *entry = yaml_document_get_node(doc, *iter);
4061+ assert_type(entry, YAML_SCALAR_NODE);
4062+
4063+ for (i = 0; supported[i] != NULL; ++i)
4064+ if (!g_strcmp0(scalar(entry), supported[i]))
4065+ break;
4066+
4067+ if (supported[i] == NULL)
4068+ return yaml_error(node, error, "Unsupported OVS 'protocol' value: %s", scalar(entry));
4069+
4070+ if (!*protocols)
4071+ *protocols = g_array_new(FALSE, FALSE, sizeof(char*));
4072+ char* s = g_strdup(scalar(entry));
4073+ g_array_append_val(*protocols, s);
4074+ }
4075+
4076+ return TRUE;
4077+}
4078+
4079+static gboolean
4080+handle_ovs_bridge_protocol(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4081+{
4082+ if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE)
4083+ return yaml_error(node, error, "Key 'protocols' is only valid for iterface type 'openvswitch bridge'");
4084+
4085+ return handle_ovs_protocol(doc, node, cur_netdef, data, error);
4086+}
4087+
4088+static gboolean
4089+handle_ovs_bridge_controller_connection_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4090+{
4091+ if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE)
4092+ return yaml_error(node, error, "Key 'controller.connection-mode' is only valid for iterface type 'openvswitch bridge'");
4093+
4094+ if (g_strcmp0(scalar(node), "in-band") && g_strcmp0(scalar(node), "out-of-band"))
4095+ return yaml_error(node, error, "Value of 'connection-mode' needs to be 'in-band' or 'out-of-band'");
4096+
4097+ return handle_netdef_str(doc, node, data, error);
4098+}
4099+
4100+static gboolean
4101+handle_ovs_bridge_controller_addresses(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4102+{
4103+ if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE)
4104+ return yaml_error(node, error, "Key 'controller.addresses' is only valid for iterface type 'openvswitch bridge'");
4105+
4106+ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
4107+ gchar** vec = NULL;
4108+ gboolean is_host = FALSE;
4109+ gboolean is_port = FALSE;
4110+ gboolean is_unix = FALSE;
4111+
4112+ yaml_node_t *entry = yaml_document_get_node(doc, *i);
4113+ assert_type(entry, YAML_SCALAR_NODE);
4114+ /* We always need at least one colon */
4115+ if (!g_strrstr(scalar(entry), ":"))
4116+ return yaml_error(node, error, "Unsupported OVS controller target: %s", scalar(entry));
4117+
4118+ vec = g_strsplit (scalar(entry), ":", 2);
4119+
4120+ is_host = !g_strcmp0(vec[0], "tcp") || !g_strcmp0(vec[0], "ssl");
4121+ is_port = !g_strcmp0(vec[0], "ptcp") || !g_strcmp0(vec[0], "pssl");
4122+ is_unix = !g_strcmp0(vec[0], "unix") || !g_strcmp0(vec[0], "punix");
4123+
4124+ if (!cur_netdef->ovs_settings.controller.addresses)
4125+ cur_netdef->ovs_settings.controller.addresses = g_array_new(FALSE, FALSE, sizeof(char*));
4126+
4127+ /* Format: [p]unix:file */
4128+ if (is_unix && vec[1] != NULL && vec[2] == NULL) {
4129+ char* s = g_strdup(scalar(entry));
4130+ g_array_append_val(cur_netdef->ovs_settings.controller.addresses, s);
4131+ g_strfreev(vec);
4132+ continue;
4133+ /* Format tcp:host[:port] or ssl:host[:port] */
4134+ } else if (is_host && validate_ovs_target(TRUE, vec[1])) {
4135+ char* s = g_strdup(scalar(entry));
4136+ g_array_append_val(cur_netdef->ovs_settings.controller.addresses, s);
4137+ g_strfreev(vec);
4138+ continue;
4139+ /* Format ptcp:[port][:host] or pssl:[port][:host] */
4140+ } else if (is_port && validate_ovs_target(FALSE, vec[1])) {
4141+ char* s = g_strdup(scalar(entry));
4142+ g_array_append_val(cur_netdef->ovs_settings.controller.addresses, s);
4143+ g_strfreev(vec);
4144+ continue;
4145+ }
4146+
4147+ g_strfreev(vec);
4148+ return yaml_error(node, error, "Unsupported OVS controller target: %s", scalar(entry));
4149+ }
4150+
4151+ return TRUE;
4152+}
4153+
4154+static const mapping_entry_handler ovs_controller_handlers[] = {
4155+ {"addresses", YAML_SEQUENCE_NODE, handle_ovs_bridge_controller_addresses, NULL, netdef_offset(ovs_settings.controller.addresses)},
4156+ {"connection-mode", YAML_SCALAR_NODE, handle_ovs_bridge_controller_connection_mode, NULL, netdef_offset(ovs_settings.controller.connection_mode)},
4157+ {NULL},
4158+};
4159+
4160+static const mapping_entry_handler ovs_backend_settings_handlers[] = {
4161+ {"external-ids", YAML_MAPPING_NODE, handle_netdef_map, NULL, netdef_offset(ovs_settings.external_ids)},
4162+ {"other-config", YAML_MAPPING_NODE, handle_netdef_map, NULL, netdef_offset(ovs_settings.other_config)},
4163+ {"lacp", YAML_SCALAR_NODE, handle_ovs_bond_lacp, NULL, netdef_offset(ovs_settings.lacp)},
4164+ {"fail-mode", YAML_SCALAR_NODE, handle_ovs_bridge_fail_mode, NULL, netdef_offset(ovs_settings.fail_mode)},
4165+ {"mcast-snooping", YAML_SCALAR_NODE, handle_ovs_bridge_bool, NULL, netdef_offset(ovs_settings.mcast_snooping)},
4166+ {"rstp", YAML_SCALAR_NODE, handle_ovs_bridge_bool, NULL, netdef_offset(ovs_settings.rstp)},
4167+ {"protocols", YAML_SEQUENCE_NODE, handle_ovs_bridge_protocol, NULL, netdef_offset(ovs_settings.protocols)},
4168+ {"controller", YAML_MAPPING_NODE, NULL, ovs_controller_handlers},
4169+ {NULL}
4170+};
4171+
4172+static gboolean
4173+handle_ovs_backend(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
4174+{
4175+ GList* values = NULL;
4176+ gboolean ret = process_mapping(doc, node, ovs_backend_settings_handlers, &values, error);
4177+ guint len = g_list_length(values);
4178+
4179+ if (cur_netdef->type != NETPLAN_DEF_TYPE_BOND && cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) {
4180+ GList *other_config = g_list_find_custom(values, "other-config", (GCompareFunc) strcmp);
4181+ GList *external_ids = g_list_find_custom(values, "external-ids", (GCompareFunc) strcmp);
4182+ /* Non-bond/non-bridge interfaces might still be handled by the networkd backend */
4183+ if (len == 1 && (other_config || external_ids))
4184+ return ret;
4185+ else if (len == 2 && other_config && external_ids)
4186+ return ret;
4187+ }
4188+ g_list_free_full(values, g_free);
4189+
4190+ /* Set the renderer for this device to NETPLAN_BACKEND_OVS, implicitly.
4191+ * But only if empty "openvswitch: {}" or "openvswitch:" with more than
4192+ * "other-config" or "external-ids" keys is given. */
4193+ cur_netdef->backend = NETPLAN_BACKEND_OVS;
4194+ return ret;
4195+}
4196+
4197 static const mapping_entry_handler nameservers_handlers[] = {
4198 {"search", YAML_SEQUENCE_NODE, handle_nameservers_search},
4199 {"addresses", YAML_SEQUENCE_NODE, handle_nameservers_addresses},
4200@@ -1668,7 +2122,7 @@ static const mapping_entry_handler nameservers_handlers[] = {
4201 {"route-metric", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(overrides.metric)}, \
4202 {"send-hostname", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.send_hostname)}, \
4203 {"use-dns", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_dns)}, \
4204- {"use-domains", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(overrides.use_domains)}, \
4205+ {"use-domains", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(overrides.use_domains)}, \
4206 {"use-hostname", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_hostname)}, \
4207 {"use-mtu", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_mtu)}, \
4208 {"use-ntp", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_ntp)}, \
4209@@ -1686,7 +2140,7 @@ static const mapping_entry_handler dhcp6_overrides_handlers[] = {
4210
4211 /* Handlers shared by all link types */
4212 #define COMMON_LINK_HANDLERS \
4213- {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra}, \
4214+ {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra, NULL, netdef_offset(accept_ra)}, \
4215 {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, \
4216 {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, \
4217 {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, \
4218@@ -1696,7 +2150,8 @@ static const mapping_entry_handler dhcp6_overrides_handlers[] = {
4219 {"dhcp6-overrides", YAML_MAPPING_NODE, NULL, dhcp6_overrides_handlers}, \
4220 {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, \
4221 {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, \
4222- {"ipv6-address-generation", YAML_SCALAR_NODE, handle_netdef_addrgen}, \
4223+ {"ipv6-address-generation", YAML_SCALAR_NODE, handle_netdef_addrgen}, \
4224+ {"ipv6-address-token", YAML_SCALAR_NODE, handle_netdef_addrtok, NULL, netdef_offset(ip6_addr_gen_token)}, \
4225 {"ipv6-mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(ipv6_mtubytes)}, \
4226 {"ipv6-privacy", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(ip6_privacy)}, \
4227 {"link-local", YAML_SEQUENCE_NODE, handle_link_local}, \
4228@@ -1709,15 +2164,16 @@ static const mapping_entry_handler dhcp6_overrides_handlers[] = {
4229 {"routes", YAML_SEQUENCE_NODE, handle_routes}, \
4230 {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules}
4231
4232-#define COMMON_BACKEND_HANDLERS \
4233- {"networkmanager", YAML_MAPPING_NODE, NULL, nm_backend_settings_handlers}
4234+#define COMMON_BACKEND_HANDLERS \
4235+ {"networkmanager", YAML_MAPPING_NODE, NULL, nm_backend_settings_handlers}, \
4236+ {"openvswitch", YAML_MAPPING_NODE, handle_ovs_backend}
4237
4238 /* Handlers for physical links */
4239-#define PHYSICAL_LINK_HANDLERS \
4240- {"match", YAML_MAPPING_NODE, handle_match}, \
4241- {"set-name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(set_name)}, \
4242- {"wakeonlan", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(wake_on_lan)}, \
4243- {"wakeonwlan", YAML_SEQUENCE_NODE, handle_wowlan, NULL, netdef_offset(wowlan)}, \
4244+#define PHYSICAL_LINK_HANDLERS \
4245+ {"match", YAML_MAPPING_NODE, handle_match}, \
4246+ {"set-name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(set_name)}, \
4247+ {"wakeonlan", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(wake_on_lan)}, \
4248+ {"wakeonwlan", YAML_SEQUENCE_NODE, handle_wowlan, NULL, netdef_offset(wowlan)}, \
4249 {"emit-lldp", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(emit_lldp)}
4250
4251 static const mapping_entry_handler ethernet_def_handlers[] = {
4252@@ -1789,6 +2245,11 @@ static const mapping_entry_handler tunnel_def_handlers[] = {
4253 */
4254 {"key", YAML_NO_NODE, handle_tunnel_key_mapping},
4255 {"keys", YAML_NO_NODE, handle_tunnel_key_mapping},
4256+
4257+ /* wireguard */
4258+ {"mark", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(tunnel.fwmark)},
4259+ {"port", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(tunnel.port)},
4260+ {"peers", YAML_SEQUENCE_NODE, handle_wireguard_peers},
4261 {NULL}
4262 };
4263
4264@@ -1814,22 +2275,74 @@ handle_network_renderer(yaml_document_t* doc, yaml_node_t* node, const void* _,
4265 return parse_renderer(node, &backend_global, error);
4266 }
4267
4268-static void
4269-initialize_dhcp_overrides(NetplanDHCPOverrides* overrides)
4270+static gboolean
4271+handle_network_ovs_settings_global(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4272 {
4273- overrides->use_dns = TRUE;
4274- overrides->use_domains = NULL;
4275- overrides->use_ntp = TRUE;
4276- overrides->send_hostname = TRUE;
4277- overrides->use_hostname = TRUE;
4278- overrides->use_mtu = TRUE;
4279- overrides->use_routes = TRUE;
4280- overrides->hostname = NULL;
4281- overrides->metric = NETPLAN_METRIC_UNSPEC;
4282+ return handle_generic_map(doc, node, &ovs_settings_global, data, error);
4283+}
4284+
4285+static gboolean
4286+handle_network_ovs_settings_global_protocol(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4287+{
4288+ return handle_ovs_protocol(doc, node, &ovs_settings_global, data, error);
4289+}
4290+
4291+static gboolean
4292+handle_network_ovs_settings_global_ports(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
4293+{
4294+ yaml_node_t* port = NULL;
4295+ yaml_node_t* peer = NULL;
4296+ yaml_node_t* pair = NULL;
4297+ yaml_node_item_t *item = NULL;
4298+ NetplanNetDefinition *component = NULL;
4299+
4300+ for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
4301+ pair = yaml_document_get_node(doc, *iter);
4302+ assert_type(pair, YAML_SEQUENCE_NODE);
4303+
4304+ item = pair->data.sequence.items.start;
4305+ /* A peer port definition must contain exactly 2 ports */
4306+ if (item+2 != pair->data.sequence.items.top) {
4307+ return yaml_error(pair, error, "An openvswitch peer port sequence must have exactly two entries");
4308+ }
4309+
4310+ port = yaml_document_get_node(doc, *item);
4311+ assert_type(port, YAML_SCALAR_NODE);
4312+ peer = yaml_document_get_node(doc, *(item+1));
4313+ assert_type(peer, YAML_SCALAR_NODE);
4314+
4315+ /* Create port 1 netdef */
4316+ component = g_hash_table_lookup(netdefs, scalar(port));
4317+ if (!component) {
4318+ component = netplan_netdef_new(scalar(port), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
4319+ if (g_hash_table_remove(missing_id, scalar(port)))
4320+ missing_ids_found++;
4321+ }
4322+
4323+ if (component->peer && g_strcmp0(component->peer, scalar(peer)))
4324+ return yaml_error(port, error, "openvswitch port '%s' is already assigned to peer '%s'",
4325+ component->id, component->peer);
4326+ component->peer = g_strdup(scalar(peer));
4327+
4328+ /* Create port 2 (peer) netdef */
4329+ component = NULL;
4330+ component = g_hash_table_lookup(netdefs, scalar(peer));
4331+ if (!component) {
4332+ component = netplan_netdef_new(scalar(peer), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
4333+ if (g_hash_table_remove(missing_id, scalar(peer)))
4334+ missing_ids_found++;
4335+ }
4336+
4337+ if (component->peer && g_strcmp0(component->peer, scalar(port)))
4338+ return yaml_error(peer, error, "openvswitch port '%s' is already assigned to peer '%s'",
4339+ component->id, component->peer);
4340+ component->peer = g_strdup(scalar(port));
4341+ }
4342+ return TRUE;
4343 }
4344
4345 /**
4346- * Callback for a net device type entry like "ethernets:" in "networks:"
4347+ * Callback for a net device type entry like "ethernets:" in "network:"
4348 * @data: netdef_type (as pointer)
4349 */
4350 static gboolean
4351@@ -1869,27 +2382,7 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G
4352 if (cur_netdef->type != GPOINTER_TO_UINT(data))
4353 return yaml_error(key, error, "Updated definition '%s' changes device type", scalar(key));
4354 } else {
4355- /* create new network definition */
4356- cur_netdef = g_new0(NetplanNetDefinition, 1);
4357- cur_netdef->type = GPOINTER_TO_UINT(data);
4358- cur_netdef->backend = backend_cur_type ?: NETPLAN_BACKEND_NONE;
4359- cur_netdef->id = g_strdup(scalar(key));
4360-
4361- /* Set some default values */
4362- cur_netdef->vlan_id = G_MAXUINT; /* 0 is a valid ID */
4363- cur_netdef->tunnel.mode = NETPLAN_TUNNEL_MODE_UNKNOWN;
4364- cur_netdef->dhcp_identifier = g_strdup("duid"); /* keep networkd's default */
4365- /* systemd-networkd defaults to IPv6 LL enabled; keep that default */
4366- cur_netdef->linklocal.ipv6 = TRUE;
4367- g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef);
4368- netdefs_ordered = g_list_append(netdefs_ordered, cur_netdef);
4369- cur_netdef->sriov_vlan_filter = FALSE;
4370-
4371- /* DHCP override defaults */
4372- initialize_dhcp_overrides(&cur_netdef->dhcp4_overrides);
4373- initialize_dhcp_overrides(&cur_netdef->dhcp6_overrides);
4374-
4375- g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef);
4376+ netplan_netdef_new(scalar(key), GPOINTER_TO_UINT(data), backend_cur_type);
4377 }
4378
4379 // XXX: breaks multi-pass parsing.
4380@@ -1907,7 +2400,7 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G
4381 case NETPLAN_DEF_TYPE_WIFI: handlers = wifi_def_handlers; break;
4382 default: g_assert_not_reached(); // LCOV_EXCL_LINE
4383 }
4384- if (!process_mapping(doc, value, handlers, error))
4385+ if (!process_mapping(doc, value, handlers, NULL, error))
4386 return FALSE;
4387
4388 /* validate definition-level conditions */
4389@@ -1923,6 +2416,34 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G
4390 return TRUE;
4391 }
4392
4393+static const mapping_entry_handler ovs_global_ssl_handlers[] = {
4394+ {"ca-cert", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(ca_certificate)},
4395+ {"certificate", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_certificate)},
4396+ {"private-key", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_key)},
4397+ {NULL}
4398+};
4399+
4400+static gboolean
4401+handle_ovs_global_ssl(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
4402+{
4403+ gboolean ret;
4404+
4405+ cur_auth = &(ovs_settings_global.ssl);
4406+ ret = process_mapping(doc, node, ovs_global_ssl_handlers, NULL, error);
4407+ cur_auth = NULL;
4408+
4409+ return ret;
4410+}
4411+
4412+static const mapping_entry_handler ovs_network_settings_handlers[] = {
4413+ {"external-ids", YAML_MAPPING_NODE, handle_network_ovs_settings_global, NULL, ovs_settings_offset(external_ids)},
4414+ {"other-config", YAML_MAPPING_NODE, handle_network_ovs_settings_global, NULL, ovs_settings_offset(other_config)},
4415+ {"protocols", YAML_SEQUENCE_NODE, handle_network_ovs_settings_global_protocol, NULL, ovs_settings_offset(protocols)},
4416+ {"ports", YAML_SEQUENCE_NODE, handle_network_ovs_settings_global_ports},
4417+ {"ssl", YAML_MAPPING_NODE, handle_ovs_global_ssl},
4418+ {NULL}
4419+};
4420+
4421 static const mapping_entry_handler network_handlers[] = {
4422 {"bonds", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_BOND)},
4423 {"bridges", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_BRIDGE)},
4424@@ -1933,6 +2454,7 @@ static const mapping_entry_handler network_handlers[] = {
4425 {"vlans", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_VLAN)},
4426 {"wifis", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_WIFI)},
4427 {"modems", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_MODEM)},
4428+ {"openvswitch", YAML_MAPPING_NODE, NULL, ovs_network_settings_handlers},
4429 {NULL}
4430 };
4431
4432@@ -1966,7 +2488,7 @@ process_document(yaml_document_t* doc, GError** error)
4433
4434 g_clear_error(error);
4435
4436- ret = process_mapping(doc, yaml_document_get_root_node(doc), root_handlers, error);
4437+ ret = process_mapping(doc, yaml_document_get_root_node(doc), root_handlers, NULL, error);
4438
4439 still_missing = g_hash_table_size(missing_id);
4440
4441diff --git a/src/parse.h b/src/parse.h
4442index b84ff4c..9f57bb4 100644
4443--- a/src/parse.h
4444+++ b/src/parse.h
4445@@ -25,14 +25,14 @@
4446
4447
4448 /* file that is currently being processed, for useful error messages */
4449-const char* current_file;
4450+extern const char* current_file;
4451
4452 /* List of "seen" ids not found in netdefs yet by the parser.
4453 * These are removed when it exists in this list and we reach the point of
4454 * creating a netdef for that id; so by the time we're done parsing the yaml
4455 * document it should be empty. */
4456-GHashTable *missing_id;
4457-int missing_ids_found;
4458+extern GHashTable *missing_id;
4459+extern int missing_ids_found;
4460
4461 /****************************************************
4462 * Parsed definitions
4463@@ -50,12 +50,14 @@ typedef enum {
4464 NETPLAN_DEF_TYPE_BOND,
4465 NETPLAN_DEF_TYPE_VLAN,
4466 NETPLAN_DEF_TYPE_TUNNEL,
4467+ NETPLAN_DEF_TYPE_PORT,
4468 } NetplanDefType;
4469
4470 typedef enum {
4471 NETPLAN_BACKEND_NONE,
4472 NETPLAN_BACKEND_NETWORKD,
4473 NETPLAN_BACKEND_NM,
4474+ NETPLAN_BACKEND_OVS,
4475 NETPLAN_BACKEND_MAX_,
4476 } NetplanBackend;
4477
4478@@ -63,6 +65,7 @@ static const char* const netplan_backend_to_name[NETPLAN_BACKEND_MAX_] = {
4479 [NETPLAN_BACKEND_NONE] = "none",
4480 [NETPLAN_BACKEND_NETWORKD] = "networkd",
4481 [NETPLAN_BACKEND_NM] = "NetworkManager",
4482+ [NETPLAN_BACKEND_OVS] = "OpenVSwitch",
4483 };
4484
4485 typedef enum {
4486@@ -111,6 +114,7 @@ typedef enum {
4487 /* systemd-only, apparently? */
4488 NETPLAN_TUNNEL_MODE_GRETAP = 101,
4489 NETPLAN_TUNNEL_MODE_IP6GRETAP = 102,
4490+ NETPLAN_TUNNEL_MODE_WIREGUARD = 103,
4491
4492 NETPLAN_TUNNEL_MODE_MAX_,
4493 } NetplanTunnelMode;
4494@@ -130,6 +134,7 @@ netplan_tunnel_mode_table[NETPLAN_TUNNEL_MODE_MAX_] = {
4495
4496 [NETPLAN_TUNNEL_MODE_GRETAP] = "gretap",
4497 [NETPLAN_TUNNEL_MODE_IP6GRETAP] = "ip6gretap",
4498+ [NETPLAN_TUNNEL_MODE_WIREGUARD] = "wireguard",
4499 };
4500
4501 typedef enum {
4502@@ -196,6 +201,23 @@ typedef struct dhcp_overrides {
4503 guint metric;
4504 } NetplanDHCPOverrides;
4505
4506+typedef struct ovs_controller {
4507+ char* connection_mode;
4508+ GArray* addresses;
4509+} NetplanOVSController;
4510+
4511+typedef struct ovs_settings {
4512+ GHashTable* external_ids;
4513+ GHashTable* other_config;
4514+ char* lacp;
4515+ char* fail_mode;
4516+ gboolean mcast_snooping;
4517+ GArray* protocols;
4518+ gboolean rstp;
4519+ NetplanOVSController controller;
4520+ NetplanAuthenticationSettings ssl;
4521+} NetplanOVSSettings;
4522+
4523 /**
4524 * Represent a configuration stanza
4525 */
4526@@ -225,8 +247,10 @@ struct net_definition {
4527 NetplanRAMode accept_ra;
4528 GArray* ip4_addresses;
4529 GArray* ip6_addresses;
4530+ GArray* address_options;
4531 gboolean ip6_privacy;
4532 guint ip6_addr_gen_mode;
4533+ char* ip6_addr_gen_token;
4534 char* gateway4;
4535 char* gateway6;
4536 GArray* ip4_nameservers;
4537@@ -234,6 +258,7 @@ struct net_definition {
4538 GArray* search_domains;
4539 GArray* routes;
4540 GArray* ip_rules;
4541+ GArray* wireguard_peers;
4542 struct {
4543 gboolean ipv4;
4544 gboolean ipv6;
4545@@ -243,6 +268,9 @@ struct net_definition {
4546 char* bridge;
4547 char* bond;
4548
4549+ /* peer ID for OVS patch ports */
4550+ char* peer;
4551+
4552 /* vlan */
4553 guint vlan_id;
4554 NetplanNetDefinition* vlan_link;
4555@@ -269,7 +297,6 @@ struct net_definition {
4556 NetplanWifiWowlanFlag wowlan;
4557 gboolean emit_lldp;
4558
4559-
4560 /* these properties are only valid for NETPLAN_DEF_TYPE_WIFI */
4561 GHashTable* access_points; /* SSID → NetplanWifiAccessPoint* */
4562
4563@@ -297,6 +324,7 @@ struct net_definition {
4564 char* primary_slave;
4565 } bond_params;
4566
4567+ /* netplan-feature: modems */
4568 struct {
4569 char* apn;
4570 gboolean auto_config;
4571@@ -328,16 +356,24 @@ struct net_definition {
4572 char *remote_ip;
4573 char *input_key;
4574 char *output_key;
4575+ char *private_key; /* used for wireguard */
4576+ guint fwmark;
4577+ guint port;
4578 } tunnel;
4579
4580 NetplanAuthenticationSettings auth;
4581 gboolean has_auth;
4582
4583 /* these properties are only valid for SR-IOV NICs */
4584+ /* netplan-feature: sriov */
4585 struct net_definition* sriov_link;
4586 gboolean sriov_vlan_filter;
4587 guint sriov_explicit_vf_count;
4588
4589+ /* these properties are only valid for OpenVSwitch */
4590+ /* netplan-feature: openvswitch */
4591+ NetplanOVSSettings ovs_settings;
4592+
4593 union {
4594 struct NetplanNMSettings {
4595 char *name;
4596@@ -357,6 +393,14 @@ typedef enum {
4597 NETPLAN_WIFI_MODE_AP
4598 } NetplanWifiMode;
4599
4600+typedef struct {
4601+ char *endpoint;
4602+ char *public_key;
4603+ char *preshared_key;
4604+ GArray *allowed_ips;
4605+ guint keepalive;
4606+} NetplanWireguardPeer;
4607+
4608 typedef enum {
4609 NETPLAN_WIFI_BAND_DEFAULT,
4610 NETPLAN_WIFI_BAND_5,
4611@@ -364,10 +408,17 @@ typedef enum {
4612 } NetplanWifiBand;
4613
4614 typedef struct {
4615+ char* address;
4616+ char* lifetime;
4617+ char* label;
4618+} NetplanAddressOptions;
4619+
4620+typedef struct {
4621 NetplanWifiMode mode;
4622 char* ssid;
4623 NetplanWifiBand band;
4624 char* bssid;
4625+ gboolean hidden;
4626 guint channel;
4627
4628 NetplanAuthenticationSettings auth;
4629@@ -415,6 +466,7 @@ typedef struct {
4630 /* Written/updated by parse_yaml(): char* id → net_definition */
4631 extern GHashTable* netdefs;
4632 extern GList* netdefs_ordered;
4633+extern NetplanOVSSettings ovs_settings_global;
4634
4635 /****************************************************
4636 * Functions
4637diff --git a/src/sriov.c b/src/sriov.c
4638new file mode 100644
4639index 0000000..60f9800
4640--- /dev/null
4641+++ b/src/sriov.c
4642@@ -0,0 +1,40 @@
4643+/*
4644+ * Copyright (C) 2020 Canonical, Ltd.
4645+ * Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@canonical.com>
4646+ *
4647+ * This program is free software; you can redistribute it and/or modify
4648+ * it under the terms of the GNU General Public License as published by
4649+ * the Free Software Foundation; version 3.
4650+ *
4651+ * This program is distributed in the hope that it will be useful,
4652+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
4653+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4654+ * GNU General Public License for more details.
4655+ *
4656+ * You should have received a copy of the GNU General Public License
4657+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
4658+ */
4659+
4660+#include <unistd.h>
4661+
4662+#include <glib.h>
4663+#include <glib/gstdio.h>
4664+#include <glib-object.h>
4665+
4666+#include "util.h"
4667+
4668+void
4669+write_sriov_conf_finish(const char* rootdir)
4670+{
4671+ /* For now we execute apply --sriov-only everytime there is a new
4672+ SR-IOV device appearing, which is fine as it's relatively fast */
4673+ GString *udev_rule = g_string_new("ACTION==\"add\", SUBSYSTEM==\"net\", ATTRS{sriov_totalvfs}==\"?*\", RUN+=\"/usr/sbin/netplan apply --sriov-only\"\n");
4674+ g_string_free_to_file(udev_rule, rootdir, "run/udev/rules.d/99-sriov-netplan-setup.rules", NULL);
4675+}
4676+
4677+void
4678+cleanup_sriov_conf(const char* rootdir)
4679+{
4680+ g_autofree char* rulepath = g_strjoin(NULL, rootdir ?: "", "/run/udev/rules.d/99-sriov-netplan-setup.rules", NULL);
4681+ unlink(rulepath);
4682+}
4683diff --git a/src/sriov.h b/src/sriov.h
4684new file mode 100644
4685index 0000000..7cd5896
4686--- /dev/null
4687+++ b/src/sriov.h
4688@@ -0,0 +1,21 @@
4689+/*
4690+ * Copyright (C) 2020 Canonical, Ltd.
4691+ * Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@canonical.com>
4692+ *
4693+ * This program is free software; you can redistribute it and/or modify
4694+ * it under the terms of the GNU General Public License as published by
4695+ * the Free Software Foundation; version 3.
4696+ *
4697+ * This program is distributed in the hope that it will be useful,
4698+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
4699+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4700+ * GNU General Public License for more details.
4701+ *
4702+ * You should have received a copy of the GNU General Public License
4703+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
4704+ */
4705+
4706+#pragma once
4707+
4708+void write_sriov_conf_finish(const char* rootdir);
4709+void cleanup_sriov_conf(const char* rootdir);
4710diff --git a/src/util.c b/src/util.c
4711index 4b71ef1..edd2969 100644
4712--- a/src/util.c
4713+++ b/src/util.c
4714@@ -24,6 +24,9 @@
4715
4716 #include "util.h"
4717
4718+GHashTable* wifi_frequency_24;
4719+GHashTable* wifi_frequency_5;
4720+
4721 /**
4722 * Create the parent directories of given file path. Exit program on failure.
4723 */
4724@@ -148,3 +151,29 @@ wifi_get_freq5(int channel)
4725 return GPOINTER_TO_INT(g_hash_table_lookup(wifi_frequency_5,
4726 GINT_TO_POINTER(channel)));
4727 }
4728+
4729+/**
4730+ * Systemd-escape the given string. The caller is responsible for freeing
4731+ * the allocated escaped string.
4732+ */
4733+gchar*
4734+systemd_escape(char* string)
4735+{
4736+ g_autoptr(GError) err = NULL;
4737+ g_autofree gchar* stderrh = NULL;
4738+ gint exit_status = 0;
4739+ gchar *escaped;
4740+
4741+ gchar *argv[] = {"bin" "/" "systemd-escape", string, NULL};
4742+ g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err);
4743+ g_spawn_check_exit_status(exit_status, &err);
4744+ if (err != NULL) {
4745+ // LCOV_EXCL_START
4746+ g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh);
4747+ exit(1);
4748+ // LCOV_EXCL_STOP
4749+ }
4750+ g_strstrip(escaped);
4751+
4752+ return escaped;
4753+}
4754diff --git a/src/util.h b/src/util.h
4755index 4ed1a60..ef6671b 100644
4756--- a/src/util.h
4757+++ b/src/util.h
4758@@ -17,8 +17,8 @@
4759
4760 #pragma once
4761
4762-GHashTable* wifi_frequency_24;
4763-GHashTable* wifi_frequency_5;
4764+extern GHashTable* wifi_frequency_24;
4765+extern GHashTable* wifi_frequency_5;
4766
4767 void safe_mkdir_p_dir(const char* file_path);
4768 void g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix);
4769@@ -26,3 +26,7 @@ void unlink_glob(const char* rootdir, const char* _glob);
4770
4771 int wifi_get_freq24(int channel);
4772 int wifi_get_freq5(int channel);
4773+
4774+gchar* systemd_escape(char* string);
4775+
4776+#define OPENVSWITCH_OVS_VSCTL "/usr/bin/ovs-vsctl"
4777diff --git a/src/validation.c b/src/validation.c
4778index ad08988..12a0231 100644
4779--- a/src/validation.c
4780+++ b/src/validation.c
4781@@ -25,6 +25,7 @@
4782
4783 #include "parse.h"
4784 #include "error.h"
4785+#include "util.h"
4786
4787
4788 /* Check sanity for address types */
4789@@ -57,15 +58,148 @@ is_ip6_address(const char* address)
4790 return FALSE;
4791 }
4792
4793+gboolean
4794+is_hostname(const char *hostname)
4795+{
4796+ static const gchar *pattern = "^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$";
4797+ return g_regex_match_simple(pattern, hostname, G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY);
4798+}
4799+
4800+gboolean
4801+is_wireguard_key(const char* key)
4802+{
4803+ /* Check if this is (most likely) a 265bit, base64 encoded wireguard key */
4804+ if (strlen(key) == 44 && key[43] == '=' && key[42] != '=') {
4805+ static const gchar *pattern = "^(?:[A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=)+$";
4806+ return g_regex_match_simple(pattern, key, 0, G_REGEX_MATCH_NOTEMPTY);
4807+ }
4808+ return FALSE;
4809+}
4810+
4811+/* Check sanity of OpenVSwitch controller targets */
4812+gboolean
4813+validate_ovs_target(gboolean host_first, gchar* s) {
4814+ static guint dport = 6653; // the default port
4815+ g_autofree gchar* host = NULL;
4816+ g_autofree gchar* port = NULL;
4817+ gchar** vec = NULL;
4818+
4819+ /* Format tcp:host[:port] or ssl:host[:port] */
4820+ if (host_first) {
4821+ g_assert(s != NULL);
4822+ // IP6 host, indicated by bracketed notation ([..IPv6..])
4823+ if (s[0] == '[') {
4824+ gchar* tmp = NULL;
4825+ tmp = s+1; //get rid of leading '['
4826+ // append default port to unify parsing
4827+ if (!g_strrstr(tmp, "]:"))
4828+ vec = g_strsplit(g_strdup_printf("%s:%u", tmp, dport), "]:", 2);
4829+ else
4830+ vec = g_strsplit(tmp, "]:", 2);
4831+ // IP4 host
4832+ } else {
4833+ // append default port to unify parsing
4834+ if (!g_strrstr(s, ":"))
4835+ vec = g_strsplit(g_strdup_printf("%s:%u", s, dport), ":", 2);
4836+ else
4837+ vec = g_strsplit(s, ":", 2);
4838+ }
4839+ // host and port are always set
4840+ host = g_strdup(vec[0]); //set host alias
4841+ port = g_strdup(vec[1]); //set port alias
4842+ g_assert(vec[2] == NULL);
4843+ g_strfreev(vec);
4844+ /* Format ptcp:[port][:host] or pssl:[port][:host] */
4845+ } else {
4846+ // special case: "ptcp:" (no port, no host)
4847+ if (!g_strcmp0(s, ""))
4848+ port = g_strdup_printf("%u", dport);
4849+ else {
4850+ vec = g_strsplit(s, ":", 2);
4851+ port = g_strdup(vec[0]);
4852+ host = g_strdup(vec[1]);
4853+ // get rid of leading & trailing IPv6 brackets
4854+ if (host && host[0] == '[') {
4855+ char **split = g_strsplit_set(host, "[]", 3);
4856+ g_free(host);
4857+ host = g_strjoinv("", split);
4858+ g_strfreev(split);
4859+ }
4860+ g_strfreev(vec);
4861+ }
4862+ }
4863+
4864+ g_assert(port != NULL);
4865+ // special case where IPv6 notation contains '%iface' name
4866+ if (host && g_strrstr(host, "%")) {
4867+ gchar** split = g_strsplit (host, "%", 2);
4868+ g_free(host);
4869+ host = g_strdup(split[0]); // designated scope for IPv6 link-level addresses
4870+ g_assert(split[1] != NULL && split[2] == NULL);
4871+ g_strfreev(split);
4872+ }
4873+
4874+ if (atoi(port) > 0 && atoi(port) <= 65535) {
4875+ if (!host)
4876+ return TRUE;
4877+ else if (host && (is_ip4_address(host) || is_ip6_address(host)))
4878+ return TRUE;
4879+ }
4880+ return FALSE;
4881+}
4882+
4883 /************************************************
4884 * Validation for grammar and backend rules.
4885 ************************************************/
4886 static gboolean
4887+validate_tunnel_key(yaml_node_t* node, gchar* key, GError** error)
4888+{
4889+ /* Tunnel key should be a number or dotted quad, except for wireguard. */
4890+ gchar* endptr;
4891+ guint64 v = g_ascii_strtoull(key, &endptr, 10);
4892+ if (*endptr != '\0' || v > G_MAXUINT) {
4893+ /* Not a simple uint, try for a dotted quad */
4894+ if (!is_ip4_address(key))
4895+ return yaml_error(node, error, "invalid tunnel key '%s'", key);
4896+ }
4897+ return TRUE;
4898+}
4899+
4900+static gboolean
4901 validate_tunnel_grammar(NetplanNetDefinition* nd, yaml_node_t* node, GError** error)
4902 {
4903 if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_UNKNOWN)
4904 return yaml_error(node, error, "%s: missing 'mode' property for tunnel", nd->id);
4905
4906+ if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) {
4907+ if (!nd->tunnel.private_key)
4908+ return yaml_error(node, error, "%s: missing 'key' property (private key) for wireguard", nd->id);
4909+ if (nd->tunnel.private_key[0] != '/' && !is_wireguard_key(nd->tunnel.private_key))
4910+ return yaml_error(node, error, "%s: invalid wireguard private key", nd->id);
4911+ if (!nd->wireguard_peers || nd->wireguard_peers->len == 0)
4912+ return yaml_error(node, error, "%s: at least one peer is required.", nd->id);
4913+ for (guint i = 0; i < nd->wireguard_peers->len; i++) {
4914+ NetplanWireguardPeer *peer = g_array_index (nd->wireguard_peers, NetplanWireguardPeer*, i);
4915+
4916+ if (!peer->public_key)
4917+ return yaml_error(node, error, "%s: keys.public is required.", nd->id);
4918+ if (!is_wireguard_key(peer->public_key))
4919+ return yaml_error(node, error, "%s: invalid wireguard public key", nd->id);
4920+ if (peer->preshared_key && peer->preshared_key[0] != '/' && !is_wireguard_key(peer->preshared_key))
4921+ return yaml_error(node, error, "%s: invalid wireguard shared key", nd->id);
4922+ if (!peer->allowed_ips || peer->allowed_ips->len == 0)
4923+ return yaml_error(node, error, "%s: 'to' is required to define the allowed IPs.", nd->id);
4924+ if (peer->keepalive > 65535)
4925+ return yaml_error(node, error, "%s: keepalive must be 0-65535 inclusive.", nd->id);
4926+ }
4927+ return TRUE;
4928+ } else {
4929+ if (nd->tunnel.input_key && !validate_tunnel_key(node, nd->tunnel.input_key, error))
4930+ return FALSE;
4931+ if (nd->tunnel.output_key && !validate_tunnel_key(node, nd->tunnel.output_key, error))
4932+ return FALSE;
4933+ }
4934+
4935 /* Validate local/remote IPs */
4936 if (!nd->tunnel.local_ip)
4937 return yaml_error(node, error, "%s: missing 'local' property for tunnel", nd->id);
4938@@ -104,6 +238,7 @@ validate_tunnel_backend_rules(NetplanNetDefinition* nd, yaml_node_t* node, GErro
4939 switch (nd->tunnel.mode) {
4940 case NETPLAN_TUNNEL_MODE_VTI:
4941 case NETPLAN_TUNNEL_MODE_VTI6:
4942+ case NETPLAN_TUNNEL_MODE_WIREGUARD:
4943 break;
4944
4945 /* TODO: Remove this exception and fix ISATAP handling with the
4946@@ -112,9 +247,9 @@ validate_tunnel_backend_rules(NetplanNetDefinition* nd, yaml_node_t* node, GErro
4947 */
4948 case NETPLAN_TUNNEL_MODE_ISATAP:
4949 return yaml_error(node, error,
4950- "%s: %s tunnel mode is not supported by networkd",
4951- nd->id,
4952- g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1));
4953+ "%s: %s tunnel mode is not supported by networkd",
4954+ nd->id,
4955+ g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1));
4956 break;
4957
4958 default:
4959@@ -130,14 +265,15 @@ validate_tunnel_backend_rules(NetplanNetDefinition* nd, yaml_node_t* node, GErro
4960 switch (nd->tunnel.mode) {
4961 case NETPLAN_TUNNEL_MODE_GRE:
4962 case NETPLAN_TUNNEL_MODE_IP6GRE:
4963+ case NETPLAN_TUNNEL_MODE_WIREGUARD:
4964 break;
4965
4966 case NETPLAN_TUNNEL_MODE_GRETAP:
4967 case NETPLAN_TUNNEL_MODE_IP6GRETAP:
4968 return yaml_error(node, error,
4969- "%s: %s tunnel mode is not supported by NetworkManager",
4970- nd->id,
4971- g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1));
4972+ "%s: %s tunnel mode is not supported by NetworkManager",
4973+ nd->id,
4974+ g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1));
4975 break;
4976
4977 default:
4978@@ -149,10 +285,7 @@ validate_tunnel_backend_rules(NetplanNetDefinition* nd, yaml_node_t* node, GErro
4979 }
4980 break;
4981
4982- // LCOV_EXCL_START
4983- default:
4984- break;
4985- // LCOV_EXCL_STOP
4986+ default: break; //LCOV_EXCL_LINE
4987 }
4988
4989 return TRUE;
4990@@ -195,6 +328,18 @@ validate_netdef_grammar(NetplanNetDefinition* nd, yaml_node_t* node, GError** er
4991 goto netdef_grammar_error;
4992 }
4993
4994+ if (nd->ip6_addr_gen_mode != NETPLAN_ADDRGEN_DEFAULT && nd->ip6_addr_gen_token)
4995+ return yaml_error(node, error, "%s: ipv6-address-generation and ipv6-address-token are mutually exclusive", nd->id);
4996+
4997+ if (nd->backend == NETPLAN_BACKEND_OVS) {
4998+ // LCOV_EXCL_START
4999+ if (!g_file_test(OPENVSWITCH_OVS_VSCTL, G_FILE_TEST_EXISTS)) {
5000+ /* Tested via integration test */
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches