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

Proposed by Lukas Märdian
Status: Merged
Approved by: Łukasz Zemczak
Approved revision: 7b74153bb50509435eaeea31a105edc2875d7adc
Merged at revision: 7b74153bb50509435eaeea31a105edc2875d7adc
Proposed branch: ~slyon/netplan/+git/ubuntu:slyon/ubuntu/focal-0100
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu/focal
Diff against target: 9043 lines (+6250/-549)
55 files modified
Makefile (+3/-2)
debian/changelog (+69/-0)
debian/control (+1/-0)
debian/libnetplan0.symbols (+7/-0)
debian/patches/0001-Implement-just-in-time-behaviour-for-generate-162.patch (+296/-0)
debian/patches/series (+1/-2)
debian/rules (+4/-0)
debian/tests/autostart (+9/-5)
debian/tests/cloud-init (+115/-0)
debian/tests/control (+18/-0)
dev/null (+0/-34)
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+391145@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Lukas Märdian (slyon) wrote :

I had to remove the Build-Dep on 'openvswitch-switch' for riscv64, due to that package (and thus the ovs-vsctl tool) missing in Focal. Therefore, OVS tests wouldn't pass on riscv64, but are disabled/ignored as well.

Revision history for this message
Łukasz Zemczak (sil2100) wrote :

Ok, glimpsing through the diff the changes seem fine. It's a bit sad that we had to disable the whole test suit on riscv64, but then again - we don't really care about the test situation for that arch!

So I'm all +1 on this. Let me merge it and sponsor.

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

Subscribers

People subscribed via source and target branches