Add --state-dir option to the Python VM launchers

Let each run-*.py launcher take the VM directory (disk, seed ISO, UEFI
variables, PID file, console log) from the command line, taking
precedence over the *_STATE_DIR environment variable. Paths derived
from the state directory are now computed after argument parsing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1dybYsc36kf3gUPB7NuNB
This commit is contained in:
GuofangandClaude Opus 5.5 committed 2026-09-29 13:35:32 +08:00
1 parent a03fb23977
commit 8a7f974c11
5 files changed
+78 -31

No files matched your search

+9
View File
@@ -25,6 +25,15 @@ original qcow2 image as its backing file. The base image therefore stays
unchanged. Resource settings can be overridden with environment variables; unchanged. Resource settings can be overridden with environment variables;
run `./run-debian13.sh --help` for the full list. run `./run-debian13.sh --help` for the full list.
The Python launchers (`run-*.py`) accept `--state-dir DIR` to keep
the VM disk, seed ISO, UEFI variables, PID file and console log in `DIR`
instead of the default `.vm/<name>/`. The option takes precedence over the
corresponding `*_STATE_DIR` environment variable:
```bash
./run-debian13.py --state-dir /srv/vms/debian13-test --daemon
```
If `.vm/debian13/seed.iso` is missing, the launcher automatically builds it If `.vm/debian13/seed.iso` is missing, the launcher automatically builds it
from `seediso/debian13/user-data`, `seediso/debian13/meta-data`, and from `seediso/debian13/user-data`, `seediso/debian13/meta-data`, and
`seediso/debian13/network-config` before starting the VM. `seediso/debian13/network-config` before starting the VM.
+16 -7
View File
@@ -52,8 +52,6 @@ else:
uses_shared_base_image = True uses_shared_base_image = True
state_dir = Path(env("ALMA_VM_STATE_DIR") or script_dir / ".vm" / "almalinux10") state_dir = Path(env("ALMA_VM_STATE_DIR") or script_dir / ".vm" / "almalinux10")
disk_image = Path(env("ALMA_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_image = Path(env("ALMA_VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
disk_size = env("ALMA_VM_DISK_SIZE") or "20G" disk_size = env("ALMA_VM_DISK_SIZE") or "20G"
memory = env("ALMA_VM_MEMORY") or "8G" memory = env("ALMA_VM_MEMORY") or "8G"
cpus = env("ALMA_VM_CPUS") or "8" cpus = env("ALMA_VM_CPUS") or "8"
@@ -117,23 +115,22 @@ else:
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", "/opt/homebrew/share/qemu/edk2-i386-vars.fd",
"/usr/local/share/qemu/edk2-i386-vars.fd", "/usr/local/share/qemu/edk2-i386-vars.fd",
) )
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
daemonize = False daemonize = False
extra_args = [] extra_args = []
def usage(stream=sys.stdout): def usage(stream=sys.stdout):
stream.write(f"""\ stream.write(f"""\
Usage: {Path(sys.argv[0]).name} [--daemon] [-- QEMU_ARGUMENT ...] Usage: {Path(sys.argv[0]).name} [--daemon] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
Start an AlmaLinux 10 Generic Cloud virtual machine. The launcher obtains a Start an AlmaLinux 10 Generic Cloud virtual machine. The launcher obtains a
missing base image through ../vm-images/download-images.sh and creates a missing base image through ../vm-images/download-images.sh and creates a
writable overlay under .vm/almalinux10/, leaving the base image unchanged. writable overlay in the state directory, leaving the base image unchanged.
Options: Options:
--daemon Run in the background while keeping the graphical window --daemon Run in the background while keeping the graphical window
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
(default: .vm/almalinux10; overrides ALMA_VM_STATE_DIR)
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
@@ -157,6 +154,12 @@ while args:
arg = args.pop(0) arg = args.pop(0)
if arg == "--daemon": if arg == "--daemon":
daemonize = True daemonize = True
elif arg == "--state-dir":
if not args:
die(f"{arg} requires a directory argument.", 2)
state_dir = Path(args.pop(0))
elif arg.startswith("--state-dir="):
state_dir = Path(arg.split("=", 1)[1])
elif arg in ("-h", "--help"): elif arg in ("-h", "--help"):
usage() usage()
sys.exit(0) sys.exit(0)
@@ -168,6 +171,12 @@ while args:
usage(sys.stderr) usage(sys.stderr)
sys.exit(2) sys.exit(2)
disk_image = Path(env("ALMA_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_image = Path(env("ALMA_VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
for command_name in (qemu_binary, "qemu-img"): for command_name in (qemu_binary, "qemu-img"):
if shutil.which(command_name) is None: if shutil.which(command_name) is None:
die(f"{command_name} is not installed.") die(f"{command_name} is not installed.")
+18 -8
View File
@@ -52,8 +52,6 @@ else:
uses_shared_base_image = True uses_shared_base_image = True
state_dir = Path(env("VM_STATE_DIR") or script_dir / ".vm" / "debian13") state_dir = Path(env("VM_STATE_DIR") or script_dir / ".vm" / "debian13")
disk_image = Path(env("VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_image = Path(env("VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
disk_size = env("VM_DISK_SIZE") or "20G" disk_size = env("VM_DISK_SIZE") or "20G"
memory = env("VM_MEMORY") or "8G" memory = env("VM_MEMORY") or "8G"
cpus = env("VM_CPUS") or "8" cpus = env("VM_CPUS") or "8"
@@ -117,22 +115,22 @@ else:
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", "/opt/homebrew/share/qemu/edk2-i386-vars.fd",
"/usr/local/share/qemu/edk2-i386-vars.fd", "/usr/local/share/qemu/edk2-i386-vars.fd",
) )
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
daemonize = False daemonize = False
extra_args = [] extra_args = []
def usage(stream=sys.stdout): def usage(stream=sys.stdout):
stream.write(f"""\ stream.write(f"""\
Usage: {Path(sys.argv[0]).name} [--daemon] [-- QEMU_ARGUMENT ...] Usage: {Path(sys.argv[0]).name} [--daemon] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
Start the Debian 13 virtual machine. A writable overlay is created under Start the Debian 13 virtual machine. A writable overlay is created in the state
.vm/debian13/, so the downloaded base image is never modified. directory (default: .vm/debian13/), so the downloaded base image is never
modified.
Options: Options:
--daemon Run in the background while keeping the graphical window --daemon Run in the background while keeping the graphical window
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
(default: .vm/debian13; overrides VM_STATE_DIR)
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
@@ -156,6 +154,12 @@ while args:
arg = args.pop(0) arg = args.pop(0)
if arg == "--daemon": if arg == "--daemon":
daemonize = True daemonize = True
elif arg == "--state-dir":
if not args:
die(f"{arg} requires a directory argument.", 2)
state_dir = Path(args.pop(0))
elif arg.startswith("--state-dir="):
state_dir = Path(arg.split("=", 1)[1])
elif arg in ("-h", "--help"): elif arg in ("-h", "--help"):
usage() usage()
sys.exit(0) sys.exit(0)
@@ -167,6 +171,12 @@ while args:
usage(sys.stderr) usage(sys.stderr)
sys.exit(2) sys.exit(2)
disk_image = Path(env("VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_image = Path(env("VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
for command_name in (qemu_binary, "qemu-img"): for command_name in (qemu_binary, "qemu-img"):
if shutil.which(command_name) is None: if shutil.which(command_name) is None:
die(f"{command_name} is not installed.") die(f"{command_name} is not installed.")
+16 -7
View File
@@ -52,8 +52,6 @@ else:
uses_shared_base_image = True uses_shared_base_image = True
state_dir = Path(env("FEDORA_VM_STATE_DIR") or script_dir / ".vm" / "fedora44") state_dir = Path(env("FEDORA_VM_STATE_DIR") or script_dir / ".vm" / "fedora44")
disk_image = Path(env("FEDORA_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_image = Path(env("FEDORA_VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
disk_size = env("FEDORA_VM_DISK_SIZE") or "20G" disk_size = env("FEDORA_VM_DISK_SIZE") or "20G"
memory = env("FEDORA_VM_MEMORY") or "8G" memory = env("FEDORA_VM_MEMORY") or "8G"
cpus = env("FEDORA_VM_CPUS") or "8" cpus = env("FEDORA_VM_CPUS") or "8"
@@ -117,23 +115,22 @@ else:
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", "/opt/homebrew/share/qemu/edk2-i386-vars.fd",
"/usr/local/share/qemu/edk2-i386-vars.fd", "/usr/local/share/qemu/edk2-i386-vars.fd",
) )
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
daemonize = False daemonize = False
extra_args = [] extra_args = []
def usage(stream=sys.stdout): def usage(stream=sys.stdout):
stream.write(f"""\ stream.write(f"""\
Usage: {Path(sys.argv[0]).name} [--daemon] [-- QEMU_ARGUMENT ...] Usage: {Path(sys.argv[0]).name} [--daemon] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
Start a Fedora 44 Cloud Base virtual machine. The launcher obtains a missing Start a Fedora 44 Cloud Base virtual machine. The launcher obtains a missing
base image through the shared image helper and creates a writable overlay base image through the shared image helper and creates a writable overlay
under .vm/fedora44/, leaving the base image unchanged. in the state directory, leaving the base image unchanged.
Options: Options:
--daemon Run in the background while keeping the graphical window --daemon Run in the background while keeping the graphical window
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
(default: .vm/fedora44; overrides FEDORA_VM_STATE_DIR)
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
@@ -157,6 +154,12 @@ while args:
arg = args.pop(0) arg = args.pop(0)
if arg == "--daemon": if arg == "--daemon":
daemonize = True daemonize = True
elif arg == "--state-dir":
if not args:
die(f"{arg} requires a directory argument.", 2)
state_dir = Path(args.pop(0))
elif arg.startswith("--state-dir="):
state_dir = Path(arg.split("=", 1)[1])
elif arg in ("-h", "--help"): elif arg in ("-h", "--help"):
usage() usage()
sys.exit(0) sys.exit(0)
@@ -168,6 +171,12 @@ while args:
usage(sys.stderr) usage(sys.stderr)
sys.exit(2) sys.exit(2)
disk_image = Path(env("FEDORA_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_image = Path(env("FEDORA_VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
for command_name in (qemu_binary, "qemu-img"): for command_name in (qemu_binary, "qemu-img"):
if shutil.which(command_name) is None: if shutil.which(command_name) is None:
die(f"{command_name} is not installed.") die(f"{command_name} is not installed.")
+19 -9
View File
@@ -37,8 +37,6 @@ def is_executable(path):
state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / ".vm" / "windows11") state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / ".vm" / "windows11")
disk_image = Path(env("WIN11_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_iso = Path(env("WIN11_VM_SEED_ISO") or disk_image.parent / "seed.iso")
disk_size = env("WIN11_VM_DISK_SIZE") or "64G" disk_size = env("WIN11_VM_DISK_SIZE") or "64G"
memory = env("WIN11_VM_MEMORY") or "8G" memory = env("WIN11_VM_MEMORY") or "8G"
cpus = env("WIN11_VM_CPUS") or "8" cpus = env("WIN11_VM_CPUS") or "8"
@@ -69,11 +67,6 @@ uefi_vars_template = pick_firmware(
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", "/opt/homebrew/share/qemu/edk2-i386-vars.fd",
"/usr/local/share/qemu/edk2-i386-vars.fd", "/usr/local/share/qemu/edk2-i386-vars.fd",
) )
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
monitor_socket = state_dir / "qemu-monitor.sock"
guest_agent_socket = state_dir / "qemu-guest-agent.sock"
daemonize = False daemonize = False
install_from_iso = False install_from_iso = False
extra_args = [] extra_args = []
@@ -81,15 +74,18 @@ extra_args = []
def usage(stream=sys.stdout): def usage(stream=sys.stdout):
stream.write(f"""\ stream.write(f"""\
Usage: {Path(sys.argv[0]).name} [--daemon] [--install-from-iso] [-- QEMU_ARGUMENT ...] Usage: {Path(sys.argv[0]).name} [--daemon] [--install-from-iso] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
If {disk_image} is missing, create it from the Windows 11 QCOW2 base image. If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from
the Windows 11 QCOW2 base image.
Use --install-from-iso to perform an automatic ISO installation instead. Use --install-from-iso to perform an automatic ISO installation instead.
If it exists, start Windows normally without attaching installation media. If it exists, start Windows normally without attaching installation media.
Options: Options:
--daemon Run QEMU in the background --daemon Run QEMU in the background
--install-from-iso Install from ISO when the system disk is missing --install-from-iso Install from ISO when the system disk is missing
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
(default: .vm/windows11; overrides WIN11_VM_STATE_DIR)
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
@@ -117,6 +113,12 @@ while args:
daemonize = True daemonize = True
elif arg == "--install-from-iso": elif arg == "--install-from-iso":
install_from_iso = True install_from_iso = True
elif arg == "--state-dir":
if not args:
die(f"{arg} requires a directory argument.", 2)
state_dir = Path(args.pop(0))
elif arg.startswith("--state-dir="):
state_dir = Path(arg.split("=", 1)[1])
elif arg in ("-h", "--help"): elif arg in ("-h", "--help"):
usage() usage()
sys.exit(0) sys.exit(0)
@@ -128,6 +130,14 @@ while args:
usage(sys.stderr) usage(sys.stderr)
sys.exit(2) sys.exit(2)
disk_image = Path(env("WIN11_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_iso = Path(env("WIN11_VM_SEED_ISO") or disk_image.parent / "seed.iso")
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log"
monitor_socket = state_dir / "qemu-monitor.sock"
guest_agent_socket = state_dir / "qemu-guest-agent.sock"
installing = False installing = False
installing_from_iso = False installing_from_iso = False
if not disk_image.is_file(): if not disk_image.is_file():