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