1#!/usr/bin/env python3 2# 3# Exercise QEMU generated ACPI/SMBIOS tables using biosbits, 4# https://biosbits.org/ 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18# 19# 20# Author: 21# Ani Sinha <anisinha@redhat.com> 22 23# pylint: disable=invalid-name 24# pylint: disable=consider-using-f-string 25 26""" 27This is QEMU ACPI/SMBIOS functional tests using biosbits. 28Biosbits is available originally at https://biosbits.org/. 29This test uses a fork of the upstream bits and has numerous fixes 30including an upgraded acpica. The fork is located here: 31https://gitlab.com/qemu-project/biosbits-bits . 32""" 33 34import logging 35import os 36import platform 37import re 38import shutil 39import subprocess 40import tarfile 41import tempfile 42import time 43import zipfile 44 45from pathlib import Path 46from typing import ( 47 List, 48 Optional, 49 Sequence, 50) 51from qemu.machine import QEMUMachine 52from unittest import skipIf 53from qemu_test import QemuBaseTest, Asset 54 55deps = ["xorriso", "mformat"] # dependent tools needed in the test setup/box. 56supported_platforms = ['x86_64'] # supported test platforms. 57 58# default timeout of 120 secs is sometimes not enough for bits test. 59BITS_TIMEOUT = 200 60 61def which(tool): 62 """ looks up the full path for @tool, returns None if not found 63 or if @tool does not have executable permissions. 64 """ 65 paths=os.getenv('PATH') 66 for p in paths.split(os.path.pathsep): 67 p = os.path.join(p, tool) 68 if os.path.exists(p) and os.access(p, os.X_OK): 69 return p 70 return None 71 72def missing_deps(): 73 """ returns True if any of the test dependent tools are absent. 74 """ 75 for dep in deps: 76 if which(dep) is None: 77 return True 78 return False 79 80def supported_platform(): 81 """ checks if the test is running on a supported platform. 82 """ 83 return platform.machine() in supported_platforms 84 85class QEMUBitsMachine(QEMUMachine): # pylint: disable=too-few-public-methods 86 """ 87 A QEMU VM, with isa-debugcon enabled and bits iso passed 88 using -cdrom to QEMU commandline. 89 90 """ 91 def __init__(self, 92 binary: str, 93 args: Sequence[str] = (), 94 wrapper: Sequence[str] = (), 95 name: Optional[str] = None, 96 base_temp_dir: str = "/var/tmp", 97 debugcon_log: str = "debugcon-log.txt", 98 debugcon_addr: str = "0x403", 99 qmp_timer: Optional[float] = None): 100 # pylint: disable=too-many-arguments 101 102 if name is None: 103 name = "qemu-bits-%d" % os.getpid() 104 super().__init__(binary, args, wrapper=wrapper, name=name, 105 base_temp_dir=base_temp_dir, 106 qmp_timer=qmp_timer) 107 self.debugcon_log = debugcon_log 108 self.debugcon_addr = debugcon_addr 109 self.base_temp_dir = base_temp_dir 110 111 @property 112 def _base_args(self) -> List[str]: 113 args = super()._base_args 114 args.extend([ 115 '-chardev', 116 'file,path=%s,id=debugcon' %os.path.join(self.base_temp_dir, 117 self.debugcon_log), 118 '-device', 119 'isa-debugcon,iobase=%s,chardev=debugcon' %self.debugcon_addr, 120 ]) 121 return args 122 123 def base_args(self): 124 """return the base argument to QEMU binary""" 125 return self._base_args 126 127@skipIf(not supported_platform() or missing_deps(), 128 'unsupported platform or dependencies (%s) not installed' \ 129 % ','.join(deps)) 130class AcpiBitsTest(QemuBaseTest): #pylint: disable=too-many-instance-attributes 131 """ 132 ACPI and SMBIOS tests using biosbits. 133 """ 134 # in slower systems the test can take as long as 3 minutes to complete. 135 timeout = BITS_TIMEOUT 136 137 # following are some standard configuration constants 138 # gitlab CI does shallow clones of depth 20 139 BITS_INTERNAL_VER = 2020 140 # commit hash must match the artifact tag below 141 BITS_COMMIT_HASH = 'c7920d2b' 142 # this is the latest bits release as of today. 143 BITS_TAG = "qemu-bits-10262023" 144 145 ASSET_BITS = Asset(("https://gitlab.com/qemu-project/" 146 "biosbits-bits/-/jobs/artifacts/%s/" 147 "download?job=qemu-bits-build" % BITS_TAG), 148 '1b8dd612c6831a6b491716a77acc486666aaa867051cdc34f7ce169c2e25f487') 149 150 def __init__(self, *args, **kwargs): 151 super().__init__(*args, **kwargs) 152 self._vm = None 153 self._workDir = None 154 self._baseDir = None 155 156 self._debugcon_addr = '0x403' 157 self._debugcon_log = 'debugcon-log.txt' 158 self.logger = self.log 159 160 def _print_log(self, log): 161 self.logger.info('\nlogs from biosbits follows:') 162 self.logger.info('==========================================\n') 163 self.logger.info(log) 164 self.logger.info('==========================================\n') 165 166 def copy_bits_config(self): 167 """ copies the bios bits config file into bits. 168 """ 169 config_file = 'bits-cfg.txt' 170 bits_config_dir = os.path.join(self._baseDir, 'acpi-bits', 171 'bits-config') 172 target_config_dir = os.path.join(self._workDir, 173 'bits-%d' %self.BITS_INTERNAL_VER, 174 'boot') 175 self.assertTrue(os.path.exists(bits_config_dir)) 176 self.assertTrue(os.path.exists(target_config_dir)) 177 self.assertTrue(os.access(os.path.join(bits_config_dir, 178 config_file), os.R_OK)) 179 shutil.copy2(os.path.join(bits_config_dir, config_file), 180 target_config_dir) 181 self.logger.info('copied config file %s to %s', 182 config_file, target_config_dir) 183 184 def copy_test_scripts(self): 185 """copies the python test scripts into bits. """ 186 187 bits_test_dir = os.path.join(self._baseDir, 'acpi-bits', 188 'bits-tests') 189 target_test_dir = os.path.join(self._workDir, 190 'bits-%d' %self.BITS_INTERNAL_VER, 191 'boot', 'python') 192 193 self.assertTrue(os.path.exists(bits_test_dir)) 194 self.assertTrue(os.path.exists(target_test_dir)) 195 196 for filename in os.listdir(bits_test_dir): 197 if os.path.isfile(os.path.join(bits_test_dir, filename)) and \ 198 filename.endswith('.py2'): 199 # all test scripts are named with extension .py2 so that 200 # avocado does not try to load them. These scripts are 201 # written for python 2.7 not python 3 and hence if avocado 202 # loaded them, it would complain about python 3 specific 203 # syntaxes. 204 newfilename = os.path.splitext(filename)[0] + '.py' 205 shutil.copy2(os.path.join(bits_test_dir, filename), 206 os.path.join(target_test_dir, newfilename)) 207 self.logger.info('copied test file %s to %s', 208 filename, target_test_dir) 209 210 # now remove the pyc test file if it exists, otherwise the 211 # changes in the python test script won't be executed. 212 testfile_pyc = os.path.splitext(filename)[0] + '.pyc' 213 if os.access(os.path.join(target_test_dir, testfile_pyc), 214 os.F_OK): 215 os.remove(os.path.join(target_test_dir, testfile_pyc)) 216 self.logger.info('removed compiled file %s', 217 os.path.join(target_test_dir, 218 testfile_pyc)) 219 220 def fix_mkrescue(self, mkrescue): 221 """ grub-mkrescue is a bash script with two variables, 'prefix' and 222 'libdir'. They must be pointed to the right location so that the 223 iso can be generated appropriately. We point the two variables to 224 the directory where we have extracted our pre-built bits grub 225 tarball. 226 """ 227 grub_x86_64_mods = os.path.join(self._workDir, 'grub-inst-x86_64-efi') 228 grub_i386_mods = os.path.join(self._workDir, 'grub-inst') 229 230 self.assertTrue(os.path.exists(grub_x86_64_mods)) 231 self.assertTrue(os.path.exists(grub_i386_mods)) 232 233 new_script = "" 234 with open(mkrescue, 'r', encoding='utf-8') as filehandle: 235 orig_script = filehandle.read() 236 new_script = re.sub('(^prefix=)(.*)', 237 r'\1"%s"' %grub_x86_64_mods, 238 orig_script, flags=re.M) 239 new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods, 240 new_script, flags=re.M) 241 242 with open(mkrescue, 'w', encoding='utf-8') as filehandle: 243 filehandle.write(new_script) 244 245 def generate_bits_iso(self): 246 """ Uses grub-mkrescue to generate a fresh bits iso with the python 247 test scripts 248 """ 249 bits_dir = os.path.join(self._workDir, 250 'bits-%d' %self.BITS_INTERNAL_VER) 251 iso_file = os.path.join(self._workDir, 252 'bits-%d.iso' %self.BITS_INTERNAL_VER) 253 mkrescue_script = os.path.join(self._workDir, 254 'grub-inst-x86_64-efi', 'bin', 255 'grub-mkrescue') 256 257 self.assertTrue(os.access(mkrescue_script, 258 os.R_OK | os.W_OK | os.X_OK)) 259 260 self.fix_mkrescue(mkrescue_script) 261 262 self.logger.info('using grub-mkrescue for generating biosbits iso ...') 263 264 try: 265 if os.getenv('V') or os.getenv('BITS_DEBUG'): 266 proc = subprocess.run([mkrescue_script, '-o', iso_file, 267 bits_dir], 268 stdout=subprocess.PIPE, 269 stderr=subprocess.STDOUT, 270 check=True) 271 self.logger.info("grub-mkrescue output %s" % proc.stdout) 272 else: 273 subprocess.check_call([mkrescue_script, '-o', 274 iso_file, bits_dir], 275 stderr=subprocess.DEVNULL, 276 stdout=subprocess.DEVNULL) 277 except Exception as e: # pylint: disable=broad-except 278 self.skipTest("Error while generating the bits iso. " 279 "Pass V=1 in the environment to get more details. " 280 + str(e)) 281 282 self.assertTrue(os.access(iso_file, os.R_OK)) 283 284 self.logger.info('iso file %s successfully generated.', iso_file) 285 286 def setUp(self): # pylint: disable=arguments-differ 287 super().setUp('qemu-system-') 288 self.logger = self.log 289 290 self._baseDir = Path(__file__).parent 291 292 # workdir could also be avocado's own workdir in self.workdir. 293 # At present, I prefer to maintain my own temporary working 294 # directory. It gives us more control over the generated bits 295 # log files and also for debugging, we may chose not to remove 296 # this working directory so that the logs and iso can be 297 # inspected manually and archived if needed. 298 self._workDir = tempfile.mkdtemp(prefix='acpi-bits-', 299 suffix='.tmp') 300 self.logger.info('working dir: %s', self._workDir) 301 302 prebuiltDir = os.path.join(self._workDir, 'prebuilt') 303 if not os.path.isdir(prebuiltDir): 304 os.mkdir(prebuiltDir, mode=0o775) 305 306 bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip' 307 %(self.BITS_INTERNAL_VER, 308 self.BITS_COMMIT_HASH)) 309 grub_tar_file = os.path.join(prebuiltDir, 310 'bits-%d-%s-grub.tar.gz' 311 %(self.BITS_INTERNAL_VER, 312 self.BITS_COMMIT_HASH)) 313 314 bitsLocalArtLoc = self.ASSET_BITS.fetch() 315 self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc) 316 317 # extract the bits artifact in the temp working directory 318 with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref: 319 zref.extractall(prebuiltDir) 320 321 # extract the bits software in the temp working directory 322 with zipfile.ZipFile(bits_zip_file, 'r') as zref: 323 zref.extractall(self._workDir) 324 325 with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball: 326 tarball.extractall(self._workDir) 327 328 self.copy_test_scripts() 329 self.copy_bits_config() 330 self.generate_bits_iso() 331 332 def parse_log(self): 333 """parse the log generated by running bits tests and 334 check for failures. 335 """ 336 debugconf = os.path.join(self._workDir, self._debugcon_log) 337 log = "" 338 with open(debugconf, 'r', encoding='utf-8') as filehandle: 339 log = filehandle.read() 340 341 matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*', 342 log) 343 for match in matchiter: 344 # verify that no test cases failed. 345 try: 346 self.assertEqual(match.group(3).split()[0], '0', 347 'Some bits tests seems to have failed. ' \ 348 'Please check the test logs for more info.') 349 except AssertionError as e: 350 self._print_log(log) 351 raise e 352 else: 353 if os.getenv('V') or os.getenv('BITS_DEBUG'): 354 self._print_log(log) 355 356 def tearDown(self): 357 """ 358 Lets do some cleanups. 359 """ 360 if self._vm: 361 self.assertFalse(not self._vm.is_running) 362 if not os.getenv('BITS_DEBUG') and self._workDir: 363 self.logger.info('removing the work directory %s', self._workDir) 364 shutil.rmtree(self._workDir) 365 else: 366 self.logger.info('not removing the work directory %s ' \ 367 'as BITS_DEBUG is ' \ 368 'passed in the environment', self._workDir) 369 super().tearDown() 370 371 def test_acpi_smbios_bits(self): 372 """The main test case implementation.""" 373 374 iso_file = os.path.join(self._workDir, 375 'bits-%d.iso' %self.BITS_INTERNAL_VER) 376 377 self.assertTrue(os.access(iso_file, os.R_OK)) 378 379 self._vm = QEMUBitsMachine(binary=self.qemu_bin, 380 base_temp_dir=self._workDir, 381 debugcon_log=self._debugcon_log, 382 debugcon_addr=self._debugcon_addr) 383 384 self._vm.add_args('-cdrom', '%s' %iso_file) 385 # the vm needs to be run under icount so that TCG emulation is 386 # consistent in terms of timing. smilatency tests have consistent 387 # timing requirements. 388 self._vm.add_args('-icount', 'auto') 389 # currently there is no support in bits for recognizing 64-bit SMBIOS 390 # entry points. QEMU defaults to 64-bit entry points since the 391 # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0 392 # for newer machine models"). Therefore, enforce 32-bit entry point. 393 self._vm.add_args('-machine', 'smbios-entry-point-type=32') 394 395 # enable console logging 396 self._vm.set_console() 397 self._vm.launch() 398 399 400 # biosbits has been configured to run all the specified test suites 401 # in batch mode and then automatically initiate a vm shutdown. 402 # Set timeout to BITS_TIMEOUT for SHUTDOWN event from bits VM at par 403 # with the avocado test timeout. 404 self._vm.event_wait('SHUTDOWN', timeout=BITS_TIMEOUT) 405 self._vm.wait(timeout=None) 406 self.logger.debug("Checking console output ...") 407 self.parse_log() 408 409if __name__ == '__main__': 410 QemuBaseTest.main() 411