]>
git.ipfire.org Git - thirdparty/systemd.git/blob - test/networkd-test.py
2 # SPDX-License-Identifier: LGPL-2.1+
4 # networkd integration test
5 # This uses temporary configuration in /run and temporary veth devices, and
6 # does not write anything on disk or change any system configuration;
7 # but it assumes (and checks at the beginning) that networkd is not currently
10 # This can be run on a normal installation, in QEMU, nspawn (with
11 # --private-network), LXD (with "--config raw.lxc=lxc.aa_profile=unconfined"),
12 # or LXC system containers. You need at least the "ip" tool from the iproute
13 # package; it is recommended to install dnsmasq too to get full test coverage.
15 # ATTENTION: This uses the *installed* networkd, not the one from the built
18 # (C) 2015 Canonical Ltd.
19 # Author: Martin Pitt <martin.pitt@ubuntu.com>
31 HAVE_DNSMASQ
= shutil
.which('dnsmasq') is not None
33 NETWORK_UNITDIR
= '/run/systemd/network'
35 NETWORKD_WAIT_ONLINE
= shutil
.which('systemd-networkd-wait-online',
36 path
='/usr/lib/systemd:/lib/systemd')
38 RESOLV_CONF
= '/run/systemd/resolve/resolv.conf'
42 """Initialize the environment, and perform sanity checks on it."""
43 if NETWORKD_WAIT_ONLINE
is None:
44 raise OSError(errno
.ENOENT
, 'systemd-networkd-wait-online not found')
46 # Do not run any tests if the system is using networkd already.
47 if subprocess
.call(['systemctl', 'is-active', '--quiet',
48 'systemd-networkd.service']) == 0:
49 raise unittest
.SkipTest('networkd is already active')
51 # Avoid "Failed to open /dev/tty" errors in containers.
52 os
.environ
['SYSTEMD_LOG_TARGET'] = 'journal'
54 # Ensure the unit directory exists so tests can dump files into it.
55 os
.makedirs(NETWORK_UNITDIR
, exist_ok
=True)
58 class NetworkdTestingUtilities
:
59 """Provide a set of utility functions to facilitate networkd tests.
61 This class must be inherited along with unittest.TestCase to define
62 some required methods.
65 def add_veth_pair(self
, veth
, peer
, veth_options
=(), peer_options
=()):
66 """Add a veth interface pair, and queue them to be removed."""
67 subprocess
.check_call(['ip', 'link', 'add', 'name', veth
] +
69 ['type', 'veth', 'peer', 'name', peer
] +
71 self
.addCleanup(subprocess
.call
, ['ip', 'link', 'del', 'dev', peer
])
73 def write_network(self
, unit_name
, contents
):
74 """Write a network unit file, and queue it to be removed."""
75 unit_path
= os
.path
.join(NETWORK_UNITDIR
, unit_name
)
77 with
open(unit_path
, 'w') as unit
:
79 self
.addCleanup(os
.remove
, unit_path
)
81 def write_network_dropin(self
, unit_name
, dropin_name
, contents
):
82 """Write a network unit drop-in, and queue it to be removed."""
83 dropin_dir
= os
.path
.join(NETWORK_UNITDIR
, "{}.d".format(unit_name
))
84 dropin_path
= os
.path
.join(dropin_dir
, "{}.conf".format(dropin_name
))
86 os
.makedirs(dropin_dir
, exist_ok
=True)
87 self
.addCleanup(os
.rmdir
, dropin_dir
)
88 with
open(dropin_path
, 'w') as dropin
:
89 dropin
.write(contents
)
90 self
.addCleanup(os
.remove
, dropin_path
)
92 def read_attr(self
, link
, attribute
):
93 """Read a link attributed from the sysfs."""
94 # Note we we don't want to check if interface `link' is managed, we
95 # want to evaluate link variable and pass the value of the link to
96 # assert_link_states e.g. eth0=managed.
97 self
.assert_link_states(**{link
:'managed'})
98 with
open(os
.path
.join('/sys/class/net', link
, attribute
)) as f
:
99 return f
.readline().strip()
101 def assert_link_states(self
, **kwargs
):
102 """Match networkctl link states to the given ones.
104 Each keyword argument should be the name of a network interface
105 with its expected value of the "SETUP" column in output from
106 networkctl. The interfaces have five seconds to come online
107 before the check is performed. Every specified interface must
108 be present in the output, and any other interfaces found in the
111 A special interface state "managed" is supported, which matches
112 any value in the "SETUP" column other than "unmanaged".
116 interfaces
= set(kwargs
)
118 # Wait for the requested interfaces, but don't fail for them.
119 subprocess
.call([NETWORKD_WAIT_ONLINE
, '--timeout=5'] +
120 ['--interface={}'.format(iface
) for iface
in kwargs
])
122 # Validate each link state found in the networkctl output.
123 out
= subprocess
.check_output(['networkctl', '--no-legend']).rstrip()
124 for line
in out
.decode('utf-8').split('\n'):
125 fields
= line
.split()
126 if len(fields
) >= 5 and fields
[1] in kwargs
:
128 expected
= kwargs
[iface
]
130 if (actual
!= expected
and
131 not (expected
== 'managed' and actual
!= 'unmanaged')):
132 self
.fail("Link {} expects state {}, found {}".format(iface
, expected
, actual
))
133 interfaces
.remove(iface
)
135 # Ensure that all requested interfaces have been covered.
137 self
.fail("Missing links in status output: {}".format(interfaces
))
140 class BridgeTest(NetworkdTestingUtilities
, unittest
.TestCase
):
141 """Provide common methods for testing networkd against servers."""
144 self
.write_network('port1.netdev', '''\
148 MACAddress=12:34:56:78:9a:bc''')
149 self
.write_network('port2.netdev', '''\
153 MACAddress=12:34:56:78:9a:bd''')
154 self
.write_network('mybridge.netdev', '''\
158 self
.write_network('port1.network', '''\
163 self
.write_network('port2.network', '''\
168 self
.write_network('mybridge.network', '''\
173 Address=192.168.250.33/24
174 Gateway=192.168.250.1''')
175 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
178 subprocess
.check_call(['systemctl', 'stop', 'systemd-networkd'])
179 subprocess
.check_call(['ip', 'link', 'del', 'mybridge'])
180 subprocess
.check_call(['ip', 'link', 'del', 'port1'])
181 subprocess
.check_call(['ip', 'link', 'del', 'port2'])
183 def test_bridge_init(self
):
184 self
.assert_link_states(
189 def test_bridge_port_priority(self
):
190 self
.assertEqual(self
.read_attr('port1', 'brport/priority'), '32')
191 self
.write_network_dropin('port1.network', 'priority', '''\
195 subprocess
.check_call(['systemctl', 'restart', 'systemd-networkd'])
196 self
.assertEqual(self
.read_attr('port1', 'brport/priority'), '28')
198 def test_bridge_port_priority_set_zero(self
):
199 """It should be possible to set the bridge port priority to 0"""
200 self
.assertEqual(self
.read_attr('port2', 'brport/priority'), '32')
201 self
.write_network_dropin('port2.network', 'priority', '''\
205 subprocess
.check_call(['systemctl', 'restart', 'systemd-networkd'])
206 self
.assertEqual(self
.read_attr('port2', 'brport/priority'), '0')
208 class ClientTestBase(NetworkdTestingUtilities
):
209 """Provide common methods for testing networkd against servers."""
212 def setUpClass(klass
):
213 klass
.orig_log_level
= subprocess
.check_output(
214 ['systemctl', 'show', '--value', '--property', 'LogLevel'],
215 universal_newlines
=True).strip()
216 subprocess
.check_call(['systemd-analyze', 'set-log-level', 'debug'])
219 def tearDownClass(klass
):
220 subprocess
.check_call(['systemd-analyze', 'set-log-level', klass
.orig_log_level
])
223 self
.iface
= 'test_eth42'
224 self
.if_router
= 'router_eth42'
225 self
.workdir_obj
= tempfile
.TemporaryDirectory()
226 self
.workdir
= self
.workdir_obj
.name
227 self
.config
= 'test_eth42.network'
229 # get current journal cursor
230 subprocess
.check_output(['journalctl', '--sync'])
231 out
= subprocess
.check_output(['journalctl', '-b', '--quiet',
232 '--no-pager', '-n0', '--show-cursor'],
233 universal_newlines
=True)
234 self
.assertTrue(out
.startswith('-- cursor:'))
235 self
.journal_cursor
= out
.split()[-1]
238 self
.shutdown_iface()
239 subprocess
.call(['systemctl', 'stop', 'systemd-networkd'])
240 subprocess
.call(['ip', 'link', 'del', 'dummy0'],
241 stderr
=subprocess
.DEVNULL
)
243 def show_journal(self
, unit
):
244 '''Show journal of given unit since start of the test'''
246 print('---- {} ----'.format(unit
))
247 subprocess
.check_output(['journalctl', '--sync'])
249 subprocess
.call(['journalctl', '-b', '--no-pager', '--quiet',
250 '--cursor', self
.journal_cursor
, '-u', unit
])
252 def create_iface(self
, ipv6
=False):
253 '''Create test interface with DHCP server behind it'''
255 raise NotImplementedError('must be implemented by a subclass')
257 def shutdown_iface(self
):
258 '''Remove test interface and stop DHCP server'''
260 raise NotImplementedError('must be implemented by a subclass')
262 def print_server_log(self
):
263 '''Print DHCP server log for debugging failures'''
265 raise NotImplementedError('must be implemented by a subclass')
267 def do_test(self
, coldplug
=True, ipv6
=False, extra_opts
='',
268 online_timeout
=10, dhcp_mode
='yes'):
270 subprocess
.check_call(['systemctl', 'start', 'systemd-resolved'])
271 except subprocess
.CalledProcessError
:
272 self
.show_journal('systemd-resolved.service')
274 self
.write_network(self
.config
, '''\
279 {}'''.format(self
.iface
, dhcp_mode
, extra_opts
))
282 # create interface first, then start networkd
283 self
.create_iface(ipv6
=ipv6
)
284 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
285 elif coldplug
is not None:
286 # start networkd first, then create interface
287 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
288 self
.create_iface(ipv6
=ipv6
)
290 # "None" means test sets up interface by itself
291 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
294 subprocess
.check_call([NETWORKD_WAIT_ONLINE
, '--interface',
295 self
.iface
, '--timeout=%i' % online_timeout
])
298 # check iface state and IP 6 address; FIXME: we need to wait a bit
299 # longer, as the iface is "configured" already with IPv4 *or*
300 # IPv6, but we want to wait for both
302 out
= subprocess
.check_output(['ip', 'a', 'show', 'dev', self
.iface
])
303 if b
'state UP' in out
and b
'inet6 2600' in out
and b
'inet 192.168' in out
:
307 self
.fail('timed out waiting for IPv6 configuration')
309 self
.assertRegex(out
, b
'inet6 2600::.* scope global .*dynamic')
310 self
.assertRegex(out
, b
'inet6 fe80::.* scope link')
312 # should have link-local address on IPv6 only
313 out
= subprocess
.check_output(['ip', '-6', 'a', 'show', 'dev', self
.iface
])
314 self
.assertRegex(out
, br
'inet6 fe80::.* scope link')
315 self
.assertNotIn(b
'scope global', out
)
317 # should have IPv4 address
318 out
= subprocess
.check_output(['ip', '-4', 'a', 'show', 'dev', self
.iface
])
319 self
.assertIn(b
'state UP', out
)
320 self
.assertRegex(out
, br
'inet 192.168.5.\d+/.* scope global dynamic')
322 # check networkctl state
323 out
= subprocess
.check_output(['networkctl'])
324 self
.assertRegex(out
, (r
'{}\s+ether\s+[a-z-]+\s+unmanaged'.format(self
.if_router
)).encode())
325 self
.assertRegex(out
, (r
'{}\s+ether\s+routable\s+configured'.format(self
.iface
)).encode())
327 out
= subprocess
.check_output(['networkctl', 'status', self
.iface
])
328 self
.assertRegex(out
, br
'Type:\s+ether')
329 self
.assertRegex(out
, br
'State:\s+routable.*configured')
330 self
.assertRegex(out
, br
'Address:\s+192.168.5.\d+')
332 self
.assertRegex(out
, br
'2600::')
334 self
.assertNotIn(br
'2600::', out
)
335 self
.assertRegex(out
, br
'fe80::')
336 self
.assertRegex(out
, br
'Gateway:\s+192.168.5.1')
337 self
.assertRegex(out
, br
'DNS:\s+192.168.5.1')
338 except (AssertionError, subprocess
.CalledProcessError
):
339 # show networkd status, journal, and DHCP server log on failure
340 with
open(os
.path
.join(NETWORK_UNITDIR
, self
.config
)) as f
:
341 print('\n---- {} ----\n{}'.format(self
.config
, f
.read()))
342 print('---- interface status ----')
344 subprocess
.call(['ip', 'a', 'show', 'dev', self
.iface
])
345 print('---- networkctl status {} ----'.format(self
.iface
))
347 subprocess
.call(['networkctl', 'status', self
.iface
])
348 self
.show_journal('systemd-networkd.service')
349 self
.print_server_log()
352 for timeout
in range(50):
353 with
open(RESOLV_CONF
) as f
:
355 if 'nameserver 192.168.5.1\n' in contents
:
359 self
.fail('nameserver 192.168.5.1 not found in ' + RESOLV_CONF
)
361 if coldplug
is False:
362 # check post-down.d hook
363 self
.shutdown_iface()
365 def test_coldplug_dhcp_yes_ip4(self
):
366 # we have a 12s timeout on RA, so we need to wait longer
367 self
.do_test(coldplug
=True, ipv6
=False, online_timeout
=15)
369 def test_coldplug_dhcp_yes_ip4_no_ra(self
):
370 # with disabling RA explicitly things should be fast
371 self
.do_test(coldplug
=True, ipv6
=False,
372 extra_opts
='IPv6AcceptRA=False')
374 def test_coldplug_dhcp_ip4_only(self
):
375 # we have a 12s timeout on RA, so we need to wait longer
376 self
.do_test(coldplug
=True, ipv6
=False, dhcp_mode
='ipv4',
379 def test_coldplug_dhcp_ip4_only_no_ra(self
):
380 # with disabling RA explicitly things should be fast
381 self
.do_test(coldplug
=True, ipv6
=False, dhcp_mode
='ipv4',
382 extra_opts
='IPv6AcceptRA=False')
384 def test_coldplug_dhcp_ip6(self
):
385 self
.do_test(coldplug
=True, ipv6
=True)
387 def test_hotplug_dhcp_ip4(self
):
388 # With IPv4 only we have a 12s timeout on RA, so we need to wait longer
389 self
.do_test(coldplug
=False, ipv6
=False, online_timeout
=15)
391 def test_hotplug_dhcp_ip6(self
):
392 self
.do_test(coldplug
=False, ipv6
=True)
394 def test_route_only_dns(self
):
395 self
.write_network('myvpn.netdev', '''\
399 MACAddress=12:34:56:78:9a:bc''')
400 self
.write_network('myvpn.network', '''\
404 Address=192.168.42.100
406 Domains= ~company''')
408 self
.do_test(coldplug
=True, ipv6
=False,
409 extra_opts
='IPv6AcceptRouterAdvertisements=False')
411 with
open(RESOLV_CONF
) as f
:
413 # ~company is not a search domain, only a routing domain
414 self
.assertNotRegex(contents
, 'search.*company')
415 # our global server should appear
416 self
.assertIn('nameserver 192.168.5.1\n', contents
)
417 # should not have domain-restricted server as global server
418 self
.assertNotIn('nameserver 192.168.42.1\n', contents
)
420 def test_route_only_dns_all_domains(self
):
421 self
.write_network('myvpn.netdev', '''[NetDev]
424 MACAddress=12:34:56:78:9a:bc''')
425 self
.write_network('myvpn.network', '''[Match]
428 Address=192.168.42.100
430 Domains= ~company ~.''')
432 self
.do_test(coldplug
=True, ipv6
=False,
433 extra_opts
='IPv6AcceptRouterAdvertisements=False')
435 with
open(RESOLV_CONF
) as f
:
438 # ~company is not a search domain, only a routing domain
439 self
.assertNotRegex(contents
, 'search.*company')
441 # our global server should appear
442 self
.assertIn('nameserver 192.168.5.1\n', contents
)
443 # should have company server as global server due to ~.
444 self
.assertIn('nameserver 192.168.42.1\n', contents
)
447 @unittest.skipUnless(HAVE_DNSMASQ
, 'dnsmasq not installed')
448 class DnsmasqClientTest(ClientTestBase
, unittest
.TestCase
):
449 '''Test networkd client against dnsmasq'''
454 self
.iface_mac
= 'de:ad:be:ef:47:11'
456 def create_iface(self
, ipv6
=False, dnsmasq_opts
=None):
457 '''Create test interface with DHCP server behind it'''
460 subprocess
.check_call(['ip', 'link', 'add', 'name', self
.iface
,
461 'address', self
.iface_mac
,
462 'type', 'veth', 'peer', 'name', self
.if_router
])
464 # give our router an IP
465 subprocess
.check_call(['ip', 'a', 'flush', 'dev', self
.if_router
])
466 subprocess
.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self
.if_router
])
468 subprocess
.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self
.if_router
])
469 subprocess
.check_call(['ip', 'link', 'set', self
.if_router
, 'up'])
472 self
.dnsmasq_log
= os
.path
.join(self
.workdir
, 'dnsmasq.log')
473 lease_file
= os
.path
.join(self
.workdir
, 'dnsmasq.leases')
475 extra_opts
= ['--enable-ra', '--dhcp-range=2600::10,2600::20']
479 extra_opts
+= dnsmasq_opts
480 self
.dnsmasq
= subprocess
.Popen(
481 ['dnsmasq', '--keep-in-foreground', '--log-queries',
482 '--log-facility=' + self
.dnsmasq_log
, '--conf-file=/dev/null',
483 '--dhcp-leasefile=' + lease_file
, '--bind-interfaces',
484 '--interface=' + self
.if_router
, '--except-interface=lo',
485 '--dhcp-range=192.168.5.10,192.168.5.200'] + extra_opts
)
487 def shutdown_iface(self
):
488 '''Remove test interface and stop DHCP server'''
491 subprocess
.check_call(['ip', 'link', 'del', 'dev', self
.if_router
])
492 self
.if_router
= None
498 def print_server_log(self
):
499 '''Print DHCP server log for debugging failures'''
501 with
open(self
.dnsmasq_log
) as f
:
502 sys
.stdout
.write('\n\n---- dnsmasq log ----\n{}\n------\n\n'.format(f
.read()))
504 def test_resolved_domain_restricted_dns(self
):
505 '''resolved: domain-restricted DNS servers'''
507 # create interface for generic connections; this will map all DNS names
509 self
.create_iface(dnsmasq_opts
=['--address=/#/192.168.42.1'])
510 self
.write_network('general.network', '''\
515 IPv6AcceptRA=False'''.format(self
.iface
))
517 # create second device/dnsmasq for a .company/.lab VPN interface
518 # static IPs for simplicity
519 self
.add_veth_pair('testvpnclient', 'testvpnrouter')
520 subprocess
.check_call(['ip', 'a', 'flush', 'dev', 'testvpnrouter'])
521 subprocess
.check_call(['ip', 'a', 'add', '10.241.3.1/24', 'dev', 'testvpnrouter'])
522 subprocess
.check_call(['ip', 'link', 'set', 'testvpnrouter', 'up'])
524 vpn_dnsmasq_log
= os
.path
.join(self
.workdir
, 'dnsmasq-vpn.log')
525 vpn_dnsmasq
= subprocess
.Popen(
526 ['dnsmasq', '--keep-in-foreground', '--log-queries',
527 '--log-facility=' + vpn_dnsmasq_log
, '--conf-file=/dev/null',
528 '--dhcp-leasefile=/dev/null', '--bind-interfaces',
529 '--interface=testvpnrouter', '--except-interface=lo',
530 '--address=/math.lab/10.241.3.3', '--address=/cantina.company/10.241.4.4'])
531 self
.addCleanup(vpn_dnsmasq
.wait
)
532 self
.addCleanup(vpn_dnsmasq
.kill
)
534 self
.write_network('vpn.network', '''\
539 Address=10.241.3.2/24
541 Domains= ~company ~lab''')
543 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
544 subprocess
.check_call([NETWORKD_WAIT_ONLINE
, '--interface', self
.iface
,
545 '--interface=testvpnclient', '--timeout=20'])
547 # ensure we start fresh with every test
548 subprocess
.check_call(['systemctl', 'restart', 'systemd-resolved'])
550 # test vpnclient specific domains; these should *not* be answered by
552 out
= subprocess
.check_output(['systemd-resolve', 'math.lab'])
553 self
.assertIn(b
'math.lab: 10.241.3.3', out
)
554 out
= subprocess
.check_output(['systemd-resolve', 'kettle.cantina.company'])
555 self
.assertIn(b
'kettle.cantina.company: 10.241.4.4', out
)
557 # test general domains
558 out
= subprocess
.check_output(['systemd-resolve', 'megasearch.net'])
559 self
.assertIn(b
'megasearch.net: 192.168.42.1', out
)
561 with
open(self
.dnsmasq_log
) as f
:
562 general_log
= f
.read()
563 with
open(vpn_dnsmasq_log
) as f
:
566 # VPN domains should only be sent to VPN DNS
567 self
.assertRegex(vpn_log
, 'query.*math.lab')
568 self
.assertRegex(vpn_log
, 'query.*cantina.company')
569 self
.assertNotIn('.lab', general_log
)
570 self
.assertNotIn('.company', general_log
)
572 # general domains should not be sent to the VPN DNS
573 self
.assertRegex(general_log
, 'query.*megasearch.net')
574 self
.assertNotIn('megasearch.net', vpn_log
)
576 def test_resolved_etc_hosts(self
):
577 '''resolved queries to /etc/hosts'''
579 # FIXME: -t MX query fails with enabled DNSSEC (even when using
580 # the known negative trust anchor .internal instead of .example)
581 conf
= '/run/systemd/resolved.conf.d/test-disable-dnssec.conf'
582 os
.makedirs(os
.path
.dirname(conf
), exist_ok
=True)
583 with
open(conf
, 'w') as f
:
584 f
.write('[Resolve]\nDNSSEC=no')
585 self
.addCleanup(os
.remove
, conf
)
587 # create /etc/hosts bind mount which resolves my.example for IPv4
588 hosts
= os
.path
.join(self
.workdir
, 'hosts')
589 with
open(hosts
, 'w') as f
:
590 f
.write('172.16.99.99 my.example\n')
591 subprocess
.check_call(['mount', '--bind', hosts
, '/etc/hosts'])
592 self
.addCleanup(subprocess
.call
, ['umount', '/etc/hosts'])
593 subprocess
.check_call(['systemctl', 'stop', 'systemd-resolved.service'])
595 # note: different IPv4 address here, so that it's easy to tell apart
596 # what resolved the query
597 self
.create_iface(dnsmasq_opts
=['--host-record=my.example,172.16.99.1,2600::99:99',
598 '--host-record=other.example,172.16.0.42,2600::42',
599 '--mx-host=example,mail.example'],
601 self
.do_test(coldplug
=None, ipv6
=True)
604 # family specific queries
605 out
= subprocess
.check_output(['systemd-resolve', '-4', 'my.example'])
606 self
.assertIn(b
'my.example: 172.16.99.99', out
)
607 # we don't expect an IPv6 answer; if /etc/hosts has any IP address,
608 # it's considered a sufficient source
609 self
.assertNotEqual(subprocess
.call(['systemd-resolve', '-6', 'my.example']), 0)
610 # "any family" query; IPv4 should come from /etc/hosts
611 out
= subprocess
.check_output(['systemd-resolve', 'my.example'])
612 self
.assertIn(b
'my.example: 172.16.99.99', out
)
613 # IP → name lookup; again, takes the /etc/hosts one
614 out
= subprocess
.check_output(['systemd-resolve', '172.16.99.99'])
615 self
.assertIn(b
'172.16.99.99: my.example', out
)
617 # non-address RRs should fall back to DNS
618 out
= subprocess
.check_output(['systemd-resolve', '--type=MX', 'example'])
619 self
.assertIn(b
'example IN MX 1 mail.example', out
)
621 # other domains query DNS
622 out
= subprocess
.check_output(['systemd-resolve', 'other.example'])
623 self
.assertIn(b
'172.16.0.42', out
)
624 out
= subprocess
.check_output(['systemd-resolve', '172.16.0.42'])
625 self
.assertIn(b
'172.16.0.42: other.example', out
)
626 except (AssertionError, subprocess
.CalledProcessError
):
627 self
.show_journal('systemd-resolved.service')
628 self
.print_server_log()
631 def test_transient_hostname(self
):
632 '''networkd sets transient hostname from DHCP'''
634 orig_hostname
= socket
.gethostname()
635 self
.addCleanup(socket
.sethostname
, orig_hostname
)
636 # temporarily move /etc/hostname away; restart hostnamed to pick it up
637 if os
.path
.exists('/etc/hostname'):
638 subprocess
.check_call(['mount', '--bind', '/dev/null', '/etc/hostname'])
639 self
.addCleanup(subprocess
.call
, ['umount', '/etc/hostname'])
640 subprocess
.check_call(['systemctl', 'stop', 'systemd-hostnamed.service'])
642 self
.create_iface(dnsmasq_opts
=['--dhcp-host={},192.168.5.210,testgreen'.format(self
.iface_mac
)])
643 self
.do_test(coldplug
=None, extra_opts
='IPv6AcceptRA=False', dhcp_mode
='ipv4')
646 # should have received the fixed IP above
647 out
= subprocess
.check_output(['ip', '-4', 'a', 'show', 'dev', self
.iface
])
648 self
.assertRegex(out
, b
'inet 192.168.5.210/24 .* scope global dynamic')
649 # should have set transient hostname in hostnamed; this is
650 # sometimes a bit lagging (issue #4753), so retry a few times
651 for retry
in range(1, 6):
652 out
= subprocess
.check_output(['hostnamectl'])
653 if b
'testgreen' in out
:
656 sys
.stdout
.write('[retry %i] ' % retry
)
659 self
.fail('Transient hostname not found in hostnamectl:\n{}'.format(out
.decode()))
660 # and also applied to the system
661 self
.assertEqual(socket
.gethostname(), 'testgreen')
662 except AssertionError:
663 self
.show_journal('systemd-networkd.service')
664 self
.show_journal('systemd-hostnamed.service')
665 self
.print_server_log()
668 def test_transient_hostname_with_static(self
):
669 '''transient hostname is not applied if static hostname exists'''
671 orig_hostname
= socket
.gethostname()
672 self
.addCleanup(socket
.sethostname
, orig_hostname
)
673 if not os
.path
.exists('/etc/hostname'):
674 self
.writeConfig('/etc/hostname', orig_hostname
)
675 subprocess
.check_call(['systemctl', 'stop', 'systemd-hostnamed.service'])
677 self
.create_iface(dnsmasq_opts
=['--dhcp-host={},192.168.5.210,testgreen'.format(self
.iface_mac
)])
678 self
.do_test(coldplug
=None, extra_opts
='IPv6AcceptRA=False', dhcp_mode
='ipv4')
681 # should have received the fixed IP above
682 out
= subprocess
.check_output(['ip', '-4', 'a', 'show', 'dev', self
.iface
])
683 self
.assertRegex(out
, b
'inet 192.168.5.210/24 .* scope global dynamic')
684 # static hostname wins over transient one, thus *not* applied
685 self
.assertEqual(socket
.gethostname(), orig_hostname
)
686 except AssertionError:
687 self
.show_journal('systemd-networkd.service')
688 self
.show_journal('systemd-hostnamed.service')
689 self
.print_server_log()
693 class NetworkdClientTest(ClientTestBase
, unittest
.TestCase
):
694 '''Test networkd client against networkd server'''
700 def create_iface(self
, ipv6
=False, dhcpserver_opts
=None):
701 '''Create test interface with DHCP server behind it'''
703 # run "router-side" networkd in own mount namespace to shield it from
704 # "client-side" configuration and networkd
705 (fd
, script
) = tempfile
.mkstemp(prefix
='networkd-router.sh')
706 self
.addCleanup(os
.remove
, script
)
707 with os
.fdopen(fd
, 'w+') as f
:
711 mkdir -p /run/systemd/network
712 mkdir -p /run/systemd/netif
713 mount -t tmpfs none /run/systemd/network
714 mount -t tmpfs none /run/systemd/netif
715 [ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus
716 # create router/client veth pair
717 cat << EOF > /run/systemd/network/test.netdev
726 cat << EOF > /run/systemd/network/test.network
731 Address=192.168.5.1/24
742 # run networkd as in systemd-networkd.service
743 exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//; s/^[@+-]//; s/^!*//; p}')
744 ''' % {'ifr': self
.if_router
, 'ifc': self
.iface
, 'addr6': ipv6
and 'Address=2600::1/64' or '',
745 'dhopts': dhcpserver_opts
or ''})
749 subprocess
.check_call(['systemd-run', '--unit=networkd-test-router.service',
750 '-p', 'InaccessibleDirectories=-/etc/systemd/network',
751 '-p', 'InaccessibleDirectories=-/run/systemd/network',
752 '-p', 'InaccessibleDirectories=-/run/systemd/netif',
753 '--service-type=notify', script
])
755 # wait until devices got created
757 out
= subprocess
.check_output(['ip', 'a', 'show', 'dev', self
.if_router
])
758 if b
'state UP' in out
and b
'scope global' in out
:
762 def shutdown_iface(self
):
763 '''Remove test interface and stop DHCP server'''
766 subprocess
.check_call(['systemctl', 'stop', 'networkd-test-router.service'])
767 # ensure failed transient unit does not stay around
768 subprocess
.call(['systemctl', 'reset-failed', 'networkd-test-router.service'])
769 subprocess
.call(['ip', 'link', 'del', 'dev', self
.if_router
])
770 self
.if_router
= None
772 def print_server_log(self
):
773 '''Print DHCP server log for debugging failures'''
775 self
.show_journal('networkd-test-router.service')
777 @unittest.skip('networkd does not have DHCPv6 server support')
778 def test_hotplug_dhcp_ip6(self
):
781 @unittest.skip('networkd does not have DHCPv6 server support')
782 def test_coldplug_dhcp_ip6(self
):
785 def test_search_domains(self
):
787 # we don't use this interface for this test
788 self
.if_router
= None
790 self
.write_network('test.netdev', '''\
794 MACAddress=12:34:56:78:9a:bc''')
795 self
.write_network('test.network', '''\
799 Address=192.168.42.100
801 Domains= one two three four five six seven eight nine ten''')
803 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
805 for timeout
in range(50):
806 with
open(RESOLV_CONF
) as f
:
808 if ' one' in contents
:
811 self
.assertRegex(contents
, 'search .*one two three four')
812 self
.assertNotIn('seven\n', contents
)
813 self
.assertIn('# Too many search domains configured, remaining ones ignored.\n', contents
)
815 def test_search_domains_too_long(self
):
817 # we don't use this interface for this test
818 self
.if_router
= None
820 name_prefix
= 'a' * 60
822 self
.write_network('test.netdev', '''\
826 MACAddress=12:34:56:78:9a:bc''')
827 self
.write_network('test.network', '''\
831 Address=192.168.42.100
833 Domains={p}0 {p}1 {p}2 {p}3 {p}4'''.format(p
=name_prefix
))
835 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
837 for timeout
in range(50):
838 with
open(RESOLV_CONF
) as f
:
840 if ' one' in contents
:
843 self
.assertRegex(contents
, 'search .*{p}0 {p}1 {p}2'.format(p
=name_prefix
))
844 self
.assertIn('# Total length of all search domains is too long, remaining ones ignored.', contents
)
846 def test_dropin(self
):
847 # we don't use this interface for this test
848 self
.if_router
= None
850 self
.write_network('test.netdev', '''\
854 MACAddress=12:34:56:78:9a:bc''')
855 self
.write_network('test.network', '''\
859 Address=192.168.42.100
861 self
.write_network_dropin('test.network', 'dns', '''\
865 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
867 for timeout
in range(50):
868 with
open(RESOLV_CONF
) as f
:
870 if ' 127.0.0.1' in contents
:
873 self
.assertIn('nameserver 192.168.42.1\n', contents
)
874 self
.assertIn('nameserver 127.0.0.1\n', contents
)
876 def test_dhcp_timezone(self
):
877 '''networkd sets time zone from DHCP'''
880 out
= subprocess
.check_output(['busctl', 'get-property', 'org.freedesktop.timedate1',
881 '/org/freedesktop/timedate1', 'org.freedesktop.timedate1', 'Timezone'])
882 assert out
.startswith(b
's "')
884 assert out
.endswith(b
'"')
885 return out
[3:-1].decode()
887 orig_timezone
= get_tz()
888 self
.addCleanup(subprocess
.call
, ['timedatectl', 'set-timezone', orig_timezone
])
890 self
.create_iface(dhcpserver_opts
='EmitTimezone=yes\nTimezone=Pacific/Honolulu')
891 self
.do_test(coldplug
=None, extra_opts
='IPv6AcceptRA=false\n[DHCP]\nUseTimezone=true', dhcp_mode
='ipv4')
893 # should have applied the received timezone
895 self
.assertEqual(get_tz(), 'Pacific/Honolulu')
896 except AssertionError:
897 self
.show_journal('systemd-networkd.service')
898 self
.show_journal('systemd-hostnamed.service')
902 class MatchClientTest(unittest
.TestCase
, NetworkdTestingUtilities
):
903 """Test [Match] sections in .network files.
905 Be aware that matching the test host's interfaces will wipe their
906 configuration, so as a precaution, all network files should have a
907 restrictive [Match] section to only ever interfere with the
908 temporary veth interfaces created here.
913 subprocess
.call(['systemctl', 'stop', 'systemd-networkd'])
915 def test_basic_matching(self
):
916 """Verify the Name= line works throughout this class."""
917 self
.add_veth_pair('test_if1', 'fake_if2')
918 self
.write_network('test.network', "[Match]\nName=test_*\n[Network]")
919 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
920 self
.assert_link_states(test_if1
='managed', fake_if2
='unmanaged')
922 def test_inverted_matching(self
):
923 """Verify that a '!'-prefixed value inverts the match."""
924 # Use a MAC address as the interfaces' common matching attribute
925 # to avoid depending on udev, to support testing in containers.
926 mac
= '00:01:02:03:98:99'
927 self
.add_veth_pair('test_veth', 'test_peer',
928 ['addr', mac
], ['addr', mac
])
929 self
.write_network('no-veth.network', """\
932 Name=!nonexistent *peer*
933 [Network]""".format(mac
))
934 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
935 self
.assert_link_states(test_veth
='managed', test_peer
='unmanaged')
938 class UnmanagedClientTest(unittest
.TestCase
, NetworkdTestingUtilities
):
939 """Test if networkd manages the correct interfaces."""
942 """Write .network files to match the named veth devices."""
943 # Define the veth+peer pairs to be created.
944 # Their pairing doesn't actually matter, only their names do.
950 # Define the contents of .network files to be read in order.
952 "[Match]\nName=m1def\n",
953 "[Match]\nName=m1unm\n[Link]\nUnmanaged=yes\n",
954 "[Match]\nName=m1*\n[Link]\nUnmanaged=no\n",
957 # Write out the .network files to be cleaned up automatically.
958 for i
, config
in enumerate(self
.configs
):
959 self
.write_network("%02d-test.network" % i
, config
)
963 subprocess
.call(['systemctl', 'stop', 'systemd-networkd'])
965 def create_iface(self
):
966 """Create temporary veth pairs for interface matching."""
967 for veth
, peer
in self
.veths
.items():
968 self
.add_veth_pair(veth
, peer
)
970 def test_unmanaged_setting(self
):
971 """Verify link states with Unmanaged= settings, hot-plug."""
972 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
974 self
.assert_link_states(m1def
='managed',
979 def test_unmanaged_setting_coldplug(self
):
980 """Verify link states with Unmanaged= settings, cold-plug."""
982 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
983 self
.assert_link_states(m1def
='managed',
988 def test_catchall_config(self
):
989 """Verify link states with a catch-all config, hot-plug."""
990 # Don't actually catch ALL interfaces. It messes up the host.
991 self
.write_network('all.network', "[Match]\nName=m[01]???\n")
992 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
994 self
.assert_link_states(m1def
='managed',
999 def test_catchall_config_coldplug(self
):
1000 """Verify link states with a catch-all config, cold-plug."""
1001 # Don't actually catch ALL interfaces. It messes up the host.
1002 self
.write_network('all.network', "[Match]\nName=m[01]???\n")
1004 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
1005 self
.assert_link_states(m1def
='managed',
1011 if __name__
== '__main__':
1012 unittest
.main(testRunner
=unittest
.TextTestRunner(stream
=sys
.stdout
,