diff --git a/tests/lib/Qemu.py b/tests/lib/Qemu.py index 62656b7..ea71851 100644 --- a/tests/lib/Qemu.py +++ b/tests/lib/Qemu.py @@ -27,6 +27,7 @@ import tempfile import time import sys +import stat import util @@ -73,6 +74,17 @@ def args(self): return ['-nographic'] return [] +class QemuSerial(): + def __init__(self, serial_file : str = None): + self.serial_file = serial_file + def args(self): + if self.serial_file: + return [ + '-chardev', f'file,id=c1,path={self.serial_file},signal=off', + '-device', 'isa-serial,chardev=c1' + ] + return ['-serial', 'stdio'] + class QemuUserConfig: def __init__(self): self.nodefaults = True @@ -229,6 +241,7 @@ def __init__( 'config': QemuUserConfig(), 'memory': QemuMemory(memory), 'ovmf' : QemuOvmf(machine), + 'serial' : QemuSerial(f'{self.workdir}/serial.log'), 'machine' : QemuMachineType(machine)} self.command = ['-pidfile', f'{self.workdir}/qemu.pid'] @@ -238,13 +251,6 @@ def get_command(self): _args.extend(p.args()) return _args + self.command - def add_serial_to_file(self): - # serial to file - self.command = self.command + [ - '-chardev', f'file,id=c1,path={self.workdir}/serial.log,signal=off', - '-device', 'isa-serial,chardev=c1' - ] - def add_qemu_run_log(self): # serial to file self.command = self.command + [ @@ -346,6 +352,9 @@ def wait_for_state(self, s, retries=5): def wakeup(self): self.send_command("system_wakeup") + def powerdown(self): + self.send_command("system_powerdown") + def __del__(self): if self.socket is not None: self.socket.close() @@ -467,6 +476,15 @@ class QemuMachineService: QEMU_MACHINE_MONITOR = enum.auto() QEMU_MACHINE_QMP = enum.auto() +# run script to run the guest +# this is used for debugging purpose and allow users +# to run the guest manually +qemu_run_script = """ +#!/bin/bash +echo "To connect to the VM : ssh -p {fwd_port} root@localhost" +{cmd_str} +""" + class QemuMachine: debug_enabled = False # hold all qemu instances @@ -498,7 +516,6 @@ def __init__(self, self.fwd_port = util.tcp_port_available() self.qcmd.add_port_forward(self.fwd_port) self.qcmd.add_qemu_run_log() - self.qcmd.add_serial_to_file() self.proc = None self.out = None @@ -572,6 +589,8 @@ def run(self): """ cmd = self.qcmd.get_command() print(' '.join(cmd)) + script=f'{self.workdir_name}/run.sh' + self.write_cmd_to_file(script) self.proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -580,11 +599,7 @@ def run_and_wait(self): """ Run qemu and wait for its start (by waiting for monitor file's availability) """ - cmd = self.qcmd.get_command() - print(' '.join(cmd)) - self.proc = subprocess.Popen(cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + self.run() QemuMonitor(self) def communicate(self): @@ -602,13 +617,28 @@ def stop(self): """ if self.proc is None: return - # self.proc.returncode== None -> not yet terminated - if self.proc.returncode is None: - try: - self.proc.terminate() - self.communicate() - except Exception as e: - print(f'Exception {e}') + if self.proc.returncode is not None: + return + + # self.proc.returncode == None -> not yet terminated + + # try to shutdown the VM properly, this is important to avoid + # rootfs corruption if we want to run the guest again + mon = QemuMonitor(self) + mon.powerdown() + try: + self.communicate() + return + except Exception as e: + pass + + print('Qemu process did not shutdown properly, terminate it ...') + # terminate qemu process (SIGTERM) + try: + self.proc.terminate() + self.communicate() + except Exception as e: + print(f'Exception {e}') def reboot(self): """ @@ -623,6 +653,33 @@ def reboot(self): # run the VM again self.run() + def write_cmd_to_file(self, fname : str): + """ + Write the qemu command to a executable bash script + """ + # force -serial to stdio to be able to have the console on stdio + cur_serial = self.qcmd.plugins['serial'] + self.qcmd.plugins['serial'] = QemuSerial() + + cmd = self.qcmd.get_command() + with open(fname, 'w+') as run_script: + cmd_str='' + for el in cmd: + # escape qemu object with quotes + # for example : -object "{'qom-type': 'tdx-guest', 'id': 'tdx'}" + if el.startswith('{') and el.endswith('}'): + cmd_str += f'\"{el}\" ' + else: + cmd_str += f'{el} ' + script_contents = qemu_run_script.format(fwd_port=self.fwd_port, + cmd_str=cmd_str) + run_script.write(script_contents) + f = pathlib.Path(fname) + f.chmod(f.stat().st_mode | stat.S_IEXEC) + + # restore serial config + self.qcmd.plugins['serial'] = cur_serial + def __del__(self): """ Make sure we stop the qemu process if it is still running