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