From 1e46dde82ba49f40cf4e9971550ea71ed5c7a47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gytis=20Sto=C5=A1kevi=C4=8Dius?= Date: Fri, 11 Apr 2025 10:04:20 +0000 Subject: [PATCH] improve qga.py --- assets/win11x64-enterprise-eval.xml | 3 + assets/win11x64-enterprise.xml | 3 + assets/win11x64-iot.xml | 3 + assets/win11x64-ltsc.xml | 3 + assets/win11x64.xml | 3 + src/boot.sh | 161 ++++++++++++++++++++++++++++ src/define.sh | 2 +- src/qga.py | 86 ++++++++++----- 8 files changed, 235 insertions(+), 29 deletions(-) create mode 100755 src/boot.sh diff --git a/assets/win11x64-enterprise-eval.xml b/assets/win11x64-enterprise-eval.xml index 7822f82..c41d244 100644 --- a/assets/win11x64-enterprise-eval.xml +++ b/assets/win11x64-enterprise-eval.xml @@ -9,6 +9,7 @@ en-US en-US en-US + UTC @@ -137,6 +138,7 @@ https://github.com/dockur/windows/issues Windows for Docker + UTC 1 @@ -301,6 +303,7 @@ 1 + UTC diff --git a/assets/win11x64-enterprise.xml b/assets/win11x64-enterprise.xml index 6620eb7..c5153a4 100644 --- a/assets/win11x64-enterprise.xml +++ b/assets/win11x64-enterprise.xml @@ -9,6 +9,7 @@ en-US en-US en-US + UTC @@ -140,6 +141,7 @@ https://github.com/dockur/windows/issues Windows for Docker + UTC 1 @@ -304,6 +306,7 @@ 1 + UTC diff --git a/assets/win11x64-iot.xml b/assets/win11x64-iot.xml index 4707774..03eabb1 100644 --- a/assets/win11x64-iot.xml +++ b/assets/win11x64-iot.xml @@ -9,6 +9,7 @@ en-US en-US en-US + UTC @@ -140,6 +141,7 @@ https://github.com/dockur/windows/issues Windows for Docker + UTC 1 @@ -304,6 +306,7 @@ 1 + UTC diff --git a/assets/win11x64-ltsc.xml b/assets/win11x64-ltsc.xml index 4359b1c..eb63d15 100644 --- a/assets/win11x64-ltsc.xml +++ b/assets/win11x64-ltsc.xml @@ -9,6 +9,7 @@ en-US en-US en-US + UTC @@ -140,6 +141,7 @@ https://github.com/dockur/windows/issues Windows for Docker + UTC 1 @@ -304,6 +306,7 @@ 1 + UTC diff --git a/assets/win11x64.xml b/assets/win11x64.xml index bec96d4..b8f58aa 100644 --- a/assets/win11x64.xml +++ b/assets/win11x64.xml @@ -9,6 +9,7 @@ en-US en-US en-US + UTC @@ -140,6 +141,7 @@ https://github.com/dockur/windows/issues Windows for Docker + UTC 1 @@ -304,6 +306,7 @@ 1 + UTC diff --git a/src/boot.sh b/src/boot.sh new file mode 100755 index 0000000..63e9dec --- /dev/null +++ b/src/boot.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Docker environment variables +: "${BIOS:=""}" # BIOS file +: "${TPM:="N"}" # Disable TPM +: "${SMM:="N"}" # Disable SMM + +BOOT_DESC="" +BOOT_OPTS="" + +SECURE="off" +[[ "$SMM" == [Yy1]* ]] && SECURE="on" +[ -n "$BIOS" ] && BOOT_MODE="custom" + +case "${BOOT_MODE,,}" in + "uefi" | "" ) + BOOT_MODE="uefi" + ROM="OVMF_CODE_4M.fd" + VARS="OVMF_VARS_4M.fd" + ;; + "secure" ) + SECURE="on" + BOOT_DESC=" securely" + ROM="OVMF_CODE_4M.secboot.fd" + VARS="OVMF_VARS_4M.secboot.fd" + ;; + "windows" | "windows_plain" ) + ROM="OVMF_CODE_4M.fd" + VARS="OVMF_VARS_4M.fd" + ;; + "windows_secure" ) + TPM="Y" + SECURE="on" + BOOT_DESC=" securely" + ROM="OVMF_CODE_4M.ms.fd" + VARS="OVMF_VARS_4M.ms.fd" + ;; + "windows_legacy" ) + HV="N" + SECURE="on" + BOOT_DESC=" (legacy)" + [ -z "${USB:-}" ] && USB="usb-ehci,id=ehci" + ;; + "legacy" ) + BOOT_DESC=" with SeaBIOS" + ;; + "custom" ) + BOOT_OPTS="-bios $BIOS" + BOOT_DESC=" with custom BIOS file" + ;; + *) + error "Unknown BOOT_MODE, value \"${BOOT_MODE}\" is not recognized!" + exit 33 + ;; +esac + +if [[ "${BOOT_MODE,,}" == "windows"* ]]; then + BOOT_OPTS+=" -rtc base=utc" + BOOT_OPTS+=" -global ICH9-LPC.disable_s3=1" + BOOT_OPTS+=" -global ICH9-LPC.disable_s4=1" +fi + +case "${BOOT_MODE,,}" in + "uefi" | "secure" | "windows" | "windows_plain" | "windows_secure" ) + + OVMF="/usr/share/OVMF" + DEST="$STORAGE/${BOOT_MODE,,}" + + if [ ! -s "$DEST.rom" ] || [ ! -f "$DEST.rom" ]; then + [ ! -s "$OVMF/$ROM" ] || [ ! -f "$OVMF/$ROM" ] && error "UEFI boot file ($OVMF/$ROM) not found!" && exit 44 + cp "$OVMF/$ROM" "$DEST.rom" + fi + + if [ ! -s "$DEST.vars" ] || [ ! -f "$DEST.vars" ]; then + [ ! -s "$OVMF/$VARS" ] || [ ! -f "$OVMF/$VARS" ]&& error "UEFI vars file ($OVMF/$VARS) not found!" && exit 45 + cp "$OVMF/$VARS" "$DEST.vars" + fi + + if [[ "${BOOT_MODE,,}" == "secure" ]] || [[ "${BOOT_MODE,,}" == "windows_secure" ]]; then + BOOT_OPTS+=" -global driver=cfi.pflash01,property=secure,value=on" + fi + + BOOT_OPTS+=" -drive file=$DEST.rom,if=pflash,unit=0,format=raw,readonly=on" + BOOT_OPTS+=" -drive file=$DEST.vars,if=pflash,unit=1,format=raw" + + ;; +esac + +MSRS="/sys/module/kvm/parameters/ignore_msrs" +if [ -e "$MSRS" ]; then + result=$(<"$MSRS") + result="${result//[![:print:]]/}" + if [[ "$result" == "0" ]] || [[ "${result^^}" == "N" ]]; then + echo 1 | tee "$MSRS" > /dev/null 2>&1 || true + fi +fi + +CLOCKSOURCE="tsc" +[[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter" +CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource" + +if [ ! -f "$CLOCK" ]; then + warn "file \"$CLOCK\" cannot not found?" +else + result=$(<"$CLOCK") + result="${result//[![:print:]]/}" + case "${result,,}" in + "${CLOCKSOURCE,,}" ) ;; + "kvm-clock" ) info "Nested KVM virtualization detected.." ;; + "hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;; + "hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;; + *) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;; + esac +fi + +SM_BIOS="" +PS="/sys/class/dmi/id/product_serial" + +if [ -s "$PS" ] && [ -r "$PS" ]; then + + BIOS_SERIAL=$(<"$PS") + BIOS_SERIAL="${BIOS_SERIAL//[![:alnum:]]/}" + + if [ -n "$BIOS_SERIAL" ]; then + SM_BIOS="-smbios type=1,serial=$BIOS_SERIAL" + fi + +fi + +if [[ "$TPM" == [Yy1]* ]]; then + + rm -f /var/run/tpm.pid + + if ! swtpm socket -t -d --tpmstate "backend-uri=file://$STORAGE/${BOOT_MODE,,}.tpm" --ctrl type=unixio,path=/run/swtpm-sock --pid file=/var/run/tpm.pid --tpm2; then + error "Failed to start TPM emulator, reason: $?" + else + + for (( i = 1; i < 20; i++ )); do + + [ -S "/run/swtpm-sock" ] && break + + if (( i % 10 == 0 )); then + echo "Waiting for TPM emulator to become available..." + fi + + sleep 0.1 + + done + + if [ ! -S "/run/swtpm-sock" ]; then + error "TPM socket not found? Disabling TPM module..." + else + BOOT_OPTS+=" -chardev socket,id=chrtpm,path=/run/swtpm-sock" + BOOT_OPTS+=" -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0" + fi + + fi +fi + +return 0 diff --git a/src/define.sh b/src/define.sh index 69896b1..d10fe5e 100644 --- a/src/define.sh +++ b/src/define.sh @@ -1412,7 +1412,7 @@ prepareInstall() { echo " OEMSkipRegional=1" echo " OemSkipWelcome=1" echo " AdminPassword=$password" - echo " TimeZone=0" + echo " TimeZone=85" echo " AutoLogon=Yes" echo " AutoLogonCount=65432" echo "" diff --git a/src/qga.py b/src/qga.py index c57f017..64b818e 100644 --- a/src/qga.py +++ b/src/qga.py @@ -29,58 +29,61 @@ def decode_output(data): return "" try: - # Try Hex decoding first return bytes.fromhex(data).decode("utf-8", errors="ignore") except ValueError: pass try: - # If hex fails, try Base64 decoding return base64.b64decode(data).decode("utf-8", errors="ignore") except ValueError: pass - # If all decoding fails, return raw return data -def execute_command(sock, command_path, command_args): +def execute_command(sock, command_path, command_args, timeout): """Execute a command inside the guest VM with specified path and arguments.""" exec_request = { "execute": "guest-exec", "arguments": { "path": command_path, "arg": command_args, - "capture-output": True, # Capture stdout and stderr + "capture-output": True, }, } + + print(f"Executing: {command_path} {' '.join(command_args)}") response = send_qga_command(sock, exec_request) - if response is None: - return None - - if "return" not in response or "pid" not in response["return"]: - print("Error: Failed to start execution:", response, file=sys.stderr) + if response is None or "return" not in response or "pid" not in response["return"]: + print( + "Error: Failed to start execution.", + json.dumps(response or {}, indent=2), + file=sys.stderr, + ) return None pid = response["return"]["pid"] print(f"Command started with PID {pid}") - # Step 2: Wait for completion + # Step 2: Wait for completion with timeout + start_time = time.time() + status = {} while True: + if time.time() - start_time > timeout: + print("Execution timeout reached.", file=sys.stderr) + return {"exit_code": -2, "stdout": "", "stderr": "Execution timed out."} + status_request = {"execute": "guest-exec-status", "arguments": {"pid": pid}} status_response = send_qga_command(sock, status_request) - if status_response is None: - continue - - if "return" in status_response: + if status_response and "return" in status_response: status = status_response["return"] if status.get("exited", False): - break # Command finished - time.sleep(0.2) # Wait before checking again + break + + time.sleep(0.2) - # Step 3: Get exit code and output exit_code = status.get("exitcode", -1) stdout_data = decode_output(status.get("out-data", "")) stderr_data = decode_output(status.get("err-data", "")) @@ -91,7 +94,7 @@ def execute_command(sock, command_path, command_args): def create_socket(): """Create and return a reusable socket connection to the QEMU Guest Agent.""" sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.settimeout(30) # 30 seconds timeout + sock.settimeout(30) try: sock.connect(QGA_SOCKET) return sock @@ -103,6 +106,21 @@ def create_socket(): def parse_args(): """Parse command-line arguments.""" parser = argparse.ArgumentParser(description="Send commands to QEMU Guest Agent.") + shell_group = parser.add_mutually_exclusive_group() + shell_group.add_argument( + "--cmd", action="store_true", help="Run the command through cmd.exe /c" + ) + shell_group.add_argument( + "--powershell", + action="store_true", + help="Run the command with powershell -Command", + ) + parser.add_argument( + "--timeout", type=int, default=60, help="Max execution time in seconds" + ) + parser.add_argument( + "--json", action="store_true", help="Output result in JSON format" + ) parser.add_argument( "command", help="Path to the command to execute inside the guest VM" ) @@ -113,23 +131,35 @@ def parse_args(): if __name__ == "__main__": - # Parse command-line arguments args = parse_args() - # Create a reusable socket + if args.cmd: + command_path = "cmd.exe" + command_args = ["/c", args.command] + args.args + elif args.powershell: + command_path = "powershell.exe" + full_command = " ".join([args.command] + args.args) + command_args = ["-Command", full_command] + else: + command_path = args.command + command_args = args.args + + # Create a reusable socket unix_sock = create_socket() if not unix_sock: print("Failed to create socket.", file=sys.stderr) sys.exit(1) # Exit if we can't connect to the socket - # Execute the command - result = execute_command(unix_sock, args.command, args.args) + result = execute_command(unix_sock, command_path, command_args, args.timeout) if result: - print(f"Exit Code: {result['exit_code']}") - if result["stdout"]: - print("STDOUT:\n", result["stdout"]) - if result["stderr"]: - print("STDERR:\n", result["stderr"]) + if args.json: + print(json.dumps(result, indent=2)) + else: + print(f"Exit Code: {result['exit_code']}") + if result["stdout"]: + print("STDOUT:\n", result["stdout"]) + if result["stderr"]: + print("STDERR:\n", result["stderr"]) # Close the socket once all commands are executed unix_sock.close()