]>
git.ipfire.org Git - thirdparty/systemd.git/blob - test/networkd-test.py
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
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.
14 # ATTENTION: This uses the *installed* networkd, not the one from the built
17 # (C) 2015 Canonical Ltd.
18 # Author: Martin Pitt <martin.pitt@ubuntu.com>
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.
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.
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/>.
41 networkd_active
= subprocess
.call(['systemctl', 'is-active', '--quiet',
42 'systemd-networkd']) == 0
43 have_dnsmasq
= shutil
.which('dnsmasq')
46 @unittest.skipIf(networkd_active
,
47 'networkd is already active')
50 self
.iface
= 'test_eth42'
51 self
.if_router
= 'router_eth42'
52 self
.workdir_obj
= tempfile
.TemporaryDirectory()
53 self
.workdir
= self
.workdir_obj
.name
54 self
.config
= '/run/systemd/network/test_eth42.network'
55 os
.makedirs(os
.path
.dirname(self
.config
), exist_ok
=True)
57 # avoid "Failed to open /dev/tty" errors in containers
58 os
.environ
['SYSTEMD_LOG_TARGET'] = 'journal'
60 # determine path to systemd-networkd-wait-online
61 for p
in ['/usr/lib/systemd/systemd-networkd-wait-online',
62 '/lib/systemd/systemd-networkd-wait-online']:
64 self
.networkd_wait_online
= p
67 self
.fail('systemd-networkd-wait-online not found')
69 # get current journal cursor
70 out
= subprocess
.check_output(['journalctl', '-b', '--quiet',
71 '--no-pager', '-n0', '--show-cursor'],
72 universal_newlines
=True)
73 self
.assertTrue(out
.startswith('-- cursor:'))
74 self
.journal_cursor
= out
.split()[-1]
78 if os
.path
.exists(self
.config
):
79 os
.unlink(self
.config
)
80 subprocess
.call(['systemctl', 'stop', 'systemd-networkd'])
82 def show_journal(self
, unit
):
83 '''Show journal of given unit since start of the test'''
85 print('---- %s ----' % unit
)
87 subprocess
.call(['journalctl', '-b', '--no-pager', '--quiet',
88 '--cursor', self
.journal_cursor
, '-u', unit
])
90 def create_iface(self
, ipv6
=False):
91 '''Create test interface with DHCP server behind it'''
93 raise NotImplementedError('must be implemented by a subclass')
95 def shutdown_iface(self
):
96 '''Remove test interface and stop DHCP server'''
98 raise NotImplementedError('must be implemented by a subclass')
100 def print_server_log(self
):
101 '''Print DHCP server log for debugging failures'''
103 raise NotImplementedError('must be implemented by a subclass')
105 def do_test(self
, coldplug
=True, ipv6
=False, extra_opts
='',
106 online_timeout
=10, dhcp_mode
='yes'):
107 with
open(self
.config
, 'w') as f
:
112 %s''' % (self
.iface
, dhcp_mode
, extra_opts
))
115 # create interface first, then start networkd
116 self
.create_iface(ipv6
=ipv6
)
117 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
119 # start networkd first, then create interface
120 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
121 self
.create_iface(ipv6
=ipv6
)
124 subprocess
.check_call([self
.networkd_wait_online
, '--interface',
125 self
.iface
, '--timeout=%i' % online_timeout
])
128 # check iface state and IP 6 address; FIXME: we need to wait a bit
129 # longer, as the iface is "configured" already with IPv4 *or*
130 # IPv6, but we want to wait for both
131 for timeout
in range(10):
132 out
= subprocess
.check_output(['ip', 'a', 'show', 'dev', self
.iface
])
133 if b
'state UP' in out
and b
'inet6 2600' in out
and b
'inet 192.168' in out
:
137 self
.fail('timed out waiting for IPv6 configuration')
139 self
.assertRegex(out
, b
'inet6 2600::.* scope global .*dynamic')
140 self
.assertRegex(out
, b
'inet6 fe80::.* scope link')
142 # should have link-local address on IPv6 only
143 out
= subprocess
.check_output(['ip', '-6', 'a', 'show', 'dev', self
.iface
])
144 self
.assertRegex(out
, b
'inet6 fe80::.* scope link')
145 self
.assertNotIn(b
'scope global', out
)
147 # should have IPv4 address
148 out
= subprocess
.check_output(['ip', '-4', 'a', 'show', 'dev', self
.iface
])
149 self
.assertIn(b
'state UP', out
)
150 self
.assertRegex(out
, b
'inet 192.168.5.\d+/.* scope global dynamic')
152 # check networkctl state
153 out
= subprocess
.check_output(['networkctl'])
154 self
.assertRegex(out
, ('%s\s+ether\s+routable\s+unmanaged' % self
.if_router
).encode())
155 self
.assertRegex(out
, ('%s\s+ether\s+routable\s+configured' % self
.iface
).encode())
157 out
= subprocess
.check_output(['networkctl', 'status', self
.iface
])
158 self
.assertRegex(out
, b
'Type:\s+ether')
159 self
.assertRegex(out
, b
'State:\s+routable.*configured')
160 self
.assertRegex(out
, b
'Address:\s+192.168.5.\d+')
162 self
.assertRegex(out
, b
'2600::')
164 self
.assertNotIn(b
'2600::', out
)
165 self
.assertRegex(out
, b
'fe80::')
166 self
.assertRegex(out
, b
'Gateway:\s+192.168.5.1')
167 self
.assertRegex(out
, b
'DNS:\s+192.168.5.1')
168 except (AssertionError, subprocess
.CalledProcessError
):
169 # show networkd status, journal, and DHCP server log on failure
170 with
open(self
.config
) as f
:
171 print('\n---- %s ----\n%s' % (self
.config
, f
.read()))
172 print('---- interface status ----')
174 subprocess
.call(['ip', 'a', 'show', 'dev', self
.iface
])
175 print('---- networkctl status %s ----' % self
.iface
)
177 subprocess
.call(['networkctl', 'status', self
.iface
])
178 self
.show_journal('systemd-networkd.service')
179 self
.print_server_log()
182 # verify resolv.conf if it gets dynamically managed
183 if os
.path
.islink('/etc/resolv.conf'):
184 for timeout
in range(50):
185 with
open('/etc/resolv.conf') as f
:
187 if 'nameserver 192.168.5.1\n' in contents
:
189 # resolv.conf can have at most three nameservers; if we already
190 # have three different ones, that's also okay
191 if contents
.count('nameserver ') >= 3:
195 self
.fail('nameserver 192.168.5.1 not found in /etc/resolv.conf')
198 # check post-down.d hook
199 self
.shutdown_iface()
201 def test_coldplug_dhcp_yes_ip4(self
):
202 # we have a 12s timeout on RA, so we need to wait longer
203 self
.do_test(coldplug
=True, ipv6
=False, online_timeout
=15)
205 def test_coldplug_dhcp_yes_ip4_no_ra(self
):
206 # with disabling RA explicitly things should be fast
207 self
.do_test(coldplug
=True, ipv6
=False,
208 extra_opts
='IPv6AcceptRA=False')
210 def test_coldplug_dhcp_ip4_only(self
):
211 # we have a 12s timeout on RA, so we need to wait longer
212 self
.do_test(coldplug
=True, ipv6
=False, dhcp_mode
='ipv4',
215 def test_coldplug_dhcp_ip4_only_no_ra(self
):
216 # with disabling RA explicitly things should be fast
217 self
.do_test(coldplug
=True, ipv6
=False, dhcp_mode
='ipv4',
218 extra_opts
='IPv6AcceptRA=False')
220 def test_coldplug_dhcp_ip6(self
):
221 self
.do_test(coldplug
=True, ipv6
=True)
223 def test_hotplug_dhcp_ip4(self
):
224 # With IPv4 only we have a 12s timeout on RA, so we need to wait longer
225 self
.do_test(coldplug
=False, ipv6
=False, online_timeout
=15)
227 def test_hotplug_dhcp_ip6(self
):
228 self
.do_test(coldplug
=False, ipv6
=True)
230 def test_route_only_dns(self
):
231 with
open('/run/systemd/network/myvpn.netdev', 'w') as f
:
235 MACAddress=12:34:56:78:9a:bc''')
236 with
open('/run/systemd/network/myvpn.network', 'w') as f
:
240 Address=192.168.42.100
242 Domains= ~company''')
243 self
.addCleanup(os
.remove
, '/run/systemd/network/myvpn.netdev')
244 self
.addCleanup(os
.remove
, '/run/systemd/network/myvpn.network')
246 self
.do_test(coldplug
=True, ipv6
=False,
247 extra_opts
='IPv6AcceptRouterAdvertisements=False')
249 if os
.path
.islink('/etc/resolv.conf'):
250 with
open('/etc/resolv.conf') as f
:
253 # ~company is not a search domain, only a routing domain
254 self
.assertNotRegex(contents
, 'search.*company')
256 # our global server should appear, unless we already have three
257 # (different) servers
258 if contents
.count('nameserver ') < 3:
259 self
.assertIn('nameserver 192.168.5.1\n', contents
)
262 @unittest.skipUnless(have_dnsmasq
, 'dnsmasq not installed')
263 class DnsmasqClientTest(ClientTestBase
, unittest
.TestCase
):
264 '''Test networkd client against dnsmasq'''
270 def create_iface(self
, ipv6
=False):
271 '''Create test interface with DHCP server behind it'''
274 subprocess
.check_call(['ip', 'link', 'add', 'name', self
.iface
, 'type',
275 'veth', 'peer', 'name', self
.if_router
])
277 # give our router an IP
278 subprocess
.check_call(['ip', 'a', 'flush', 'dev', self
.if_router
])
279 subprocess
.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self
.if_router
])
281 subprocess
.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self
.if_router
])
282 subprocess
.check_call(['ip', 'link', 'set', self
.if_router
, 'up'])
285 self
.dnsmasq_log
= os
.path
.join(self
.workdir
, 'dnsmasq.log')
286 lease_file
= os
.path
.join(self
.workdir
, 'dnsmasq.leases')
288 extra_opts
= ['--enable-ra', '--dhcp-range=2600::10,2600::20']
291 self
.dnsmasq
= subprocess
.Popen(
292 ['dnsmasq', '--keep-in-foreground', '--log-queries',
293 '--log-facility=' + self
.dnsmasq_log
, '--conf-file=/dev/null',
294 '--dhcp-leasefile=' + lease_file
, '--bind-interfaces',
295 '--interface=' + self
.if_router
, '--except-interface=lo',
296 '--dhcp-range=192.168.5.10,192.168.5.200'] + extra_opts
)
298 def shutdown_iface(self
):
299 '''Remove test interface and stop DHCP server'''
302 subprocess
.check_call(['ip', 'link', 'del', 'dev', self
.if_router
])
303 self
.if_router
= None
309 def print_server_log(self
):
310 '''Print DHCP server log for debugging failures'''
312 with
open(self
.dnsmasq_log
) as f
:
313 sys
.stdout
.write('\n\n---- dnsmasq log ----\n%s\n------\n\n' % f
.read())
316 class NetworkdClientTest(ClientTestBase
, unittest
.TestCase
):
317 '''Test networkd client against networkd server'''
323 def create_iface(self
, ipv6
=False):
324 '''Create test interface with DHCP server behind it'''
326 # run "router-side" networkd in own mount namespace to shield it from
327 # "client-side" configuration and networkd
328 (fd
, script
) = tempfile
.mkstemp(prefix
='networkd-router.sh')
329 self
.addCleanup(os
.remove
, script
)
330 with os
.fdopen(fd
, 'w+') as f
:
331 f
.write('''#!/bin/sh -eu
332 mkdir -p /run/systemd/network
333 mkdir -p /run/systemd/netif
334 mount -t tmpfs none /run/systemd/network
335 mount -t tmpfs none /run/systemd/netif
336 [ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus
337 # create router/client veth pair
338 cat << EOF > /run/systemd/network/test.netdev
347 cat << EOF > /run/systemd/network/test.network
352 Address=192.168.5.1/24
362 # run networkd as in systemd-networkd.service
363 exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//; p}')
364 ''' % {'ifr': self
.if_router
, 'ifc': self
.iface
, 'addr6': ipv6
and 'Address=2600::1/64' or ''})
368 subprocess
.check_call(['systemd-run', '--unit=networkd-test-router.service',
369 '-p', 'InaccessibleDirectories=-/etc/systemd/network',
370 '-p', 'InaccessibleDirectories=-/run/systemd/network',
371 '-p', 'InaccessibleDirectories=-/run/systemd/netif',
372 '--service-type=notify', script
])
374 # wait until devices got created
375 for timeout
in range(50):
376 out
= subprocess
.check_output(['ip', 'a', 'show', 'dev', self
.if_router
])
377 if b
'state UP' in out
and b
'scope global' in out
:
381 def shutdown_iface(self
):
382 '''Remove test interface and stop DHCP server'''
385 subprocess
.check_call(['systemctl', 'stop', 'networkd-test-router.service'])
386 # ensure failed transient unit does not stay around
387 subprocess
.call(['systemctl', 'reset-failed', 'networkd-test-router.service'])
388 subprocess
.call(['ip', 'link', 'del', 'dev', self
.if_router
])
389 self
.if_router
= None
391 def print_server_log(self
):
392 '''Print DHCP server log for debugging failures'''
394 self
.show_journal('networkd-test-router.service')
396 @unittest.skip('networkd does not have DHCPv6 server support')
397 def test_hotplug_dhcp_ip6(self
):
400 @unittest.skip('networkd does not have DHCPv6 server support')
401 def test_coldplug_dhcp_ip6(self
):
404 def test_search_domains(self
):
406 # we don't use this interface for this test
407 self
.if_router
= None
409 with
open('/run/systemd/network/test.netdev', 'w') as f
:
413 MACAddress=12:34:56:78:9a:bc''')
414 with
open('/run/systemd/network/test.network', 'w') as f
:
418 Address=192.168.42.100
420 Domains= one two three four five six seven eight nine ten''')
421 self
.addCleanup(os
.remove
, '/run/systemd/network/test.netdev')
422 self
.addCleanup(os
.remove
, '/run/systemd/network/test.network')
424 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
426 if os
.path
.islink('/etc/resolv.conf'):
427 for timeout
in range(50):
428 with
open('/etc/resolv.conf') as f
:
430 if 'search one\n' in contents
:
433 self
.assertIn('search one two three four five six\n'
434 '# Too many search domains configured, remaining ones ignored.\n',
437 def test_search_domains_too_long(self
):
439 # we don't use this interface for this test
440 self
.if_router
= None
442 name_prefix
= 'a' * 60
444 with
open('/run/systemd/network/test.netdev', 'w') as f
:
448 MACAddress=12:34:56:78:9a:bc''')
449 with
open('/run/systemd/network/test.network', 'w') as f
:
453 Address=192.168.42.100
457 f
.write('%s%i ' % (name_prefix
, i
))
459 self
.addCleanup(os
.remove
, '/run/systemd/network/test.netdev')
460 self
.addCleanup(os
.remove
, '/run/systemd/network/test.network')
462 subprocess
.check_call(['systemctl', 'start', 'systemd-networkd'])
464 if os
.path
.islink('/etc/resolv.conf'):
465 for timeout
in range(50):
466 with
open('/etc/resolv.conf') as f
:
468 if 'search one\n' in contents
:
471 self
.assertIn('search %(p)s0 %(p)s1 %(p)s2 %(p)s3\n'
472 '# Total length of all search domains is too long, remaining ones ignored.' % {'p': name_prefix
},
476 if __name__
== '__main__':
477 unittest
.main(testRunner
=unittest
.TextTestRunner(stream
=sys
.stdout
,