]> git.ipfire.org Git - thirdparty/systemd.git/blame - test/networkd-test.py
shared: simplify dns_name_hash_func() end of name detection
[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.
8# This can be run on a normal installation, in QEMU, nspawn, or LXC.
9# ATTENTION: This uses the *installed* networkd, not the one from the built
10# source tree.
11#
12# (C) 2015 Canonical Ltd.
13# Author: Martin Pitt <martin.pitt@ubuntu.com>
14#
15# systemd is free software; you can redistribute it and/or modify it
16# under the terms of the GNU Lesser General Public License as published by
17# the Free Software Foundation; either version 2.1 of the License, or
18# (at your option) any later version.
19
20# systemd is distributed in the hope that it will be useful, but
21# WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23# Lesser General Public License for more details.
24#
25# You should have received a copy of the GNU Lesser General Public License
26# along with systemd; If not, see <http://www.gnu.org/licenses/>.
27
28import os
29import sys
30import time
31import unittest
32import tempfile
33import subprocess
34import shutil
35
36networkd_active = subprocess.call(['systemctl', 'is-active', '--quiet',
37 'systemd-networkd']) == 0
38have_dnsmasq = shutil.which('dnsmasq')
39
40
41@unittest.skipIf(networkd_active,
42 'networkd is already active')
43class ClientTestBase:
44 def setUp(self):
45 self.iface = 'test_eth42'
46 self.if_router = 'router_eth42'
47 self.workdir_obj = tempfile.TemporaryDirectory()
48 self.workdir = self.workdir_obj.name
49 self.config = '/run/systemd/network/test_eth42.network'
50 os.makedirs(os.path.dirname(self.config), exist_ok=True)
51
52 # avoid "Failed to open /dev/tty" errors in containers
53 os.environ['SYSTEMD_LOG_TARGET'] = 'journal'
54
55 # determine path to systemd-networkd-wait-online
56 for p in ['/usr/lib/systemd/systemd-networkd-wait-online',
57 '/lib/systemd/systemd-networkd-wait-online']:
58 if os.path.exists(p):
59 self.networkd_wait_online = p
60 break
61 else:
62 self.fail('systemd-networkd-wait-online not found')
63
64 # get current journal cursor
65 out = subprocess.check_output(['journalctl', '-b', '--quiet',
66 '--no-pager', '-n0', '--show-cursor'],
67 universal_newlines=True)
68 self.assertTrue(out.startswith('-- cursor:'))
69 self.journal_cursor = out.split()[-1]
70
71 def tearDown(self):
72 self.shutdown_iface()
73 if os.path.exists(self.config):
74 os.unlink(self.config)
75 subprocess.call(['systemctl', 'stop', 'systemd-networkd'])
76
77 def show_journal(self, unit):
78 '''Show journal of given unit since start of the test'''
79
80 print('---- %s ----' % unit)
81 sys.stdout.flush()
82 subprocess.call(['journalctl', '-b', '--no-pager', '--quiet',
83 '--cursor', self.journal_cursor, '-u', unit])
84
85 def create_iface(self, ipv6=False):
86 '''Create test interface with DHCP server behind it'''
87
88 raise NotImplementedError('must be implemented by a subclass')
89
90 def shutdown_iface(self):
91 '''Remove test interface and stop DHCP server'''
92
93 raise NotImplementedError('must be implemented by a subclass')
94
95 def print_server_log(self):
96 '''Print DHCP server log for debugging failures'''
97
98 raise NotImplementedError('must be implemented by a subclass')
99
100 def do_test(self, coldplug=True, ipv6=False, extra_opts='',
101 online_timeout=10, dhcp_mode='yes'):
102 with open(self.config, 'w') as f:
103 f.write('''[Match]
104Name=%s
105[Network]
106DHCP=%s
107%s''' % (self.iface, dhcp_mode, extra_opts))
108
109 if coldplug:
110 # create interface first, then start networkd
111 self.create_iface(ipv6=ipv6)
112 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
113 else:
114 # start networkd first, then create interface
115 subprocess.check_call(['systemctl', 'start', 'systemd-networkd'])
116 self.create_iface(ipv6=ipv6)
117
118 try:
119 subprocess.check_call([self.networkd_wait_online, '--interface',
120 self.iface, '--timeout=%i' % online_timeout])
121
122 if ipv6:
123 # check iface state and IP 6 address; FIXME: we need to wait a bit
124 # longer, as the iface is "configured" already with IPv4 *or*
125 # IPv6, but we want to wait for both
126 for timeout in range(10):
127 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.iface])
128 if b'state UP' in out and b'inet6 2600' in out and b'inet 192.168' in out:
129 break
130 time.sleep(1)
131 else:
132 self.fail('timed out waiting for IPv6 configuration')
133
134 self.assertRegex(out, b'inet6 2600::.* scope global .*dynamic')
135 self.assertRegex(out, b'inet6 fe80::.* scope link')
136 else:
137 # should have link-local address on IPv6 only
138 out = subprocess.check_output(['ip', '-6', 'a', 'show', 'dev', self.iface])
139 self.assertRegex(out, b'inet6 fe80::.* scope link')
140 self.assertNotIn(b'scope global', out)
141
142 # should have IPv4 address
143 out = subprocess.check_output(['ip', '-4', 'a', 'show', 'dev', self.iface])
144 self.assertIn(b'state UP', out)
145 self.assertRegex(out, b'inet 192.168.5.\d+/.* scope global dynamic')
146
147 # check networkctl state
148 out = subprocess.check_output(['networkctl'])
149 self.assertRegex(out, ('%s\s+ether\s+routable\s+unmanaged' % self.if_router).encode())
150 self.assertRegex(out, ('%s\s+ether\s+routable\s+configured' % self.iface).encode())
151
152 out = subprocess.check_output(['networkctl', 'status', self.iface])
153 self.assertRegex(out, b'Type:\s+ether')
154 self.assertRegex(out, b'State:\s+routable.*configured')
155 self.assertRegex(out, b'Address:\s+192.168.5.\d+')
156 if ipv6:
157 self.assertRegex(out, b'2600::')
158 else:
159 self.assertNotIn(b'2600::', out)
160 self.assertRegex(out, b'fe80::')
161 self.assertRegex(out, b'Gateway:\s+192.168.5.1')
162 self.assertRegex(out, b'DNS:\s+192.168.5.1')
163 except (AssertionError, subprocess.CalledProcessError):
164 # show networkd status, journal, and DHCP server log on failure
165 with open(self.config) as f:
166 print('\n---- %s ----\n%s' % (self.config, f.read()))
167 print('---- interface status ----')
168 sys.stdout.flush()
169 subprocess.call(['ip', 'a', 'show', 'dev', self.iface])
170 print('---- networkctl status %s ----' % self.iface)
171 sys.stdout.flush()
172 subprocess.call(['networkctl', 'status', self.iface])
173 self.show_journal('systemd-networkd.service')
174 self.print_server_log()
175 raise
176
177 # verify resolv.conf if it gets dynamically managed
178 if os.path.islink('/etc/resolv.conf'):
179 for timeout in range(50):
180 with open('/etc/resolv.conf') as f:
181 contents = f.read()
182 if 'nameserver 192.168.5.1\n' in contents:
183 break
184 # resolv.conf can have at most three nameservers; if we already
185 # have three different ones, that's also okay
186 if contents.count('nameserver ') >= 3:
187 break
188 time.sleep(0.1)
189 else:
190 self.fail('nameserver 192.168.5.1 not found in /etc/resolv.conf')
191
192 if not coldplug:
193 # check post-down.d hook
194 self.shutdown_iface()
195
196 def test_coldplug_dhcp_yes_ip4(self):
197 # we have a 12s timeout on RA, so we need to wait longer
198 self.do_test(coldplug=True, ipv6=False, online_timeout=15)
199
200 def test_coldplug_dhcp_yes_ip4_no_ra(self):
201 # with disabling RA explicitly things should be fast
202 self.do_test(coldplug=True, ipv6=False,
203 extra_opts='IPv6AcceptRouterAdvertisements=False')
204
205 def test_coldplug_dhcp_ip4_only(self):
206 # we have a 12s timeout on RA, so we need to wait longer
207 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
208 online_timeout=15)
209
210 def test_coldplug_dhcp_ip4_only_no_ra(self):
211 # with disabling RA explicitly things should be fast
212 self.do_test(coldplug=True, ipv6=False, dhcp_mode='ipv4',
213 extra_opts='IPv6AcceptRouterAdvertisements=False')
214
215 def test_coldplug_dhcp_ip6(self):
216 self.do_test(coldplug=True, ipv6=True)
217
218 def test_hotplug_dhcp_ip4(self):
219 # With IPv4 only we have a 12s timeout on RA, so we need to wait longer
220 self.do_test(coldplug=False, ipv6=False, online_timeout=15)
221
222 def test_hotplug_dhcp_ip6(self):
223 self.do_test(coldplug=False, ipv6=True)
224
225
226@unittest.skipUnless(have_dnsmasq, 'dnsmasq not installed')
227class DnsmasqClientTest(ClientTestBase, unittest.TestCase):
228 '''Test networkd client against dnsmasq'''
229
230 def setUp(self):
231 super().setUp()
232 self.dnsmasq = None
233
234 def create_iface(self, ipv6=False):
235 '''Create test interface with DHCP server behind it'''
236
237 # add veth pair
238 subprocess.check_call(['ip', 'link', 'add', 'name', self.iface, 'type',
239 'veth', 'peer', 'name', self.if_router])
240
241 # give our router an IP
242 subprocess.check_call(['ip', 'a', 'flush', 'dev', self.if_router])
243 subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', self.if_router])
244 if ipv6:
245 subprocess.check_call(['ip', 'a', 'add', '2600::1/64', 'dev', self.if_router])
246 subprocess.check_call(['ip', 'link', 'set', self.if_router, 'up'])
247
248 # add DHCP server
249 self.dnsmasq_log = os.path.join(self.workdir, 'dnsmasq.log')
250 lease_file = os.path.join(self.workdir, 'dnsmasq.leases')
251 if ipv6:
252 extra_opts = ['--enable-ra', '--dhcp-range=2600::10,2600::20']
253 else:
254 extra_opts = []
255 self.dnsmasq = subprocess.Popen(
256 ['dnsmasq', '--keep-in-foreground', '--log-queries',
257 '--log-facility=' + self.dnsmasq_log, '--conf-file=/dev/null',
258 '--dhcp-leasefile=' + lease_file, '--bind-interfaces',
259 '--interface=' + self.if_router, '--except-interface=lo',
260 '--dhcp-range=192.168.5.10,192.168.5.200'] + extra_opts)
261
262 def shutdown_iface(self):
263 '''Remove test interface and stop DHCP server'''
264
265 if self.if_router:
266 subprocess.check_call(['ip', 'link', 'del', 'dev', self.if_router])
267 self.if_router = None
268 if self.dnsmasq:
269 self.dnsmasq.kill()
270 self.dnsmasq.wait()
271 self.dnsmasq = None
272
273 def print_server_log(self):
274 '''Print DHCP server log for debugging failures'''
275
276 with open(self.dnsmasq_log) as f:
277 sys.stdout.write('\n\n---- dnsmasq log ----\n%s\n------\n\n' % f.read())
278
279
280class NetworkdClientTest(ClientTestBase, unittest.TestCase):
281 '''Test networkd client against networkd server'''
282
283 def setUp(self):
284 super().setUp()
285 self.dnsmasq = None
286
287 def create_iface(self, ipv6=False):
288 '''Create test interface with DHCP server behind it'''
289
290 # run "router-side" networkd in own mount namespace to shield it from
291 # "client-side" configuration and networkd
292 (fd, script) = tempfile.mkstemp(prefix='networkd-router.sh')
293 self.addCleanup(os.remove, script)
294 with os.fdopen(fd, 'w+') as f:
295 f.write('''#!/bin/sh -eu
296mkdir -p /run/systemd/network
297mkdir -p /run/systemd/netif
298mount -t tmpfs none /run/systemd/network
299mount -t tmpfs none /run/systemd/netif
300[ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus
301# create router/client veth pair
302cat << EOF > /run/systemd/network/test.netdev
303[NetDev]
304Name=%(ifr)s
305Kind=veth
306
307[Peer]
308Name=%(ifc)s
309EOF
310
311cat << EOF > /run/systemd/network/test.network
312[Match]
313Name=%(ifr)s
314
315[Network]
316Address=192.168.5.1/24
317%(addr6)s
318DHCPServer=yes
319
320[DHCPServer]
321PoolOffset=10
322PoolSize=50
323DNS=192.168.5.1
324EOF
325
326# run networkd as in systemd-networkd.service
327exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ { s/^.*=//; p}')
328''' % {'ifr': self.if_router, 'ifc': self.iface, 'addr6': ipv6 and 'Address=2600::1/64' or ''})
329
330 os.fchmod(fd, 0o755)
331
332 subprocess.check_call(['systemd-run', '--unit=networkd-test-router.service',
333 '-p', 'InaccessibleDirectories=-/etc/systemd/network',
334 '-p', 'InaccessibleDirectories=-/run/systemd/network',
335 '-p', 'InaccessibleDirectories=-/run/systemd/netif',
336 '--service-type=notify', script])
337
338 # wait until devices got created
339 for timeout in range(50):
340 out = subprocess.check_output(['ip', 'a', 'show', 'dev', self.if_router])
341 if b'state UP' in out and b'scope global' in out:
342 break
343 time.sleep(0.1)
344
345 def shutdown_iface(self):
346 '''Remove test interface and stop DHCP server'''
347
348 if self.if_router:
349 subprocess.check_call(['systemctl', 'stop', 'networkd-test-router.service'])
350 # ensure failed transient unit does not stay around
351 subprocess.call(['systemctl', 'reset-failed', 'networkd-test-router.service'])
352 subprocess.call(['ip', 'link', 'del', 'dev', self.if_router])
353 self.if_router = None
354
355 def print_server_log(self):
356 '''Print DHCP server log for debugging failures'''
357
358 self.show_journal('networkd-test-router.service')
359
360 @unittest.skip('networkd does not have DHCPv6 server support')
361 def test_hotplug_dhcp_ip6(self):
362 pass
363
364 @unittest.skip('networkd does not have DHCPv6 server support')
365 def test_coldplug_dhcp_ip6(self):
366 pass
367
368
369if __name__ == '__main__':
370 unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout,
371 verbosity=2))