1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7import re
8import threading
9import time
10
11from oeqa.runtime.case import OERuntimeTestCase
12from oeqa.core.decorator.depends import OETestDepends
13from oeqa.core.decorator.data import skipIfDataVar, skipIfNotDataVar
14from oeqa.runtime.decorator.package import OEHasPackage
15from oeqa.core.decorator.data import skipIfNotFeature, skipIfFeature
16
17class SystemdTest(OERuntimeTestCase):
18
19    def systemctl(self, action='', target='', expected=0, verbose=False):
20        command = 'SYSTEMD_BUS_TIMEOUT=240s systemctl %s %s' % (action, target)
21        status, output = self.target.run(command)
22        message = '\n'.join([command, output])
23        if status != expected and verbose:
24            cmd = 'SYSTEMD_BUS_TIMEOUT=240s systemctl status --full %s' % target
25            message += self.target.run(cmd)[1]
26        self.assertEqual(status, expected, message)
27        return output
28
29    #TODO: use pyjournalctl instead
30    def journalctl(self, args='',l_match_units=None):
31        """
32        Request for the journalctl output to the current target system
33
34        Arguments:
35        -args, an optional argument pass through argument
36        -l_match_units, an optional list of units to filter the output
37        Returns:
38        -string output of the journalctl command
39        Raises:
40        -AssertionError, on remote commands that fail
41        -ValueError, on a journalctl call with filtering by l_match_units that
42        returned no entries
43        """
44
45        query_units=''
46        if l_match_units:
47            query_units = ['_SYSTEMD_UNIT='+unit for unit in l_match_units]
48            query_units = ' '.join(query_units)
49        command = 'journalctl %s %s' %(args, query_units)
50        status, output = self.target.run(command)
51        if status:
52            raise AssertionError("Command '%s' returned non-zero exit "
53                    'code %d:\n%s' % (command, status, output))
54        if len(output) == 1 and "-- No entries --" in output:
55            raise ValueError('List of units to match: %s, returned no entries'
56                    % l_match_units)
57        return output
58
59class SystemdBasicTests(SystemdTest):
60
61    def settle(self):
62        """
63        Block until systemd has finished activating any units being activated,
64        or until two minutes has elapsed.
65
66        Returns a tuple, either (True, '') if all units have finished
67        activating, or (False, message string) if there are still units
68        activating (generally, failing units that restart).
69        """
70        endtime = time.time() + (60 * 2)
71        while True:
72            status, output = self.target.run('SYSTEMD_BUS_TIMEOUT=240s systemctl is-system-running')
73            if "running" in output or "degraded" in output:
74                return (True, '')
75            if time.time() >= endtime:
76                return (False, output)
77            time.sleep(10)
78
79    @skipIfNotFeature('systemd',
80                      'Test requires systemd to be in DISTRO_FEATURES')
81    @skipIfNotDataVar('VIRTUAL-RUNTIME_init_manager', 'systemd',
82                      'systemd is not the init manager for this image')
83    @OETestDepends(['ssh.SSHTest.test_ssh'])
84    def test_systemd_basic(self):
85        self.systemctl('--version')
86
87    @OETestDepends(['systemd.SystemdBasicTests.test_systemd_basic'])
88    def test_systemd_list(self):
89        self.systemctl('list-unit-files')
90
91    @OETestDepends(['systemd.SystemdBasicTests.test_systemd_basic'])
92    def test_systemd_failed(self):
93        settled, output = self.settle()
94        msg = "Timed out waiting for systemd to settle:\n%s" % output
95        self.assertTrue(settled, msg=msg)
96
97        output = self.systemctl('list-units', '--failed')
98        match = re.search('0 loaded units listed', output)
99        if not match:
100            output += self.systemctl('status --full --failed')
101        self.assertTrue(match, msg='Some systemd units failed:\n%s' % output)
102
103
104class SystemdServiceTests(SystemdTest):
105
106    @OEHasPackage(['avahi-daemon'])
107    @OETestDepends(['systemd.SystemdBasicTests.test_systemd_basic'])
108    def test_systemd_status(self):
109        self.systemctl('status --full', 'avahi-daemon.service')
110
111    @OETestDepends(['systemd.SystemdServiceTests.test_systemd_status'])
112    def test_systemd_stop_start(self):
113        self.systemctl('stop', 'avahi-daemon.service')
114        self.systemctl('is-active', 'avahi-daemon.service',
115                       expected=3, verbose=True)
116        self.systemctl('start','avahi-daemon.service')
117        self.systemctl('is-active', 'avahi-daemon.service', verbose=True)
118
119    @OETestDepends(['systemd.SystemdServiceTests.test_systemd_status'])
120    @skipIfFeature('read-only-rootfs',
121                   'Test is only meant to run without read-only-rootfs in IMAGE_FEATURES')
122    def test_systemd_disable_enable(self):
123        self.systemctl('disable', 'avahi-daemon.service')
124        self.systemctl('is-enabled', 'avahi-daemon.service', expected=1)
125        self.systemctl('enable', 'avahi-daemon.service')
126        self.systemctl('is-enabled', 'avahi-daemon.service')
127
128    @OETestDepends(['systemd.SystemdServiceTests.test_systemd_status'])
129    @skipIfNotFeature('read-only-rootfs',
130                      'Test is only meant to run with read-only-rootfs in IMAGE_FEATURES')
131    def test_systemd_disable_enable_ro(self):
132        status = self.target.run('mount -orw,remount /')[0]
133        self.assertTrue(status == 0, msg='Remounting / as r/w failed')
134        try:
135            self.test_systemd_disable_enable()
136        finally:
137            status = self.target.run('mount -oro,remount /')[0]
138            self.assertTrue(status == 0, msg='Remounting / as r/o failed')
139
140    @OETestDepends(['systemd.SystemdBasicTests.test_systemd_basic'])
141    @skipIfNotFeature('minidebuginfo', 'Test requires minidebuginfo to be in DISTRO_FEATURES')
142    @OEHasPackage(['busybox'])
143    def test_systemd_coredump_minidebuginfo(self):
144        """
145        Verify that call-stacks generated by systemd-coredump contain symbolicated call-stacks,
146        extracted from the minidebuginfo metadata (.gnu_debugdata elf section).
147        """
148        # use "env sleep" instead of "sleep" to avoid calling the shell builtin function
149        t_thread = threading.Thread(target=self.target.run, args=("ulimit -c unlimited && env sleep 1000",))
150        t_thread.start()
151        time.sleep(1)
152
153        status, sleep_pid = self.target.run('pidof sleep')
154        # cause segfault on purpose
155        self.target.run('kill -SEGV %s' % sleep_pid)
156        self.assertEqual(status, 0, msg = 'Not able to find process that runs sleep, output : %s' % sleep_pid)
157
158        # Give some time to systemd-coredump@.service to process the coredump
159        for x in range(20):
160            status, output = self.target.run('coredumpctl list %s' % sleep_pid)
161            if status == 0:
162                break
163            time.sleep(1)
164        else:
165            self.fail("Timed out waiting for coredump creation")
166
167        (status, output) = self.target.run('coredumpctl info %s' % sleep_pid)
168        self.assertEqual(status, 0, msg='MiniDebugInfo Test failed: %s' % output)
169        self.assertEqual('sleep_for_duration (busybox.nosuid' in output or 'xnanosleep (sleep.coreutils' in output,
170                         True, msg='Call stack is missing minidebuginfo symbols (functions shown as "n/a"): %s' % output)
171
172class SystemdJournalTests(SystemdTest):
173
174    @OETestDepends(['systemd.SystemdBasicTests.test_systemd_basic'])
175    def test_systemd_journal(self):
176        status, output = self.target.run('journalctl')
177        self.assertEqual(status, 0, output)
178
179    @OETestDepends(['systemd.SystemdBasicTests.test_systemd_basic'])
180    def test_systemd_boot_time(self, systemd_TimeoutStartSec=90):
181        """
182        Get the target boot time from journalctl and log it
183
184        Arguments:
185        -systemd_TimeoutStartSec, an optional argument containing systemd's
186        unit start timeout to compare against
187        """
188
189        # The expression chain that uniquely identifies the time boot message.
190        expr_items=['Startup finished', 'kernel', 'userspace', r'\.$']
191        try:
192            output = self.journalctl(args='-o cat --reverse')
193        except AssertionError:
194            self.fail('Error occurred while calling journalctl')
195        if not len(output):
196            self.fail('Error, unable to get startup time from systemd journal')
197
198        # Check for the regular expression items that match the startup time.
199        for line in output.split('\n'):
200            check_match = ''.join(re.findall('.*'.join(expr_items), line))
201            if check_match:
202                break
203        # Put the startup time in the test log
204        if check_match:
205            self.tc.logger.info('%s' % check_match)
206        else:
207            self.skipTest('Error at obtaining the boot time from journalctl')
208        boot_time_sec = 0
209
210        # Get the numeric values from the string and convert them to seconds
211        # same data will be placed in list and string for manipulation.
212        l_boot_time = check_match.split(' ')[-2:]
213        s_boot_time = ' '.join(l_boot_time)
214        try:
215            # Obtain the minutes it took to boot.
216            if l_boot_time[0].endswith('min') and l_boot_time[0][0].isdigit():
217                boot_time_min = s_boot_time.split('min')[0]
218                # Convert to seconds and accumulate it.
219                boot_time_sec += int(boot_time_min) * 60
220            # Obtain the seconds it took to boot and accumulate.
221            boot_time_sec += float(l_boot_time[1].split('s')[0])
222        except ValueError:
223            self.skipTest('Error when parsing time from boot string')
224
225        # Assert the target boot time against systemd's unit start timeout.
226        if boot_time_sec > systemd_TimeoutStartSec:
227            msg = ("Target boot time %s exceeds systemd's TimeoutStartSec %s"
228                    % (boot_time_sec, systemd_TimeoutStartSec))
229            self.tc.logger.info(msg)
230