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