1#!/usr/bin/env python3 2# group: rw quick 3# Exercize 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 <ani@anisinha.ca> 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_qemu import QemuBaseTest 52 53deps = ["xorriso", "mformat"] # dependent tools needed in the test setup/box. 54supported_platforms = ['x86_64'] # supported test platforms. 55 56 57def which(tool): 58 """ looks up the full path for @tool, returns None if not found 59 or if @tool does not have executable permissions. 60 """ 61 paths=os.getenv('PATH') 62 for p in paths.split(os.path.pathsep): 63 p = os.path.join(p, tool) 64 if os.path.exists(p) and os.access(p, os.X_OK): 65 return p 66 return None 67 68def missing_deps(): 69 """ returns True if any of the test dependent tools are absent. 70 """ 71 for dep in deps: 72 if which(dep) is None: 73 return True 74 return False 75 76def supported_platform(): 77 """ checks if the test is running on a supported platform. 78 """ 79 return platform.machine() in supported_platforms 80 81class QEMUBitsMachine(QEMUMachine): # pylint: disable=too-few-public-methods 82 """ 83 A QEMU VM, with isa-debugcon enabled and bits iso passed 84 using -cdrom to QEMU commandline. 85 86 """ 87 def __init__(self, 88 binary: str, 89 args: Sequence[str] = (), 90 wrapper: Sequence[str] = (), 91 name: Optional[str] = None, 92 base_temp_dir: str = "/var/tmp", 93 debugcon_log: str = "debugcon-log.txt", 94 debugcon_addr: str = "0x403", 95 sock_dir: Optional[str] = None, 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 if sock_dir is None: 102 sock_dir = base_temp_dir 103 super().__init__(binary, args, wrapper=wrapper, name=name, 104 base_temp_dir=base_temp_dir, 105 sock_dir=sock_dir, 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(QemuBaseTest): #pylint: disable=too-many-instance-attributes 130 """ 131 ACPI and SMBIOS tests using biosbits. 132 133 :avocado: tags=arch:x86_64 134 :avocado: tags=acpi 135 136 """ 137 # in slower systems the test can take as long as 3 minutes to complete. 138 timeout = 200 139 140 def __init__(self, *args, **kwargs): 141 super().__init__(*args, **kwargs) 142 self._vm = None 143 self._workDir = None 144 self._baseDir = None 145 146 # following are some standard configuration constants 147 self._bitsInternalVer = 2020 148 self._bitsCommitHash = 'b48b88ff' # commit hash must match 149 # the artifact tag below 150 self._bitsTag = "qemu-bits-10182022" # this is the latest bits 151 # release as of today. 152 self._bitsArtSHA1Hash = 'b04790ac9b99b5662d0416392c73b97580641fe5' 153 self._bitsArtURL = ("https://gitlab.com/qemu-project/" 154 "biosbits-bits/-/jobs/artifacts/%s/" 155 "download?job=qemu-bits-build" %self._bitsTag) 156 self._debugcon_addr = '0x403' 157 self._debugcon_log = 'debugcon-log.txt' 158 logging.basicConfig(level=logging.INFO) 159 self.logger = logging.getLogger('acpi-bits') 160 161 def _print_log(self, log): 162 self.logger.info('\nlogs from biosbits follows:') 163 self.logger.info('==========================================\n') 164 self.logger.info(log) 165 self.logger.info('==========================================\n') 166 167 def copy_bits_config(self): 168 """ copies the bios bits config file into bits. 169 """ 170 config_file = 'bits-cfg.txt' 171 bits_config_dir = os.path.join(self._baseDir, 'acpi-bits', 172 'bits-config') 173 target_config_dir = os.path.join(self._workDir, 174 'bits-%d' %self._bitsInternalVer, 175 'boot') 176 self.assertTrue(os.path.exists(bits_config_dir)) 177 self.assertTrue(os.path.exists(target_config_dir)) 178 self.assertTrue(os.access(os.path.join(bits_config_dir, 179 config_file), os.R_OK)) 180 shutil.copy2(os.path.join(bits_config_dir, config_file), 181 target_config_dir) 182 self.logger.info('copied config file %s to %s', 183 config_file, target_config_dir) 184 185 def copy_test_scripts(self): 186 """copies the python test scripts into bits. """ 187 188 bits_test_dir = os.path.join(self._baseDir, 'acpi-bits', 189 'bits-tests') 190 target_test_dir = os.path.join(self._workDir, 191 'bits-%d' %self._bitsInternalVer, 192 'boot', 'python') 193 194 self.assertTrue(os.path.exists(bits_test_dir)) 195 self.assertTrue(os.path.exists(target_test_dir)) 196 197 for filename in os.listdir(bits_test_dir): 198 if os.path.isfile(os.path.join(bits_test_dir, filename)) and \ 199 filename.endswith('.py2'): 200 # all test scripts are named with extension .py2 so that 201 # avocado does not try to load them. These scripts are 202 # written for python 2.7 not python 3 and hence if avocado 203 # loaded them, it would complain about python 3 specific 204 # syntaxes. 205 newfilename = os.path.splitext(filename)[0] + '.py' 206 shutil.copy2(os.path.join(bits_test_dir, filename), 207 os.path.join(target_test_dir, newfilename)) 208 self.logger.info('copied test file %s to %s', 209 filename, target_test_dir) 210 211 # now remove the pyc test file if it exists, otherwise the 212 # changes in the python test script won't be executed. 213 testfile_pyc = os.path.splitext(filename)[0] + '.pyc' 214 if os.access(os.path.join(target_test_dir, testfile_pyc), 215 os.F_OK): 216 os.remove(os.path.join(target_test_dir, testfile_pyc)) 217 self.logger.info('removed compiled file %s', 218 os.path.join(target_test_dir, 219 testfile_pyc)) 220 221 def fix_mkrescue(self, mkrescue): 222 """ grub-mkrescue is a bash script with two variables, 'prefix' and 223 'libdir'. They must be pointed to the right location so that the 224 iso can be generated appropriately. We point the two variables to 225 the directory where we have extracted our pre-built bits grub 226 tarball. 227 """ 228 grub_x86_64_mods = os.path.join(self._workDir, 'grub-inst-x86_64-efi') 229 grub_i386_mods = os.path.join(self._workDir, 'grub-inst') 230 231 self.assertTrue(os.path.exists(grub_x86_64_mods)) 232 self.assertTrue(os.path.exists(grub_i386_mods)) 233 234 new_script = "" 235 with open(mkrescue, 'r', encoding='utf-8') as filehandle: 236 orig_script = filehandle.read() 237 new_script = re.sub('(^prefix=)(.*)', 238 r'\1"%s"' %grub_x86_64_mods, 239 orig_script, flags=re.M) 240 new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods, 241 new_script, flags=re.M) 242 243 with open(mkrescue, 'w', encoding='utf-8') as filehandle: 244 filehandle.write(new_script) 245 246 def generate_bits_iso(self): 247 """ Uses grub-mkrescue to generate a fresh bits iso with the python 248 test scripts 249 """ 250 bits_dir = os.path.join(self._workDir, 251 'bits-%d' %self._bitsInternalVer) 252 iso_file = os.path.join(self._workDir, 253 'bits-%d.iso' %self._bitsInternalVer) 254 mkrescue_script = os.path.join(self._workDir, 255 'grub-inst-x86_64-efi', 'bin', 256 'grub-mkrescue') 257 258 self.assertTrue(os.access(mkrescue_script, 259 os.R_OK | os.W_OK | os.X_OK)) 260 261 self.fix_mkrescue(mkrescue_script) 262 263 self.logger.info('using grub-mkrescue for generating biosbits iso ...') 264 265 try: 266 if os.getenv('V') or os.getenv('BITS_DEBUG'): 267 subprocess.check_call([mkrescue_script, '-o', iso_file, 268 bits_dir], stderr=subprocess.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('qemu-system-') 285 286 self._baseDir = os.getenv('AVOCADO_TEST_BASEDIR') 287 288 # workdir could also be avocado's own workdir in self.workdir. 289 # At present, I prefer to maintain my own temporary working 290 # directory. It gives us more control over the generated bits 291 # log files and also for debugging, we may chose not to remove 292 # this working directory so that the logs and iso can be 293 # inspected manually and archived if needed. 294 self._workDir = tempfile.mkdtemp(prefix='acpi-bits-', 295 suffix='.tmp') 296 self.logger.info('working dir: %s', self._workDir) 297 298 prebuiltDir = os.path.join(self._workDir, 'prebuilt') 299 if not os.path.isdir(prebuiltDir): 300 os.mkdir(prebuiltDir, mode=0o775) 301 302 bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip' 303 %(self._bitsInternalVer, 304 self._bitsCommitHash)) 305 grub_tar_file = os.path.join(prebuiltDir, 306 'bits-%d-%s-grub.tar.gz' 307 %(self._bitsInternalVer, 308 self._bitsCommitHash)) 309 310 bitsLocalArtLoc = self.fetch_asset(self._bitsArtURL, 311 asset_hash=self._bitsArtSHA1Hash) 312 self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc) 313 314 # extract the bits artifact in the temp working directory 315 with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref: 316 zref.extractall(prebuiltDir) 317 318 # extract the bits software in the temp working directory 319 with zipfile.ZipFile(bits_zip_file, 'r') as zref: 320 zref.extractall(self._workDir) 321 322 with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball: 323 tarball.extractall(self._workDir) 324 325 self.copy_test_scripts() 326 self.copy_bits_config() 327 self.generate_bits_iso() 328 329 def parse_log(self): 330 """parse the log generated by running bits tests and 331 check for failures. 332 """ 333 debugconf = os.path.join(self._workDir, self._debugcon_log) 334 log = "" 335 with open(debugconf, 'r', encoding='utf-8') as filehandle: 336 log = filehandle.read() 337 338 matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*', 339 log) 340 for match in matchiter: 341 # verify that no test cases failed. 342 try: 343 self.assertEqual(match.group(3).split()[0], '0', 344 'Some bits tests seems to have failed. ' \ 345 'Please check the test logs for more info.') 346 except AssertionError as e: 347 self._print_log(log) 348 raise e 349 else: 350 if os.getenv('V') or os.getenv('BITS_DEBUG'): 351 self._print_log(log) 352 353 def tearDown(self): 354 """ 355 Lets do some cleanups. 356 """ 357 if self._vm: 358 self.assertFalse(not self._vm.is_running) 359 if not os.getenv('BITS_DEBUG') and self._workDir: 360 self.logger.info('removing the work directory %s', self._workDir) 361 shutil.rmtree(self._workDir) 362 else: 363 self.logger.info('not removing the work directory %s ' \ 364 'as BITS_DEBUG is ' \ 365 'passed in the environment', self._workDir) 366 super().tearDown() 367 368 def test_acpi_smbios_bits(self): 369 """The main test case implementaion.""" 370 371 iso_file = os.path.join(self._workDir, 372 'bits-%d.iso' %self._bitsInternalVer) 373 374 self.assertTrue(os.access(iso_file, os.R_OK)) 375 376 self._vm = QEMUBitsMachine(binary=self.qemu_bin, 377 base_temp_dir=self._workDir, 378 debugcon_log=self._debugcon_log, 379 debugcon_addr=self._debugcon_addr) 380 381 self._vm.add_args('-cdrom', '%s' %iso_file) 382 # the vm needs to be run under icount so that TCG emulation is 383 # consistent in terms of timing. smilatency tests have consistent 384 # timing requirements. 385 self._vm.add_args('-icount', 'auto') 386 387 args = " ".join(str(arg) for arg in self._vm.base_args()) + \ 388 " " + " ".join(str(arg) for arg in self._vm.args) 389 390 self.logger.info("launching QEMU vm with the following arguments: %s", 391 args) 392 393 self._vm.launch() 394 # biosbits has been configured to run all the specified test suites 395 # in batch mode and then automatically initiate a vm shutdown. 396 # Rely on avocado's unit test timeout. 397 self._vm.wait(timeout=None) 398 self.parse_log() 399