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"] # 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() or os.getenv('GITLAB_CI'), 127 'incorrect platform or dependencies (%s) not installed ' \ 128 'or running on GitLab' % ','.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 def __init__(self, *args, **kwargs): 138 super().__init__(*args, **kwargs) 139 self._vm = None 140 self._workDir = None 141 self._baseDir = None 142 143 # following are some standard configuration constants 144 self._bitsInternalVer = 2020 145 self._bitsCommitHash = 'b48b88ff' # commit hash must match 146 # the artifact tag below 147 self._bitsTag = "qemu-bits-10182022" # this is the latest bits 148 # release as of today. 149 self._bitsArtSHA1Hash = 'b04790ac9b99b5662d0416392c73b97580641fe5' 150 self._bitsArtURL = ("https://gitlab.com/qemu-project/" 151 "biosbits-bits/-/jobs/artifacts/%s/" 152 "download?job=qemu-bits-build" %self._bitsTag) 153 self._debugcon_addr = '0x403' 154 self._debugcon_log = 'debugcon-log.txt' 155 logging.basicConfig(level=logging.INFO) 156 self.logger = logging.getLogger('acpi-bits') 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._bitsInternalVer, 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._bitsInternalVer, 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 # avocado does not try to load them. These scripts are 199 # written for python 2.7 not python 3 and hence if avocado 200 # loaded them, it would complain about python 3 specific 201 # syntaxes. 202 newfilename = os.path.splitext(filename)[0] + '.py' 203 shutil.copy2(os.path.join(bits_test_dir, filename), 204 os.path.join(target_test_dir, newfilename)) 205 self.logger.info('copied test file %s to %s', 206 filename, target_test_dir) 207 208 # now remove the pyc test file if it exists, otherwise the 209 # changes in the python test script won't be executed. 210 testfile_pyc = os.path.splitext(filename)[0] + '.pyc' 211 if os.access(os.path.join(target_test_dir, testfile_pyc), 212 os.F_OK): 213 os.remove(os.path.join(target_test_dir, testfile_pyc)) 214 self.logger.info('removed compiled file %s', 215 os.path.join(target_test_dir, 216 testfile_pyc)) 217 218 def fix_mkrescue(self, mkrescue): 219 """ grub-mkrescue is a bash script with two variables, 'prefix' and 220 'libdir'. They must be pointed to the right location so that the 221 iso can be generated appropriately. We point the two variables to 222 the directory where we have extracted our pre-built bits grub 223 tarball. 224 """ 225 grub_x86_64_mods = os.path.join(self._workDir, 'grub-inst-x86_64-efi') 226 grub_i386_mods = os.path.join(self._workDir, 'grub-inst') 227 228 self.assertTrue(os.path.exists(grub_x86_64_mods)) 229 self.assertTrue(os.path.exists(grub_i386_mods)) 230 231 new_script = "" 232 with open(mkrescue, 'r', encoding='utf-8') as filehandle: 233 orig_script = filehandle.read() 234 new_script = re.sub('(^prefix=)(.*)', 235 r'\1"%s"' %grub_x86_64_mods, 236 orig_script, flags=re.M) 237 new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods, 238 new_script, flags=re.M) 239 240 with open(mkrescue, 'w', encoding='utf-8') as filehandle: 241 filehandle.write(new_script) 242 243 def generate_bits_iso(self): 244 """ Uses grub-mkrescue to generate a fresh bits iso with the python 245 test scripts 246 """ 247 bits_dir = os.path.join(self._workDir, 248 'bits-%d' %self._bitsInternalVer) 249 iso_file = os.path.join(self._workDir, 250 'bits-%d.iso' %self._bitsInternalVer) 251 mkrescue_script = os.path.join(self._workDir, 252 'grub-inst-x86_64-efi', 'bin', 253 'grub-mkrescue') 254 255 self.assertTrue(os.access(mkrescue_script, 256 os.R_OK | os.W_OK | os.X_OK)) 257 258 self.fix_mkrescue(mkrescue_script) 259 260 self.logger.info('using grub-mkrescue for generating biosbits iso ...') 261 262 try: 263 if os.getenv('V'): 264 subprocess.check_call([mkrescue_script, '-o', iso_file, 265 bits_dir], stderr=subprocess.STDOUT) 266 else: 267 subprocess.check_call([mkrescue_script, '-o', 268 iso_file, bits_dir], 269 stderr=subprocess.DEVNULL, 270 stdout=subprocess.DEVNULL) 271 except Exception as e: # pylint: disable=broad-except 272 self.skipTest("Error while generating the bits iso. " 273 "Pass V=1 in the environment to get more details. " 274 + str(e)) 275 276 self.assertTrue(os.access(iso_file, os.R_OK)) 277 278 self.logger.info('iso file %s successfully generated.', iso_file) 279 280 def setUp(self): # pylint: disable=arguments-differ 281 super().setUp('qemu-system-') 282 283 self._baseDir = os.getenv('AVOCADO_TEST_BASEDIR') 284 285 # workdir could also be avocado's own workdir in self.workdir. 286 # At present, I prefer to maintain my own temporary working 287 # directory. It gives us more control over the generated bits 288 # log files and also for debugging, we may chose not to remove 289 # this working directory so that the logs and iso can be 290 # inspected manually and archived if needed. 291 self._workDir = tempfile.mkdtemp(prefix='acpi-bits-', 292 suffix='.tmp') 293 self.logger.info('working dir: %s', self._workDir) 294 295 prebuiltDir = os.path.join(self._workDir, 'prebuilt') 296 if not os.path.isdir(prebuiltDir): 297 os.mkdir(prebuiltDir, mode=0o775) 298 299 bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip' 300 %(self._bitsInternalVer, 301 self._bitsCommitHash)) 302 grub_tar_file = os.path.join(prebuiltDir, 303 'bits-%d-%s-grub.tar.gz' 304 %(self._bitsInternalVer, 305 self._bitsCommitHash)) 306 307 bitsLocalArtLoc = self.fetch_asset(self._bitsArtURL, 308 asset_hash=self._bitsArtSHA1Hash) 309 self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc) 310 311 # extract the bits artifact in the temp working directory 312 with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref: 313 zref.extractall(prebuiltDir) 314 315 # extract the bits software in the temp working directory 316 with zipfile.ZipFile(bits_zip_file, 'r') as zref: 317 zref.extractall(self._workDir) 318 319 with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball: 320 tarball.extractall(self._workDir) 321 322 self.copy_test_scripts() 323 self.copy_bits_config() 324 self.generate_bits_iso() 325 326 def parse_log(self): 327 """parse the log generated by running bits tests and 328 check for failures. 329 """ 330 debugconf = os.path.join(self._workDir, self._debugcon_log) 331 log = "" 332 with open(debugconf, 'r', encoding='utf-8') as filehandle: 333 log = filehandle.read() 334 335 matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*', 336 log) 337 for match in matchiter: 338 # verify that no test cases failed. 339 try: 340 self.assertEqual(match.group(3).split()[0], '0', 341 'Some bits tests seems to have failed. ' \ 342 'Please check the test logs for more info.') 343 except AssertionError as e: 344 self._print_log(log) 345 raise e 346 else: 347 if os.getenv('V'): 348 self._print_log(log) 349 350 def tearDown(self): 351 """ 352 Lets do some cleanups. 353 """ 354 if self._vm: 355 self.assertFalse(not self._vm.is_running) 356 self.logger.info('removing the work directory %s', self._workDir) 357 shutil.rmtree(self._workDir) 358 super().tearDown() 359 360 def test_acpi_smbios_bits(self): 361 """The main test case implementaion.""" 362 363 iso_file = os.path.join(self._workDir, 364 'bits-%d.iso' %self._bitsInternalVer) 365 366 self.assertTrue(os.access(iso_file, os.R_OK)) 367 368 self._vm = QEMUBitsMachine(binary=self.qemu_bin, 369 base_temp_dir=self._workDir, 370 debugcon_log=self._debugcon_log, 371 debugcon_addr=self._debugcon_addr) 372 373 self._vm.add_args('-cdrom', '%s' %iso_file) 374 # the vm needs to be run under icount so that TCG emulation is 375 # consistent in terms of timing. smilatency tests have consistent 376 # timing requirements. 377 self._vm.add_args('-icount', 'auto') 378 379 args = " ".join(str(arg) for arg in self._vm.base_args()) + \ 380 " " + " ".join(str(arg) for arg in self._vm.args) 381 382 self.logger.info("launching QEMU vm with the following arguments: %s", 383 args) 384 385 self._vm.launch() 386 # biosbits has been configured to run all the specified test suites 387 # in batch mode and then automatically initiate a vm shutdown. 388 # sleep for maximum of one minute 389 max_sleep_time = time.monotonic() + 60 390 while self._vm.is_running() and time.monotonic() < max_sleep_time: 391 time.sleep(1) 392 393 self.assertFalse(time.monotonic() > max_sleep_time, 394 'The VM seems to have failed to shutdown in time') 395 396 self.parse_log() 397