]> git.ipfire.org Git - thirdparty/systemd.git/blame - test/networkd-test.py
networkd: bond support primary slave and active slave (#4873)
[thirdparty/systemd.git] / test / networkd-test.py
CommitLineData
4ddb85b1
MP
1#!/usr/bin/env python3
2#
3# networkd integration test
4# This uses temporary configuration in /run and temporary veth devices, and
5# does not write anything on disk or change any system configuration;
6# but it assumes (and checks at the beginning) that networkd is not currently
7# running.
daad34df
MP
8#
9# This can be run on a normal installation, in QEMU, nspawn (with
10# --private-network), LXD (with "--config raw.lxc=lxc.aa_profile=unconfined"),
11# or LXC system containers. You need at least the "ip" tool from the iproute
12# package; it is recommended to install dnsmasq too to get full test coverage.
13#
4ddb85b1
MP
14# ATTENTION: This uses the *installed* networkd, not the one from the built
15# source tree.
16#
17# (C) 2015 Canonical Ltd.
18# Author: Martin Pitt <martin.pitt@ubuntu.com>
19#
20# systemd is free software; you can redistribute it and/or modify it
21# under the terms of the GNU Lesser General Public License as published by
22# the Free Software Foundation; either version 2.1 of the License, or
23# (at your option) any later version.
24
25# systemd is distributed in the hope that it will be useful, but
26# WITHOUT ANY WARRANTY; without even the implied warranty of
27# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
28# Lesser General Public License for more details.
29#
30# You should have received a copy of the GNU Lesser General Public License
31# along with systemd; If not, see <http://www.gnu.org/licenses/>.
32
ec89276c 33import errno
4ddb85b1
MP
34import os
35import sys
36import time
37import unittest
38import tempfile
39import subprocess
40import shutil
89748b0a 41import socket
4ddb85b1 42
ec89276c
DM
43HAVE_DNSMASQ = shutil.which('dnsmasq') is not None
44
45NETWORK_UNITDIR = '/run/systemd/network'
46
47NETWORKD_WAIT_ONLINE = shutil.which('systemd-networkd-wait-online',
48 path='/usr/lib/systemd:/lib/systemd')
4ddb85b1 49
30b42a9a
MP
50RESOLV_CONF = '/run/systemd/resolve/resolv.conf'
51
4ddb85b1 52
ec89276c
DM
53def setUpModule():
54 """Initialize the environment, and perform sanity checks on it."""
55 if NETWORKD_WAIT_ONLINE is None:
56 raise OSError(errno.ENOENT, 'systemd-networkd-wait-online not found')
57
58 # Do not run any tests if the system is using networkd already.
59 if subprocess.call(['systemctl', 'is-active', '--quiet',
60 'systemd-networkd.service']) == 0:
61 raise unittest.SkipTest('networkd is already active')
62
63 # Avoid "Failed to open /dev/tty" errors in containers.
64 os.environ['SYSTEMD_LOG_TARGET'] = 'journal'
65
66 # Ensure the unit directory exists so tests can dump files into it.
67 os.makedirs(NETWORK_UNITDIR, exist_ok=True)
68
69
70class NetworkdTestingUtilities:
71 """Provide a set of utility functions to facilitate networkd tests.
72
73 This class must be inherited along with unittest.TestCase to define
74 some required methods.
75 """
76
618b196e
DM
77 def add_veth_pair(self, veth, peer, veth_options=(), peer_options=()):
78 """Add a veth interface pair, and queue them to be removed."""
79 subprocess.check_call(['ip', 'link', 'add', 'name', veth] +
80 list(veth_options) +
81 ['type', 'veth', 'peer', 'name', peer] +
82 list(peer_options))
83 self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'dev', peer])
84
ec89276c
DM
85 def write_network(self, unit_name, contents):
86 """Write a network unit file, and queue it to be removed."""
87 unit_path = os.path.join(NETWORK_UNITDIR, unit_name)
88
89 with open(unit_path, 'w') as unit:
90 unit.write(contents)
91 self.addCleanup(os.remove, unit_path)
92
93 def write_network_dropin(self, unit_name, dropin_name, contents):
94 """Write a network unit drop-in, and queue it to be removed."""
95 dropin_dir = os.path.join(NETWORK_UNITDIR, "%s.d" % unit_name)
96 dropin_path = os.path.join(dropin_dir, "%s.conf" % dropin_name)
97
98 os.makedirs(dropin_dir, exist_ok=True)
99 with open(dropin_path, 'w') as dropin:
100 dropin.write(contents)
101 self.addCleanup(os.remove, dropin_path)
102
a09dc546
DM
103 def assert_link_states(self, **kwargs):
104 """Match networkctl link states to the given ones.
105
106 Each keyword argument should be the name of a network interface
107 with its expected value of the "SETUP" column in output from
108 networkctl. The interfaces have five seconds to come online
109 before the check is performed. Every specified interface must
110 be present in the output, and any other interfaces found in the
111 output are ignored.
112
113 A special interface state "managed" is supported, which matches
114 any value in the "SETUP" column other than "unmanaged".
115 """
116 if not kwargs:
117 return
118 interfaces = set(kwargs)
119
120 # Wait for the requested interfaces, but don't fail for them.
121 subprocess.call([NETWORKD_WAIT_ONLINE, '--timeout=5'] +
122 ['--interface=%s' % iface for iface in kwargs])
123
124 # Validate each link state found in the networkctl output.
125 out = subprocess.check_output(['networkctl', '--no-legend']).rstrip()
126 for line in out.decode('utf-8').split('\n'):
127 fields = line.split()
128 if len(fields) >= 5 and fields[1] in kwargs:
129 iface = fields[1]
130 expected = kwargs[iface]
131 actual = fields[-1]
132 if (actual != expected and
133 not (expected == 'managed' and actual != 'unmanaged')):
134 self.fail("Link %s expects state %s, found %s" %
135 (iface, expected, actual))
136 interfaces.remove(iface)
137
138 # Ensure that all requested interfaces have been covered.
139 if interfaces:
140 self.fail("Missing links in status output: %s" % interfaces)
141
ec89276c
DM
142
143class ClientTestBase(NetworkdTestingUtilities):
144 """Provide common methods for testing networkd against servers."""
145
fd0cec03
MP
146 @classmethod
147 def setUpClass(klass):
148 klass.orig_log_level = subprocess.check_output(
149 ['systemctl', 'show', '--value', '--property', 'LogLevel'],
150 universal_newlines=True).strip()
151 subprocess.check_call(['systemd-analyze', 'set-log-level', 'debug'])
152
153 @classmethod
154 def tearDownClass(klass):
155 subprocess.check_call(['systemd-analyze', 'set-log-level', klass.orig_log_level])
156
4ddb85b1
MP
157 def setUp(self):
158 self.iface = 'test_eth42'
159 self.if_router = 'router_eth42'
160 self.workdir_obj = tempfile.TemporaryDirectory()
161 self.workdir = self.workdir_obj.name
ec89276c 162 self.config = 'test_eth42.network'
4ddb85b1
MP
163
164 # get current journal cursor
fd0cec03 165 subprocess.check_output(['journalctl', '--sync'])
4ddb85b1
MP
166 out = subprocess.check_output(['journalctl', '-b', '--quiet',
167 '--no-pager', '-n0', '--show-cursor'],
168 universal_newlines=True)
169 self.assertTrue(out.startswith('-- cursor:'))
170 self.journal_cursor = out.split()[-1]
171
172 def tearDown(self):
173 self.shutdown_iface()
4ddb85b1 174 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
9e0c296a
MP
175 subprocess.call(['ip', 'link', 'del', 'dummy0'],
176 stderr=subprocess.DEVNULL)
4ddb85b1
MP
177
178 def show_journal(self, unit):
179 '''Show journal of given unit since start of the test'''
180
181 print('---- %s ----' % unit)
fd0cec03 182 subprocess.check_output(['journalctl', '--sync'])
4ddb85b1
MP
183 sys.stdout.flush()
184 subprocess.call(['journalctl', '-b', '--no-pager', '--quiet',
185 '--cursor', self.journal_cursor, '-u', unit])
186
187 def create_iface(self, ipv6=False):
188 '''Create test interface with DHCP server behind it'''
189
190 raise NotImplementedError('must be implemented by a subclass')
191
192 def shutdown_iface(self):
193 '''Remove test interface and stop DHCP server'''
194
195 raise NotImplementedError('must be implemented by a subclass')
196
197 def print_server_log(self):
198 '''Print DHCP server log for debugging failures'''
199
200 raise NotImplementedError('must be implemented by a subclass')
201
202 def do_test(self, coldplug=True, ipv6=False, extra_opts='',
203 online_timeout=10, dhcp_mode='yes'):
30b42a9a 204 subprocess.check_call(['systemctl', 'start', 'systemd-resolved'])
ec89276c 205 self.write_network(self.config, '''\
38d78d1e 206[Match]
4ddb85b1
MP
207Name=%s
208[Network]
209DHCP=%s
210%s''' % (self.iface, dhcp_mode, extra_opts))
211
212 if coldplug:
213 # create interface first, then start networkd
214 self.create_iface(ipv6=ipv6)
215 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
e8c0de91 216 elif coldplug is not None:
4ddb85b1
MP
217 # start networkd first, then create interface
218 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
219 self.create_iface(ipv6=ipv6)
e8c0de91
MP
220 else:
221 # "None" means test sets up interface by itself
222 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
4ddb85b1
MP
223
224 try:
ec89276c 225 subprocess.check_call([NETWORKD_WAIT_ONLINE, '--interface',
4ddb85b1
MP
226 self.iface, '--timeout=%i' % online_timeout])
227
228 if ipv6:
229 # check iface state and IP 6 address; FIXME: we need to wait a bit
230 # longer, as the iface is "configured" already with IPv4 *or*
231 # IPv6, but we want to wait for both
00d5eaaf 232 for _ in range(10):
4ddb85b1
MP
233 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.iface])
234 if b'state UP' in out and b'inet6 2600' in out and b'inet 192.168' in out:
235 break
236 time.sleep(1)
237 else:
238 self.fail('timed out waiting for IPv6 configuration')
239
240 self.assertRegex(out, b'inet6 2600::.* scope global .*dynamic')
241 self.assertRegex(out, b'inet6 fe80::.* scope link')
242 else:
243 # should have link-local address on IPv6 only
244 out = subprocess.check_output(['ip', '-6', 'a', 'show', 'dev', self.iface])
cda39975 245 self.assertRegex(out, br'inet6 fe80::.* scope link')
4ddb85b1
MP
246 self.assertNotIn(b'scope global', out)
247
248 # should have IPv4 address
249 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
250 self.assertIn(b'state UP', out)
cda39975 251 self.assertRegex(out, br'inet 192.168.5.\d+/.* scope global dynamic')
4ddb85b1
MP
252
253 # check networkctl state
254 out = subprocess.check_output(['networkctl'])
cda39975
ZJS
255 self.assertRegex(out, (r'%s\s+ether\s+routable\s+unmanaged' % self.if_router).encode())
256 self.assertRegex(out, (r'%s\s+ether\s+routable\s+configured' % self.iface).encode())
4ddb85b1
MP
257
258 out = subprocess.check_output(['networkctl', 'status', self.iface])
cda39975
ZJS
259 self.assertRegex(out, br'Type:\s+ether')
260 self.assertRegex(out, br'State:\s+routable.*configured')
261 self.assertRegex(out, br'Address:\s+192.168.5.\d+')
4ddb85b1 262 if ipv6:
cda39975 263 self.assertRegex(out, br'2600::')
4ddb85b1 264 else:
cda39975
ZJS
265 self.assertNotIn(br'2600::', out)
266 self.assertRegex(out, br'fe80::')
267 self.assertRegex(out, br'Gateway:\s+192.168.5.1')
268 self.assertRegex(out, br'DNS:\s+192.168.5.1')
4ddb85b1
MP
269 except (AssertionError, subprocess.CalledProcessError):
270 # show networkd status, journal, and DHCP server log on failure
ec89276c 271 with open(os.path.join(NETWORK_UNITDIR, self.config)) as f:
4ddb85b1
MP
272 print('\n---- %s ----\n%s' % (self.config, f.read()))
273 print('---- interface status ----')
274 sys.stdout.flush()
275 subprocess.call(['ip', 'a', 'show', 'dev', self.iface])
276 print('---- networkctl status %s ----' % self.iface)
277 sys.stdout.flush()
278 subprocess.call(['networkctl', 'status', self.iface])
279 self.show_journal('systemd-networkd.service')
280 self.print_server_log()
281 raise
282
30b42a9a
MP
283 for timeout in range(50):
284 with open(RESOLV_CONF) as f:
285 contents = f.read()
286 if 'nameserver 192.168.5.1\n' in contents:
287 break
288 time.sleep(0.1)
289 else:
290 self.fail('nameserver 192.168.5.1 not found in ' + RESOLV_CONF)
4ddb85b1 291
e8c0de91 292 if coldplug is False:
4ddb85b1
MP
293 # check post-down.d hook
294 self.shutdown_iface()
295
296 def test_coldplug_dhcp_yes_ip4(self):
297 # we have a 12s timeout on RA, so we need to wait longer
298 self.do_test(coldplug=True, ipv6=False, online_timeout=15)
299
300 def test_coldplug_dhcp_yes_ip4_no_ra(self):
301 # with disabling RA explicitly things should be fast
302 self.do_test(coldplug=True, ipv6=False,
f921f573 303 extra_opts='IPv6AcceptRA=False')
4ddb85b1
MP
304
305 def test_coldplug_dhcp_ip4_only(self):
306 # we have a 12s timeout on RA, so we need to wait longer
307 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
308 online_timeout=15)
309
310 def test_coldplug_dhcp_ip4_only_no_ra(self):
311 # with disabling RA explicitly things should be fast
312 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
f921f573 313 extra_opts='IPv6AcceptRA=False')
4ddb85b1
MP
314
315 def test_coldplug_dhcp_ip6(self):
316 self.do_test(coldplug=True, ipv6=True)
317
318 def test_hotplug_dhcp_ip4(self):
319 # With IPv4 only we have a 12s timeout on RA, so we need to wait longer
320 self.do_test(coldplug=False, ipv6=False, online_timeout=15)
321
322 def test_hotplug_dhcp_ip6(self):
323 self.do_test(coldplug=False, ipv6=True)
324
94363cbb 325 def test_route_only_dns(self):
ec89276c 326 self.write_network('myvpn.netdev', '''\
38d78d1e 327[NetDev]
94363cbb
MP
328Name=dummy0
329Kind=dummy
330MACAddress=12:34:56:78:9a:bc''')
ec89276c 331 self.write_network('myvpn.network', '''\
38d78d1e 332[Match]
94363cbb
MP
333Name=dummy0
334[Network]
335Address=192.168.42.100
336DNS=192.168.42.1
337Domains= ~company''')
94363cbb
MP
338
339 self.do_test(coldplug=True, ipv6=False,
340 extra_opts='IPv6AcceptRouterAdvertisements=False')
341
30b42a9a
MP
342 with open(RESOLV_CONF) as f:
343 contents = f.read()
94363cbb
MP
344 # ~company is not a search domain, only a routing domain
345 self.assertNotRegex(contents, 'search.*company')
30b42a9a
MP
346 # our global server should appear
347 self.assertIn('nameserver 192.168.5.1\n', contents)
b9fe94ca
MP
348 # should not have domain-restricted server as global server
349 self.assertNotIn('nameserver 192.168.42.1\n', contents)
350
351 def test_route_only_dns_all_domains(self):
ec89276c 352 self.write_network('myvpn.netdev', '''[NetDev]
b9fe94ca
MP
353Name=dummy0
354Kind=dummy
355MACAddress=12:34:56:78:9a:bc''')
ec89276c 356 self.write_network('myvpn.network', '''[Match]
b9fe94ca
MP
357Name=dummy0
358[Network]
359Address=192.168.42.100
360DNS=192.168.42.1
361Domains= ~company ~.''')
b9fe94ca
MP
362
363 self.do_test(coldplug=True, ipv6=False,
364 extra_opts='IPv6AcceptRouterAdvertisements=False')
365
366 with open(RESOLV_CONF) as f:
367 contents = f.read()
368
369 # ~company is not a search domain, only a routing domain
370 self.assertNotRegex(contents, 'search.*company')
371
372 # our global server should appear
373 self.assertIn('nameserver 192.168.5.1\n', contents)
374 # should have company server as global server due to ~.
375 self.assertIn('nameserver 192.168.42.1\n', contents)
94363cbb 376
4ddb85b1 377
ec89276c 378@unittest.skipUnless(HAVE_DNSMASQ, 'dnsmasq not installed')
4ddb85b1
MP
379class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
380 '''Test networkd client against dnsmasq'''
381
382 def setUp(self):
383 super().setUp()
384 self.dnsmasq = None
e8c0de91 385 self.iface_mac = 'de:ad:be:ef:47:11'
4ddb85b1 386
b9fe94ca 387 def create_iface(self, ipv6=False, dnsmasq_opts=None):
4ddb85b1
MP
388 '''Create test interface with DHCP server behind it'''
389
390 # add veth pair
e8c0de91
MP
391 subprocess.check_call(['ip', 'link', 'add', 'name', self.iface,
392 'address', self.iface_mac,
393 'type', 'veth', 'peer', 'name', self.if_router])
4ddb85b1
MP
394
395 # give our router an IP
396 subprocess.check_call(['ip', 'a', 'flush', 'dev', self.if_router])
397 subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self.if_router])
398 if ipv6:
399 subprocess.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self.if_router])
400 subprocess.check_call(['ip', 'link', 'set', self.if_router, 'up'])
401
402 # add DHCP server
403 self.dnsmasq_log = os.path.join(self.workdir, 'dnsmasq.log')
404 lease_file = os.path.join(self.workdir, 'dnsmasq.leases')
405 if ipv6:
406 extra_opts = ['--enable-ra', '--dhcp-range=2600::10,2600::20']
407 else:
408 extra_opts = []
b9fe94ca
MP
409 if dnsmasq_opts:
410 extra_opts += dnsmasq_opts
4ddb85b1
MP
411 self.dnsmasq = subprocess.Popen(
412 ['dnsmasq', '--keep-in-foreground', '--log-queries',
413 '--log-facility=' + self.dnsmasq_log, '--conf-file=/dev/null',
414 '--dhcp-leasefile=' + lease_file, '--bind-interfaces',
415 '--interface=' + self.if_router, '--except-interface=lo',
416 '--dhcp-range=192.168.5.10,192.168.5.200'] + extra_opts)
417
418 def shutdown_iface(self):
419 '''Remove test interface and stop DHCP server'''
420
421 if self.if_router:
422 subprocess.check_call(['ip', 'link', 'del', 'dev', self.if_router])
423 self.if_router = None
424 if self.dnsmasq:
425 self.dnsmasq.kill()
426 self.dnsmasq.wait()
427 self.dnsmasq = None
428
429 def print_server_log(self):
430 '''Print DHCP server log for debugging failures'''
431
432 with open(self.dnsmasq_log) as f:
433 sys.stdout.write('\n\n---- dnsmasq log ----\n%s\n------\n\n' % f.read())
434
b9fe94ca
MP
435 def test_resolved_domain_restricted_dns(self):
436 '''resolved: domain-restricted DNS servers'''
437
438 # create interface for generic connections; this will map all DNS names
439 # to 192.168.42.1
440 self.create_iface(dnsmasq_opts=['--address=/#/192.168.42.1'])
ec89276c 441 self.write_network('general.network', '''\
b9fe94ca
MP
442[Match]
443Name=%s
444[Network]
445DHCP=ipv4
446IPv6AcceptRA=False''' % self.iface)
447
448 # create second device/dnsmasq for a .company/.lab VPN interface
449 # static IPs for simplicity
618b196e 450 self.add_veth_pair('testvpnclient', 'testvpnrouter')
b9fe94ca
MP
451 subprocess.check_call(['ip', 'a', 'flush', 'dev', 'testvpnrouter'])
452 subprocess.check_call(['ip', 'a', 'add', '10.241.3.1/24', 'dev', 'testvpnrouter'])
453 subprocess.check_call(['ip', 'link', 'set', 'testvpnrouter', 'up'])
454
455 vpn_dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-vpn.log')
456 vpn_dnsmasq = subprocess.Popen(
457 ['dnsmasq', '--keep-in-foreground', '--log-queries',
458 '--log-facility=' + vpn_dnsmasq_log, '--conf-file=/dev/null',
459 '--dhcp-leasefile=/dev/null', '--bind-interfaces',
460 '--interface=testvpnrouter', '--except-interface=lo',
461 '--address=/math.lab/10.241.3.3', '--address=/cantina.company/10.241.4.4'])
462 self.addCleanup(vpn_dnsmasq.wait)
463 self.addCleanup(vpn_dnsmasq.kill)
464
ec89276c 465 self.write_network('vpn.network', '''\
b9fe94ca
MP
466[Match]
467Name=testvpnclient
468[Network]
469IPv6AcceptRA=False
470Address=10.241.3.2/24
471DNS=10.241.3.1
472Domains= ~company ~lab''')
473
474 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
ec89276c 475 subprocess.check_call([NETWORKD_WAIT_ONLINE, '--interface', self.iface,
b9fe94ca
MP
476 '--interface=testvpnclient', '--timeout=20'])
477
478 # ensure we start fresh with every test
479 subprocess.check_call(['systemctl', 'restart', 'systemd-resolved'])
480
481 # test vpnclient specific domains; these should *not* be answered by
482 # the general DNS
483 out = subprocess.check_output(['systemd-resolve', 'math.lab'])
484 self.assertIn(b'math.lab: 10.241.3.3', out)
485 out = subprocess.check_output(['systemd-resolve', 'kettle.cantina.company'])
486 self.assertIn(b'kettle.cantina.company: 10.241.4.4', out)
487
488 # test general domains
489 out = subprocess.check_output(['systemd-resolve', 'megasearch.net'])
490 self.assertIn(b'megasearch.net: 192.168.42.1', out)
491
492 with open(self.dnsmasq_log) as f:
493 general_log = f.read()
494 with open(vpn_dnsmasq_log) as f:
495 vpn_log = f.read()
496
497 # VPN domains should only be sent to VPN DNS
498 self.assertRegex(vpn_log, 'query.*math.lab')
499 self.assertRegex(vpn_log, 'query.*cantina.company')
500 self.assertNotIn('lab', general_log)
501 self.assertNotIn('company', general_log)
502
503 # general domains should not be sent to the VPN DNS
504 self.assertRegex(general_log, 'query.*megasearch.net')
505 self.assertNotIn('megasearch.net', vpn_log)
506
e8c0de91
MP
507 def test_transient_hostname(self):
508 '''networkd sets transient hostname from DHCP'''
509
89748b0a
MP
510 orig_hostname = socket.gethostname()
511 self.addCleanup(socket.sethostname, orig_hostname)
512 # temporarily move /etc/hostname away; restart hostnamed to pick it up
513 if os.path.exists('/etc/hostname'):
514 subprocess.check_call(['mount', '--bind', '/dev/null', '/etc/hostname'])
515 self.addCleanup(subprocess.call, ['umount', '/etc/hostname'])
516 subprocess.check_call(['systemctl', 'stop', 'systemd-hostnamed.service'])
517
e8c0de91
MP
518 self.create_iface(dnsmasq_opts=['--dhcp-host=%s,192.168.5.210,testgreen' % self.iface_mac])
519 self.do_test(coldplug=None, extra_opts='IPv6AcceptRA=False', dhcp_mode='ipv4')
520
fd0cec03
MP
521 try:
522 # should have received the fixed IP above
523 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
524 self.assertRegex(out, b'inet 192.168.5.210/24 .* scope global dynamic')
2926b130
MP
525 # should have set transient hostname in hostnamed; this is
526 # sometimes a bit lagging (issue #4753), so retry a few times
527 for retry in range(1, 6):
528 out = subprocess.check_output(['hostnamectl'])
529 if b'testgreen' in out:
530 break
531 time.sleep(5)
532 sys.stdout.write('[retry %i] ' % retry)
533 sys.stdout.flush()
534 else:
535 self.fail('Transient hostname not found in hostnamectl:\n%s' % out.decode())
fd0cec03
MP
536 # and also applied to the system
537 self.assertEqual(socket.gethostname(), 'testgreen')
538 except AssertionError:
539 self.show_journal('systemd-networkd.service')
540 self.show_journal('systemd-hostnamed.service')
541 self.print_server_log()
542 raise
89748b0a
MP
543
544 def test_transient_hostname_with_static(self):
545 '''transient hostname is not applied if static hostname exists'''
546
547 orig_hostname = socket.gethostname()
548 self.addCleanup(socket.sethostname, orig_hostname)
549 if not os.path.exists('/etc/hostname'):
550 self.writeConfig('/etc/hostname', orig_hostname)
551 subprocess.check_call(['systemctl', 'stop', 'systemd-hostnamed.service'])
552
553 self.create_iface(dnsmasq_opts=['--dhcp-host=%s,192.168.5.210,testgreen' % self.iface_mac])
554 self.do_test(coldplug=None, extra_opts='IPv6AcceptRA=False', dhcp_mode='ipv4')
555
fd0cec03
MP
556 try:
557 # should have received the fixed IP above
558 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
559 self.assertRegex(out, b'inet 192.168.5.210/24 .* scope global dynamic')
560 # static hostname wins over transient one, thus *not* applied
561 self.assertEqual(socket.gethostname(), orig_hostname)
562 except AssertionError:
563 self.show_journal('systemd-networkd.service')
564 self.show_journal('systemd-hostnamed.service')
565 self.print_server_log()
566 raise
e8c0de91 567
4ddb85b1
MP
568
569class NetworkdClientTest(ClientTestBase, unittest.TestCase):
570 '''Test networkd client against networkd server'''
571
572 def setUp(self):
573 super().setUp()
574 self.dnsmasq = None
575
2c99aba7 576 def create_iface(self, ipv6=False, dhcpserver_opts=None):
4ddb85b1
MP
577 '''Create test interface with DHCP server behind it'''
578
579 # run "router-side" networkd in own mount namespace to shield it from
580 # "client-side" configuration and networkd
581 (fd, script) = tempfile.mkstemp(prefix='networkd-router.sh')
582 self.addCleanup(os.remove, script)
583 with os.fdopen(fd, 'w+') as f:
38d78d1e
ZJS
584 f.write('''\
585#!/bin/sh -eu
4ddb85b1
MP
586mkdir -p /run/systemd/network
587mkdir -p /run/systemd/netif
588mount -t tmpfs none /run/systemd/network
589mount -t tmpfs none /run/systemd/netif
590[ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus
591# create router/client veth pair
592cat << EOF > /run/systemd/network/test.netdev
593[NetDev]
594Name=%(ifr)s
595Kind=veth
596
597[Peer]
598Name=%(ifc)s
599EOF
600
601cat << EOF > /run/systemd/network/test.network
602[Match]
603Name=%(ifr)s
604
605[Network]
606Address=192.168.5.1/24
607%(addr6)s
608DHCPServer=yes
609
610[DHCPServer]
611PoolOffset=10
612PoolSize=50
613DNS=192.168.5.1
2c99aba7 614%(dhopts)s
4ddb85b1
MP
615EOF
616
617# run networkd as in systemd-networkd.service
618exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//; p}')
2c99aba7
MP
619''' % {'ifr': self.if_router, 'ifc': self.iface, 'addr6': ipv6 and 'Address=2600::1/64' or '',
620 'dhopts': dhcpserver_opts or ''})
4ddb85b1
MP
621
622 os.fchmod(fd, 0o755)
623
624 subprocess.check_call(['systemd-run', '--unit=networkd-test-router.service',
625 '-p', 'InaccessibleDirectories=-/etc/systemd/network',
626 '-p', 'InaccessibleDirectories=-/run/systemd/network',
627 '-p', 'InaccessibleDirectories=-/run/systemd/netif',
628 '--service-type=notify', script])
629
630 # wait until devices got created
00d5eaaf 631 for _ in range(50):
4ddb85b1
MP
632 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.if_router])
633 if b'state UP' in out and b'scope global' in out:
634 break
635 time.sleep(0.1)
636
637 def shutdown_iface(self):
638 '''Remove test interface and stop DHCP server'''
639
640 if self.if_router:
641 subprocess.check_call(['systemctl', 'stop', 'networkd-test-router.service'])
642 # ensure failed transient unit does not stay around
643 subprocess.call(['systemctl', 'reset-failed', 'networkd-test-router.service'])
644 subprocess.call(['ip', 'link', 'del', 'dev', self.if_router])
645 self.if_router = None
646
647 def print_server_log(self):
648 '''Print DHCP server log for debugging failures'''
649
650 self.show_journal('networkd-test-router.service')
651
652 @unittest.skip('networkd does not have DHCPv6 server support')
653 def test_hotplug_dhcp_ip6(self):
654 pass
655
656 @unittest.skip('networkd does not have DHCPv6 server support')
657 def test_coldplug_dhcp_ip6(self):
658 pass
659
d2bc1251
MP
660 def test_search_domains(self):
661
662 # we don't use this interface for this test
663 self.if_router = None
664
ec89276c 665 self.write_network('test.netdev', '''\
38d78d1e 666[NetDev]
d2bc1251
MP
667Name=dummy0
668Kind=dummy
669MACAddress=12:34:56:78:9a:bc''')
ec89276c 670 self.write_network('test.network', '''\
38d78d1e 671[Match]
d2bc1251
MP
672Name=dummy0
673[Network]
674Address=192.168.42.100
675DNS=192.168.42.1
676Domains= one two three four five six seven eight nine ten''')
d2bc1251
MP
677
678 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
679
30b42a9a
MP
680 for timeout in range(50):
681 with open(RESOLV_CONF) as f:
682 contents = f.read()
683 if ' one' in contents:
684 break
685 time.sleep(0.1)
686 self.assertRegex(contents, 'search .*one two three four')
687 self.assertNotIn('seven\n', contents)
688 self.assertIn('# Too many search domains configured, remaining ones ignored.\n', contents)
d2bc1251
MP
689
690 def test_search_domains_too_long(self):
691
692 # we don't use this interface for this test
693 self.if_router = None
694
695 name_prefix = 'a' * 60
696
ec89276c 697 self.write_network('test.netdev', '''\
38d78d1e 698[NetDev]
d2bc1251
MP
699Name=dummy0
700Kind=dummy
701MACAddress=12:34:56:78:9a:bc''')
ec89276c 702 self.write_network('test.network', '''\
38d78d1e 703[Match]
d2bc1251
MP
704Name=dummy0
705[Network]
706Address=192.168.42.100
707DNS=192.168.42.1
38d78d1e 708Domains={p}0 {p}1 {p}2 {p}3 {p}4'''.format(p=name_prefix))
d2bc1251
MP
709
710 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
711
30b42a9a
MP
712 for timeout in range(50):
713 with open(RESOLV_CONF) as f:
714 contents = f.read()
715 if ' one' in contents:
716 break
717 time.sleep(0.1)
38d78d1e 718 self.assertRegex(contents, 'search .*{p}0 {p}1 {p}2'.format(p=name_prefix))
30b42a9a 719 self.assertIn('# Total length of all search domains is too long, remaining ones ignored.', contents)
d2bc1251 720
047a0dac
JSB
721 def test_dropin(self):
722 # we don't use this interface for this test
723 self.if_router = None
724
ec89276c 725 self.write_network('test.netdev', '''\
047a0dac
JSB
726[NetDev]
727Name=dummy0
728Kind=dummy
729MACAddress=12:34:56:78:9a:bc''')
ec89276c 730 self.write_network('test.network', '''\
047a0dac
JSB
731[Match]
732Name=dummy0
733[Network]
734Address=192.168.42.100
735DNS=192.168.42.1''')
ec89276c 736 self.write_network_dropin('test.network', 'dns', '''\
047a0dac
JSB
737[Network]
738DNS=127.0.0.1''')
739
740 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
741
742 for timeout in range(50):
743 with open(RESOLV_CONF) as f:
744 contents = f.read()
745 if ' 127.0.0.1' in contents:
746 break
747 time.sleep(0.1)
748 self.assertIn('nameserver 192.168.42.1\n', contents)
749 self.assertIn('nameserver 127.0.0.1\n', contents)
750
2c99aba7
MP
751 def test_dhcp_timezone(self):
752 '''networkd sets time zone from DHCP'''
753
754 def get_tz():
755 out = subprocess.check_output(['busctl', 'get-property', 'org.freedesktop.timedate1',
756 '/org/freedesktop/timedate1', 'org.freedesktop.timedate1', 'Timezone'])
757 assert out.startswith(b's "')
758 out = out.strip()
759 assert out.endswith(b'"')
760 return out[3:-1].decode()
761
762 orig_timezone = get_tz()
763 self.addCleanup(subprocess.call, ['timedatectl', 'set-timezone', orig_timezone])
764
765 self.create_iface(dhcpserver_opts='EmitTimezone=yes\nTimezone=Pacific/Honolulu')
766 self.do_test(coldplug=None, extra_opts='IPv6AcceptRA=false\n[DHCP]\nUseTimezone=true', dhcp_mode='ipv4')
767
768 # should have applied the received timezone
769 try:
770 self.assertEqual(get_tz(), 'Pacific/Honolulu')
771 except AssertionError:
772 self.show_journal('systemd-networkd.service')
773 self.show_journal('systemd-hostnamed.service')
774 raise
775
776
618b196e
DM
777class MatchClientTest(unittest.TestCase, NetworkdTestingUtilities):
778 """Test [Match] sections in .network files.
779
780 Be aware that matching the test host's interfaces will wipe their
781 configuration, so as a precaution, all network files should have a
782 restrictive [Match] section to only ever interfere with the
783 temporary veth interfaces created here.
784 """
785
786 def tearDown(self):
787 """Stop networkd."""
788 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
789
790 def test_basic_matching(self):
791 """Verify the Name= line works throughout this class."""
792 self.add_veth_pair('test_if1', 'fake_if2')
793 self.write_network('test.network', "[Match]\nName=test_*\n[Network]")
794 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
795 self.assert_link_states(test_if1='managed', fake_if2='unmanaged')
796
797 def test_inverted_matching(self):
798 """Verify that a '!'-prefixed value inverts the match."""
799 # Use a MAC address as the interfaces' common matching attribute
800 # to avoid depending on udev, to support testing in containers.
801 mac = '00:01:02:03:98:99'
802 self.add_veth_pair('test_veth', 'test_peer',
803 ['addr', mac], ['addr', mac])
804 self.write_network('no-veth.network', """\
805[Match]
806MACAddress=%s
807Name=!nonexistent *peer*
808[Network]""" % mac)
809 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
810 self.assert_link_states(test_veth='managed', test_peer='unmanaged')
811
812
a09dc546
DM
813class UnmanagedClientTest(unittest.TestCase, NetworkdTestingUtilities):
814 """Test if networkd manages the correct interfaces."""
815
816 def setUp(self):
817 """Write .network files to match the named veth devices."""
818 # Define the veth+peer pairs to be created.
819 # Their pairing doesn't actually matter, only their names do.
820 self.veths = {
821 'm1def': 'm0unm',
822 'm1man': 'm1unm',
823 }
824
825 # Define the contents of .network files to be read in order.
826 self.configs = (
827 "[Match]\nName=m1def\n",
828 "[Match]\nName=m1unm\n[Link]\nUnmanaged=yes\n",
829 "[Match]\nName=m1*\n[Link]\nUnmanaged=no\n",
830 )
831
832 # Write out the .network files to be cleaned up automatically.
833 for i, config in enumerate(self.configs):
834 self.write_network("%02d-test.network" % i, config)
835
836 def tearDown(self):
837 """Stop networkd."""
838 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
839
840 def create_iface(self):
841 """Create temporary veth pairs for interface matching."""
842 for veth, peer in self.veths.items():
618b196e 843 self.add_veth_pair(veth, peer)
a09dc546
DM
844
845 def test_unmanaged_setting(self):
846 """Verify link states with Unmanaged= settings, hot-plug."""
847 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
848 self.create_iface()
849 self.assert_link_states(m1def='managed',
850 m1man='managed',
851 m1unm='unmanaged',
852 m0unm='unmanaged')
853
854 def test_unmanaged_setting_coldplug(self):
855 """Verify link states with Unmanaged= settings, cold-plug."""
856 self.create_iface()
857 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
858 self.assert_link_states(m1def='managed',
859 m1man='managed',
860 m1unm='unmanaged',
861 m0unm='unmanaged')
862
863 def test_catchall_config(self):
864 """Verify link states with a catch-all config, hot-plug."""
865 # Don't actually catch ALL interfaces. It messes up the host.
866 self.write_network('all.network', "[Match]\nName=m[01]???\n")
867 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
868 self.create_iface()
869 self.assert_link_states(m1def='managed',
870 m1man='managed',
871 m1unm='unmanaged',
872 m0unm='managed')
873
874 def test_catchall_config_coldplug(self):
875 """Verify link states with a catch-all config, cold-plug."""
876 # Don't actually catch ALL interfaces. It messes up the host.
877 self.write_network('all.network', "[Match]\nName=m[01]???\n")
878 self.create_iface()
879 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
880 self.assert_link_states(m1def='managed',
881 m1man='managed',
882 m1unm='unmanaged',
883 m0unm='managed')
884
885
4ddb85b1
MP
886if __name__ == '__main__':
887 unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout,
888 verbosity=2))