1#!/usr/bin/env python3 2# group: rw quick 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 avocado 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 44from typing import ( 45 List, 46 Optional, 47 Sequence, 48) 49from qemu.machine import QEMUMachine 50from avocado import skipIf 51from avocado.utils import datadrainer as drainer 52from avocado_qemu import QemuBaseTest 53 54deps = ["xorriso", "mformat"] # dependent tools needed in the test setup/box. 55supported_platforms = ['x86_64'] # supported test platforms. 56 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(QemuBaseTest): #pylint: disable=too-many-instance-attributes 128 """ 129 ACPI and SMBIOS tests using biosbits. 130 131 :avocado: tags=arch:x86_64 132 :avocado: tags=acpi 133 134 """ 135 # in slower systems the test can take as long as 3 minutes to complete. 136 timeout = 200 137 138 def __init__(self, *args, **kwargs): 139 super().__init__(*args, **kwargs) 140 self._vm = None 141 self._workDir = None 142 self._baseDir = None 143 144 # following are some standard configuration constants 145 self._bitsInternalVer = 2020 # gitlab CI does shallow clones of depth 20 146 self._bitsCommitHash = 'c7920d2b' # commit hash must match 147 # the artifact tag below 148 self._bitsTag = "qemu-bits-10262023" # this is the latest bits 149 # release as of today. 150 self._bitsArtSHA1Hash = 'b22cdfcfc7453875297d06d626f5474ee36a343f' 151 self._bitsArtURL = ("https://gitlab.com/qemu-project/" 152 "biosbits-bits/-/jobs/artifacts/%s/" 153 "download?job=qemu-bits-build" %self._bitsTag) 154 self._debugcon_addr = '0x403' 155 self._debugcon_log = 'debugcon-log.txt' 156 logging.basicConfig(level=logging.INFO) 157 self.logger = logging.getLogger('acpi-bits') 158 159 def _print_log(self, log): 160 self.logger.info('\nlogs from biosbits follows:') 161 self.logger.info('==========================================\n') 162 self.logger.info(log) 163 self.logger.info('==========================================\n') 164 165 def copy_bits_config(self): 166 """ copies the bios bits config file into bits. 167 """ 168 config_file = 'bits-cfg.txt' 169 bits_config_dir = os.path.join(self._baseDir, 'acpi-bits', 170 'bits-config') 171 target_config_dir = os.path.join(self._workDir, 172 'bits-%d' %self._bitsInternalVer, 173 'boot') 174 self.assertTrue(os.path.exists(bits_config_dir)) 175 self.assertTrue(os.path.exists(target_config_dir)) 176 self.assertTrue(os.access(os.path.join(bits_config_dir, 177 config_file), os.R_OK)) 178 shutil.copy2(os.path.join(bits_config_dir, config_file), 179 target_config_dir) 180 self.logger.info('copied config file %s to %s', 181 config_file, target_config_dir) 182 183 def copy_test_scripts(self): 184 """copies the python test scripts into bits. """ 185 186 bits_test_dir = os.path.join(self._baseDir, 'acpi-bits', 187 'bits-tests') 188 target_test_dir = os.path.join(self._workDir, 189 'bits-%d' %self._bitsInternalVer, 190 'boot', 'python') 191 192 self.assertTrue(os.path.exists(bits_test_dir)) 193 self.assertTrue(os.path.exists(target_test_dir)) 194 195 for filename in os.listdir(bits_test_dir): 196 if os.path.isfile(os.path.join(bits_test_dir, filename)) and \ 197 filename.endswith('.py2'): 198 # all test scripts are named with extension .py2 so that 199 # avocado does not try to load them. These scripts are 200 # written for python 2.7 not python 3 and hence if avocado 201 # loaded them, it would complain about python 3 specific 202 # syntaxes. 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._bitsInternalVer) 250 iso_file = os.path.join(self._workDir, 251 'bits-%d.iso' %self._bitsInternalVer) 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 subprocess.check_call([mkrescue_script, '-o', iso_file, 266 bits_dir], stderr=subprocess.STDOUT) 267 else: 268 subprocess.check_call([mkrescue_script, '-o', 269 iso_file, bits_dir], 270 stderr=subprocess.DEVNULL, 271 stdout=subprocess.DEVNULL) 272 except Exception as e: # pylint: disable=broad-except 273 self.skipTest("Error while generating the bits iso. " 274 "Pass V=1 in the environment to get more details. " 275 + str(e)) 276 277 self.assertTrue(os.access(iso_file, os.R_OK)) 278 279 self.logger.info('iso file %s successfully generated.', iso_file) 280 281 def setUp(self): # pylint: disable=arguments-differ 282 super().setUp('qemu-system-') 283 284 self._baseDir = os.getenv('AVOCADO_TEST_BASEDIR') 285 286 # workdir could also be avocado's own workdir in self.workdir. 287 # At present, I prefer to maintain my own temporary working 288 # directory. It gives us more control over the generated bits 289 # log files and also for debugging, we may chose not to remove 290 # this working directory so that the logs and iso can be 291 # inspected manually and archived if needed. 292 self._workDir = tempfile.mkdtemp(prefix='acpi-bits-', 293 suffix='.tmp') 294 self.logger.info('working dir: %s', self._workDir) 295 296 prebuiltDir = os.path.join(self._workDir, 'prebuilt') 297 if not os.path.isdir(prebuiltDir): 298 os.mkdir(prebuiltDir, mode=0o775) 299 300 bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip' 301 %(self._bitsInternalVer, 302 self._bitsCommitHash)) 303 grub_tar_file = os.path.join(prebuiltDir, 304 'bits-%d-%s-grub.tar.gz' 305 %(self._bitsInternalVer, 306 self._bitsCommitHash)) 307 308 bitsLocalArtLoc = self.fetch_asset(self._bitsArtURL, 309 asset_hash=self._bitsArtSHA1Hash) 310 self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc) 311 312 # extract the bits artifact in the temp working directory 313 with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref: 314 zref.extractall(prebuiltDir) 315 316 # extract the bits software in the temp working directory 317 with zipfile.ZipFile(bits_zip_file, 'r') as zref: 318 zref.extractall(self._workDir) 319 320 with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball: 321 tarball.extractall(self._workDir) 322 323 self.copy_test_scripts() 324 self.copy_bits_config() 325 self.generate_bits_iso() 326 327 def parse_log(self): 328 """parse the log generated by running bits tests and 329 check for failures. 330 """ 331 debugconf = os.path.join(self._workDir, self._debugcon_log) 332 log = "" 333 with open(debugconf, 'r', encoding='utf-8') as filehandle: 334 log = filehandle.read() 335 336 matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*', 337 log) 338 for match in matchiter: 339 # verify that no test cases failed. 340 try: 341 self.assertEqual(match.group(3).split()[0], '0', 342 'Some bits tests seems to have failed. ' \ 343 'Please check the test logs for more info.') 344 except AssertionError as e: 345 self._print_log(log) 346 raise e 347 else: 348 if os.getenv('V') or os.getenv('BITS_DEBUG'): 349 self._print_log(log) 350 351 def tearDown(self): 352 """ 353 Lets do some cleanups. 354 """ 355 if self._vm: 356 self.assertFalse(not self._vm.is_running) 357 if not os.getenv('BITS_DEBUG') and self._workDir: 358 self.logger.info('removing the work directory %s', self._workDir) 359 shutil.rmtree(self._workDir) 360 else: 361 self.logger.info('not removing the work directory %s ' \ 362 'as BITS_DEBUG is ' \ 363 'passed in the environment', self._workDir) 364 super().tearDown() 365 366 def test_acpi_smbios_bits(self): 367 """The main test case implementation.""" 368 369 iso_file = os.path.join(self._workDir, 370 'bits-%d.iso' %self._bitsInternalVer) 371 372 self.assertTrue(os.access(iso_file, os.R_OK)) 373 374 self._vm = QEMUBitsMachine(binary=self.qemu_bin, 375 base_temp_dir=self._workDir, 376 debugcon_log=self._debugcon_log, 377 debugcon_addr=self._debugcon_addr) 378 379 self._vm.add_args('-cdrom', '%s' %iso_file) 380 # the vm needs to be run under icount so that TCG emulation is 381 # consistent in terms of timing. smilatency tests have consistent 382 # timing requirements. 383 self._vm.add_args('-icount', 'auto') 384 # currently there is no support in bits for recognizing 64-bit SMBIOS 385 # entry points. QEMU defaults to 64-bit entry points since the 386 # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0 387 # for newer machine models"). Therefore, enforce 32-bit entry point. 388 self._vm.add_args('-machine', 'smbios-entry-point-type=32') 389 390 # enable console logging 391 self._vm.set_console() 392 self._vm.launch() 393 394 self.logger.debug("Console output from bits VM follows ...") 395 c_drainer = drainer.LineLogger(self._vm.console_socket.fileno(), 396 logger=self.logger.getChild("console"), 397 stop_check=(lambda : 398 not self._vm.is_running())) 399 c_drainer.start() 400 401 # biosbits has been configured to run all the specified test suites 402 # in batch mode and then automatically initiate a vm shutdown. 403 # Rely on avocado's unit test timeout. 404 self._vm.event_wait('SHUTDOWN') 405 self._vm.wait(timeout=None) 406 self.parse_log() 407