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