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