Add --download-only to the VM launchers
The option downloads the media a launcher would fetch automatically and exits without checking for QEMU, creating a VM disk or starting the VM: the base image for Debian, AlmaLinux and Fedora, and the VirtIO tools ISO for Windows 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YKxXLyiL7XJ3a9e8HmR2my
This commit is contained in:
1 parent
7912a1549e
commit
204051f930
5 files changed
+74
-15
No files matched your search
@@ -44,6 +44,15 @@ corresponding `*_STATE_DIR` environment variable:
|
|||||||
./run-debian13.py --state-dir /srv/vms/debian13-test --daemon
|
./run-debian13.py --state-dir /srv/vms/debian13-test --daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
|
With `--download-only`, a launcher only downloads the media it would fetch
|
||||||
|
automatically and then exits without creating a VM disk or starting QEMU: the
|
||||||
|
base image for Debian, AlmaLinux and Fedora, and the VirtIO tools ISO for
|
||||||
|
Windows 11 (whose installation ISO and base QCOW2 are supplied manually):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-fedora44.py --download-only
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
+10
-2
@@ -116,12 +116,13 @@ else:
|
|||||||
"/usr/local/share/qemu/edk2-i386-vars.fd",
|
"/usr/local/share/qemu/edk2-i386-vars.fd",
|
||||||
)
|
)
|
||||||
daemonize = False
|
daemonize = False
|
||||||
|
download_only = 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] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
|
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--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
|
||||||
@@ -129,6 +130,7 @@ 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
|
||||||
|
--download-only Download the base image if it is missing, then exit
|
||||||
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
||||||
(default: .vm/almalinux10; overrides ALMA_VM_STATE_DIR)
|
(default: .vm/almalinux10; overrides ALMA_VM_STATE_DIR)
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
@@ -154,6 +156,8 @@ while args:
|
|||||||
arg = args.pop(0)
|
arg = args.pop(0)
|
||||||
if arg == "--daemon":
|
if arg == "--daemon":
|
||||||
daemonize = True
|
daemonize = True
|
||||||
|
elif arg == "--download-only":
|
||||||
|
download_only = True
|
||||||
elif arg == "--state-dir":
|
elif arg == "--state-dir":
|
||||||
if not args:
|
if not args:
|
||||||
die(f"{arg} requires a directory argument.", 2)
|
die(f"{arg} requires a directory argument.", 2)
|
||||||
@@ -177,7 +181,8 @@ uefi_vars = state_dir / "OVMF_VARS_4M.fd"
|
|||||||
pid_file = state_dir / "qemu.pid"
|
pid_file = state_dir / "qemu.pid"
|
||||||
log_file = state_dir / "console.log"
|
log_file = state_dir / "console.log"
|
||||||
|
|
||||||
for command_name in (qemu_binary, "qemu-img"):
|
if not download_only:
|
||||||
|
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.")
|
||||||
|
|
||||||
@@ -188,6 +193,9 @@ if not base_image.is_file():
|
|||||||
die(f"AlmaLinux base image was not found: {base_image}")
|
die(f"AlmaLinux base image was not found: {base_image}")
|
||||||
if not base_image.is_file():
|
if not base_image.is_file():
|
||||||
die(f"downloader did not create: {base_image}")
|
die(f"downloader did not create: {base_image}")
|
||||||
|
if download_only:
|
||||||
|
print(f"Base image is ready: {base_image}")
|
||||||
|
sys.exit(0)
|
||||||
for firmware_file in (uefi_code, uefi_vars_template):
|
for firmware_file in (uefi_code, uefi_vars_template):
|
||||||
if not firmware_file.is_file():
|
if not firmware_file.is_file():
|
||||||
die(f"required UEFI firmware was not found: {firmware_file}")
|
die(f"required UEFI firmware was not found: {firmware_file}")
|
||||||
|
|||||||
+10
-2
@@ -111,12 +111,13 @@ else:
|
|||||||
"/usr/local/share/qemu/edk2-i386-vars.fd",
|
"/usr/local/share/qemu/edk2-i386-vars.fd",
|
||||||
)
|
)
|
||||||
daemonize = False
|
daemonize = False
|
||||||
|
download_only = 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] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
|
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
|
||||||
|
|
||||||
Start the Debian 13 virtual machine. A writable overlay is created in the state
|
Start the Debian 13 virtual machine. A writable overlay is created in the state
|
||||||
directory (default: .vm/debian13/), so the downloaded base image is never
|
directory (default: .vm/debian13/), so the downloaded base image is never
|
||||||
@@ -127,6 +128,7 @@ QEMU arguments override them, e.g. `-- -m 4G -smp 4`.
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--daemon Run in the background while keeping the graphical window
|
--daemon Run in the background while keeping the graphical window
|
||||||
|
--download-only Download the base image if it is missing, then exit
|
||||||
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
||||||
(default: .vm/debian13; overrides VM_STATE_DIR)
|
(default: .vm/debian13; overrides VM_STATE_DIR)
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
@@ -151,6 +153,8 @@ while args:
|
|||||||
arg = args.pop(0)
|
arg = args.pop(0)
|
||||||
if arg == "--daemon":
|
if arg == "--daemon":
|
||||||
daemonize = True
|
daemonize = True
|
||||||
|
elif arg == "--download-only":
|
||||||
|
download_only = True
|
||||||
elif arg == "--state-dir":
|
elif arg == "--state-dir":
|
||||||
if not args:
|
if not args:
|
||||||
die(f"{arg} requires a directory argument.", 2)
|
die(f"{arg} requires a directory argument.", 2)
|
||||||
@@ -175,7 +179,8 @@ pid_file = state_dir / "qemu.pid"
|
|||||||
log_file = state_dir / "console.log"
|
log_file = state_dir / "console.log"
|
||||||
qemu_config = state_dir / "qemu.ini"
|
qemu_config = state_dir / "qemu.ini"
|
||||||
|
|
||||||
for command_name in (qemu_binary, "qemu-img"):
|
if not download_only:
|
||||||
|
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.")
|
||||||
|
|
||||||
@@ -186,6 +191,9 @@ if not base_image.is_file():
|
|||||||
die(f"Debian base image was not found: {base_image}")
|
die(f"Debian base image was not found: {base_image}")
|
||||||
if not base_image.is_file():
|
if not base_image.is_file():
|
||||||
die(f"downloader did not create: {base_image}")
|
die(f"downloader did not create: {base_image}")
|
||||||
|
if download_only:
|
||||||
|
print(f"Base image is ready: {base_image}")
|
||||||
|
sys.exit(0)
|
||||||
for firmware_file in (uefi_code, uefi_vars_template):
|
for firmware_file in (uefi_code, uefi_vars_template):
|
||||||
if not firmware_file.is_file():
|
if not firmware_file.is_file():
|
||||||
die(f"required UEFI firmware was not found: {firmware_file}")
|
die(f"required UEFI firmware was not found: {firmware_file}")
|
||||||
|
|||||||
+10
-2
@@ -112,12 +112,13 @@ else:
|
|||||||
"/usr/local/share/qemu/edk2-i386-vars.fd",
|
"/usr/local/share/qemu/edk2-i386-vars.fd",
|
||||||
)
|
)
|
||||||
daemonize = False
|
daemonize = False
|
||||||
|
download_only = 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] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
|
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--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
|
||||||
@@ -129,6 +130,7 @@ followed by qemu/fedora44-<arch>.ini. Later QEMU arguments override them, e.g.
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--daemon Run in the background while keeping the graphical window
|
--daemon Run in the background while keeping the graphical window
|
||||||
|
--download-only Download the base image if it is missing, then exit
|
||||||
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
||||||
(default: .vm/fedora44; overrides FEDORA_VM_STATE_DIR)
|
(default: .vm/fedora44; overrides FEDORA_VM_STATE_DIR)
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
@@ -153,6 +155,8 @@ while args:
|
|||||||
arg = args.pop(0)
|
arg = args.pop(0)
|
||||||
if arg == "--daemon":
|
if arg == "--daemon":
|
||||||
daemonize = True
|
daemonize = True
|
||||||
|
elif arg == "--download-only":
|
||||||
|
download_only = True
|
||||||
elif arg == "--state-dir":
|
elif arg == "--state-dir":
|
||||||
if not args:
|
if not args:
|
||||||
die(f"{arg} requires a directory argument.", 2)
|
die(f"{arg} requires a directory argument.", 2)
|
||||||
@@ -177,7 +181,8 @@ pid_file = state_dir / "qemu.pid"
|
|||||||
log_file = state_dir / "console.log"
|
log_file = state_dir / "console.log"
|
||||||
qemu_config = state_dir / "qemu.ini"
|
qemu_config = state_dir / "qemu.ini"
|
||||||
|
|
||||||
for command_name in (qemu_binary, "qemu-img"):
|
if not download_only:
|
||||||
|
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.")
|
||||||
|
|
||||||
@@ -188,6 +193,9 @@ if not base_image.is_file():
|
|||||||
die(f"Fedora base image was not found: {base_image}")
|
die(f"Fedora base image was not found: {base_image}")
|
||||||
if not base_image.is_file():
|
if not base_image.is_file():
|
||||||
die(f"downloader did not create: {base_image}")
|
die(f"downloader did not create: {base_image}")
|
||||||
|
if download_only:
|
||||||
|
print(f"Base image is ready: {base_image}")
|
||||||
|
sys.exit(0)
|
||||||
for firmware_file in (uefi_code, uefi_vars_template):
|
for firmware_file in (uefi_code, uefi_vars_template):
|
||||||
if not firmware_file.is_file():
|
if not firmware_file.is_file():
|
||||||
die(f"required UEFI firmware was not found: {firmware_file}")
|
die(f"required UEFI firmware was not found: {firmware_file}")
|
||||||
|
|||||||
+35
-9
@@ -36,6 +36,18 @@ def is_executable(path):
|
|||||||
return path.is_file() and os.access(path, os.X_OK)
|
return path.is_file() and os.access(path, os.X_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def find_image_downloader():
|
||||||
|
if env("VM_IMAGE_HELPER"):
|
||||||
|
image_downloader = Path(env("VM_IMAGE_HELPER"))
|
||||||
|
elif is_executable(script_dir / "vm-images" / "download-images.sh"):
|
||||||
|
image_downloader = script_dir / "vm-images" / "download-images.sh"
|
||||||
|
else:
|
||||||
|
image_downloader = script_dir / ".." / "vm-images" / "download-images.sh"
|
||||||
|
if not is_executable(image_downloader):
|
||||||
|
die(f"shared image helper is unavailable: {image_downloader}")
|
||||||
|
return image_downloader
|
||||||
|
|
||||||
|
|
||||||
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_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"
|
||||||
@@ -69,12 +81,13 @@ uefi_vars_template = pick_firmware(
|
|||||||
)
|
)
|
||||||
daemonize = False
|
daemonize = False
|
||||||
install_from_iso = False
|
install_from_iso = False
|
||||||
|
download_only = 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] [--install-from-iso] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
|
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--install-from-iso] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
|
||||||
|
|
||||||
If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from
|
If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from
|
||||||
the Windows 11 QCOW2 base image.
|
the Windows 11 QCOW2 base image.
|
||||||
@@ -83,6 +96,8 @@ If it exists, start Windows normally without attaching installation media.
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--daemon Run QEMU in the background
|
--daemon Run QEMU in the background
|
||||||
|
--download-only Download the VirtIO tools ISO if it is missing, then exit
|
||||||
|
(the Windows ISO and base QCOW2 are supplied manually)
|
||||||
--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
|
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
||||||
(default: .vm/windows11; overrides WIN11_VM_STATE_DIR)
|
(default: .vm/windows11; overrides WIN11_VM_STATE_DIR)
|
||||||
@@ -111,6 +126,8 @@ while args:
|
|||||||
arg = args.pop(0)
|
arg = args.pop(0)
|
||||||
if arg == "--daemon":
|
if arg == "--daemon":
|
||||||
daemonize = True
|
daemonize = True
|
||||||
|
elif arg == "--download-only":
|
||||||
|
download_only = True
|
||||||
elif arg == "--install-from-iso":
|
elif arg == "--install-from-iso":
|
||||||
install_from_iso = True
|
install_from_iso = True
|
||||||
elif arg == "--state-dir":
|
elif arg == "--state-dir":
|
||||||
@@ -130,6 +147,22 @@ while args:
|
|||||||
usage(sys.stderr)
|
usage(sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
if download_only:
|
||||||
|
# The VirtIO tools ISO is the only medium the launcher downloads itself.
|
||||||
|
if env("WIN11_VM_TOOLS_ISO"):
|
||||||
|
tools_iso = Path(env("WIN11_VM_TOOLS_ISO"))
|
||||||
|
if not tools_iso.is_file():
|
||||||
|
die(f"VirtIO Windows tools ISO was not found: {tools_iso}")
|
||||||
|
else:
|
||||||
|
image_downloader = find_image_downloader()
|
||||||
|
tools_iso = Path(capture([str(image_downloader), "--print-path", "virtio-win"]))
|
||||||
|
if not tools_iso.is_file():
|
||||||
|
run([str(image_downloader), "virtio-win"])
|
||||||
|
if not tools_iso.is_file():
|
||||||
|
die(f"downloader did not create: {tools_iso}")
|
||||||
|
print(f"VirtIO Windows tools ISO is ready: {tools_iso}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
disk_image = Path(env("WIN11_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
|
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")
|
seed_iso = Path(env("WIN11_VM_SEED_ISO") or disk_image.parent / "seed.iso")
|
||||||
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
|
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
|
||||||
@@ -143,14 +176,7 @@ installing_from_iso = False
|
|||||||
if not disk_image.is_file():
|
if not disk_image.is_file():
|
||||||
installing = True
|
installing = True
|
||||||
installing_from_iso = install_from_iso
|
installing_from_iso = install_from_iso
|
||||||
if env("VM_IMAGE_HELPER"):
|
image_downloader = find_image_downloader()
|
||||||
image_downloader = Path(env("VM_IMAGE_HELPER"))
|
|
||||||
elif is_executable(script_dir / "vm-images" / "download-images.sh"):
|
|
||||||
image_downloader = script_dir / "vm-images" / "download-images.sh"
|
|
||||||
else:
|
|
||||||
image_downloader = script_dir / ".." / "vm-images" / "download-images.sh"
|
|
||||||
if not is_executable(image_downloader):
|
|
||||||
die(f"shared image helper is unavailable: {image_downloader}")
|
|
||||||
if install_from_iso:
|
if install_from_iso:
|
||||||
install_iso = Path(env("WIN11_VM_INSTALL_ISO")
|
install_iso = Path(env("WIN11_VM_INSTALL_ISO")
|
||||||
or capture([str(image_downloader), "--print-path", "windows11", "--iso"]))
|
or capture([str(image_downloader), "--print-path", "windows11", "--iso"]))
|
||||||
|
|||||||
Reference in new issue
Block a user