Compare commits

...
9 Commits
Author SHA1 Message Date
GuofangandClaude Sonnet 5.5 fd79eaa407 Add a Rocky Linux 9 virtual machine
run-rocky9.py with its QEMU config, port forwards (SSH on 2226) and
cloud-init seed, modelled on the Fedora 44 launcher. Update vm-images for
the rocky9 download target.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 11:02:14 +08:00
GuofangandClaude Sonnet 5.5 007ba992af Point vm-images submodule at git.wangguofang.net
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 10:46:59 +08:00
GuofangandClaude Sonnet 5.5 70d595d61e Update vm-images: install prompts default to yes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 23:36:15 +08:00
GuofangandClaude Sonnet 5.5 428d99108b Download aqtinstall through the wangguofang.net GitHub proxy
Also update vm-images: download Win32-OpenSSH through the same proxy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 09:28:34 +08:00
guofangandClaude Sonnet 5.5 fc8134ecf9 Use virtio-net and virtio-gpu on arm64 again
The base image now carries the VirtIO drivers, which restores the 2304x1440
display that ramfb had lost.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 22:09:07 +08:00
guofangandClaude Sonnet 5.5 d639e1f785 Update vm-images: build the base image with a window by default
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 21:31:12 +08:00
guofangandClaude Sonnet 5.5 33dbc91fa0 Update vm-images: more download connections
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 21:00:51 +08:00
guofangandClaude Sonnet 5.5 152006f57d Update vm-images: step-by-step prompts for building the base image
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 20:53:53 +08:00
guofangandClaude Sonnet 5.5 0ca1390895 Update vm-images: clearer missing base image error
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 20:26:09 +08:00
20 changed files with 586 additions and 21 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "vm-images"] [submodule "vm-images"]
path = vm-images path = vm-images
url = https://wangguofang.net/gitea/guofang/vm-images.git url = https://git.wangguofang.net/guofang/vm-images.git
+28 -6
View File
@@ -5,6 +5,7 @@ Start the required operating system with its distribution-specific launcher:
```bash ```bash
./run-debian13.py ./run-debian13.py
./run-almalinux10.py ./run-almalinux10.py
./run-rocky9.py
./run-fedora44.py ./run-fedora44.py
./run-ubuntu2604.py ./run-ubuntu2604.py
./run-windows11.py ./run-windows11.py
@@ -48,7 +49,7 @@ corresponding `*_STATE_DIR` environment variable:
With `--download-only`, a launcher only downloads the media it would fetch 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 automatically and then exits without creating a VM disk or starting QEMU: the
base image for Debian, AlmaLinux, Fedora and Ubuntu: base image for Debian, AlmaLinux, Rocky Linux, Fedora and Ubuntu:
```bash ```bash
./run-fedora44.py --download-only ./run-fedora44.py --download-only
@@ -101,6 +102,27 @@ stored alongside the VM disk at `vm/almalinux10/seed.iso`. If it already
exists after editing its source files, rebuild it explicitly with exists after editing its source files, rebuild it explicitly with
`./seediso/almalinux10/build-seed.sh vm/almalinux10/seed.iso`. `./seediso/almalinux10/build-seed.sh vm/almalinux10/seed.iso`.
## Rocky Linux 9
Start a Rocky Linux 9 GenericCloud Base VM with:
```bash
./run-rocky9.py
```
The launcher downloads the `rocky9` image through the shared helper when it
is missing, creates `vm/rocky9/disk.qcow2`, and builds its NoCloud seed from
`seediso/rocky9/`. SSH is forwarded to host port `2226` by default:
```bash
ssh -p 2226 guofang@localhost
```
Like the Debian and Fedora VMs, the Rocky Linux VM hardware (8G memory, 8 CPUs, drives) is
defined in `qemu/rocky9/config.ini`, with the accelerators, machine type and
platform-specific devices in `qemu/rocky9/<os>-<arch>.ini`. Other Rocky Linux settings use the `ROCKY_VM_` prefix.
Run `./run-rocky9.py --help` for the complete list of overrides.
## Fedora 44 ## Fedora 44
Start a Fedora 44 Cloud Base VM with: Start a Fedora 44 Cloud Base VM with:
@@ -167,7 +189,7 @@ from the shared Windows 11 QCOW2 base image:
It creates a persistent 64G disk at `vm/windows11/disk.qcow2` as a QCOW2 It creates a persistent 64G disk at `vm/windows11/disk.qcow2` as a QCOW2
overlay on the base image and nothing else: the launcher only creates the overlay on the base image and nothing else: the launcher only creates the
differential disk and starts QEMU. The base image is generic and holds nothing differential disk and starts QEMU. The base image is generic and holds nothing
but Windows (see `vm-images/README.md`): no account, no SSH, no VirtIO or other but Windows (see `vm-images/README.md`): no account, no SSH, no other
hardware-specific drivers, no machine identity. Like a cloud image, each VM is hardware-specific drivers, no machine identity. Like a cloud image, each VM is
configured on its first boot, from the seed ISO. configured on its first boot, from the seed ISO.
@@ -186,9 +208,9 @@ turns off Windows Update and copies the numbered installer scripts
(`seediso/windows11/NN-*.bat` and `.ps1`) to the desktop folder `Tools`. Later (`seediso/windows11/NN-*.bat` and `.ps1`) to the desktop folder `Tools`. Later
starts do not mount the seed ISO. starts do not mount the seed ISO.
Because the image has no VirtIO drivers, the VM only uses devices that Windows The base image carries the VirtIO drivers (`build-windows-base.sh` installs them
has inbox drivers for: an IDE disk and e1000e NIC on amd64, an NVMe disk, e1000e from the virtio-win ISO before generalizing), so on arm64 the VM uses virtio-net and
NIC and ramfb display on arm64 (a fixed-size framebuffer there). The SPICE virtio-gpu (2304x1440 on macOS); amd64 uses an IDE disk and an e1000e NIC. The SPICE
vdagent and QEMU Guest Agent come with the VirtIO guest tools, so host/guest vdagent and QEMU Guest Agent come with the VirtIO guest tools, so host/guest
clipboard integration and the guest agent are not available unless you install clipboard integration and the guest agent are not available unless you install
those tools in the VM yourself. those tools in the VM yourself.
@@ -222,7 +244,7 @@ SSH 2222 22
``` ```
Add a line to forward another port. A rule whose host port is already in use Add a line to forward another port. A rule whose host port is already in use
is skipped with a warning. `ALMA_VM_FORWARDS`, `FEDORA_VM_FORWARDS`, is skipped with a warning. `ALMA_VM_FORWARDS`, `ROCKY_VM_FORWARDS`, `FEDORA_VM_FORWARDS`,
`UBUNTU_VM_FORWARDS`, `VM_FORWARDS` (Debian) and `WIN11_VM_FORWARDS` select `UBUNTU_VM_FORWARDS`, `VM_FORWARDS` (Debian) and `WIN11_VM_FORWARDS` select
another file. `ALMA_VM_SSH_PORT`, `WIN11_VM_RDP_PORT`, `WIN11_VM_SSH_PORT` another file. `ALMA_VM_SSH_PORT`, `WIN11_VM_RDP_PORT`, `WIN11_VM_SSH_PORT`
and `--rdp-port`/`--ssh-port` still replace the host port of the rule with the and `--rdp-port`/`--ssh-port` still replace the host port of the rule with the
+48
View File
@@ -0,0 +1,48 @@
# QEMU configuration for the Rocky Linux 9 virtual machine, shared by all
# architectures. The accelerators, machine type and
# platform-specific devices are in <os>-<arch>.ini (os is linux or darwin),
# which QEMU reads after this file.
#
# run-rocky9.py fills in the file path placeholders below, writes the result
# to <state dir>/qemu.ini and passes it to QEMU with -readconfig. Options that
# have no -readconfig equivalent (-cpu, -display, daemon mode) are still added
# by the launcher.
[name]
guest = "rocky9"
[memory]
size = "8G"
[smp-opts]
cpus = "8"
# UEFI firmware: read-only code and the per-VM variable store.
[drive "pflash0"]
if = "pflash"
format = "raw"
readonly = "on"
file = "${uefi_code}"
[drive "pflash1"]
if = "pflash"
format = "raw"
file = "${uefi_vars}"
# Writable qcow2 overlay on top of the shared base image.
[drive "disk0"]
if = "none"
format = "qcow2"
file = "${disk_image}"
[device "disk0-dev"]
driver = "virtio-blk-pci"
drive = "disk0"
# The NIC is attached to the user-mode network "net0", which the launcher
# defines with -netdev from qemu/rocky9/forwards.conf because a config file
# section cannot hold more than one hostfwd.
[device "nic0"]
driver = "virtio-net-pci"
netdev = "net0"
+24
View File
@@ -0,0 +1,24 @@
# macOS host, arm64 part of the Rocky Linux 9 QEMU configuration; read after config.ini.
# Accelerators are tried in order: HVF on macOS, else emulation.
[accel]
accel = "hvf"
[accel]
accel = "tcg"
[machine]
type = "virt"
# Unlike q35, the "virt" machine has no default GPU or USB input devices.
[device "gpu0"]
driver = "virtio-gpu-pci"
[device "usb0"]
driver = "qemu-xhci"
[device "kbd0"]
driver = "usb-kbd"
[device "tablet0"]
driver = "usb-tablet"
+2
View File
@@ -0,0 +1,2 @@
# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT
SSH 2226 22
+11
View File
@@ -0,0 +1,11 @@
# Linux host, amd64 part of the Rocky Linux 9 QEMU configuration; read after config.ini.
# Accelerators are tried in order: KVM on Linux, else emulation.
[accel]
accel = "kvm"
[accel]
accel = "tcg"
[machine]
type = "q35"
+24
View File
@@ -0,0 +1,24 @@
# Linux host, arm64 part of the Rocky Linux 9 QEMU configuration; read after config.ini.
# Accelerators are tried in order: KVM on Linux, else emulation.
[accel]
accel = "kvm"
[accel]
accel = "tcg"
[machine]
type = "virt"
# Unlike q35, the "virt" machine has no default GPU or USB input devices.
[device "gpu0"]
driver = "virtio-gpu-pci"
[device "usb0"]
driver = "qemu-xhci"
[device "kbd0"]
driver = "usb-kbd"
[device "tablet0"]
driver = "usb-tablet"
+10 -5
View File
@@ -19,15 +19,20 @@
serial = "win11disk" serial = "win11disk"
bootindex = "1" bootindex = "1"
# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image # The base image carries the VirtIO drivers (build-windows-base.sh installs them
# contains no VirtIO drivers, so the network card is an Intel e1000e and the # from the virtio-win ISO), so the VM uses virtio-net and virtio-gpu. virtio-gpu
# display is ramfb (a fixed-size framebuffer). # is the only display, driven by viogpudo; ramfb is not added because Windows
# would then see a second adapter with a fixed-mode monitor. xres/yres set the
# preferred mode in the EDID (QEMU defaults to 1280x800); viogpudo accepted
# 1920x1200, 2048x1280 and 2304x1440 but not 2560x1440 or 2560x1600.
[device "nic0"] [device "nic0"]
driver = "e1000e" driver = "virtio-net-pci"
netdev = "net0" netdev = "net0"
[device "gpu0"] [device "gpu0"]
driver = "ramfb" driver = "virtio-gpu-pci"
xres = "2304"
yres = "1440"
[device "kbd0"] [device "kbd0"]
driver = "usb-kbd" driver = "usb-kbd"
+10 -5
View File
@@ -18,15 +18,20 @@
serial = "win11disk" serial = "win11disk"
bootindex = "1" bootindex = "1"
# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image # The base image carries the VirtIO drivers (build-windows-base.sh installs them
# contains no VirtIO drivers, so the network card is an Intel e1000e and the # from the virtio-win ISO), so the VM uses virtio-net and virtio-gpu. virtio-gpu
# display is ramfb (a fixed-size framebuffer). # is the only display, driven by viogpudo; ramfb is not added because Windows
# would then see a second adapter with a fixed-mode monitor. xres/yres set the
# preferred mode in the EDID (QEMU defaults to 1280x800); viogpudo accepted
# 1920x1200, 2048x1280 and 2304x1440 but not 2560x1440 or 2560x1600.
[device "nic0"] [device "nic0"]
driver = "e1000e" driver = "virtio-net-pci"
netdev = "net0" netdev = "net0"
[device "gpu0"] [device "gpu0"]
driver = "ramfb" driver = "virtio-gpu-pci"
xres = "2048"
yres = "1280"
[device "kbd0"] [device "kbd0"]
driver = "usb-kbd" driver = "usb-kbd"
Executable
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""Start the Rocky Linux 9 virtual machine."""
import os
import platform
import shutil
import string
import subprocess
import sys
from pathlib import Path
import vmforwards
script_dir = Path(__file__).resolve().parent
env = os.environ.get
def die(message, code=1):
print(f"Error: {message}", file=sys.stderr)
sys.exit(code)
def run(command):
"""Run a command, exiting with its status on failure (like `set -e`)."""
result = subprocess.run(command)
if result.returncode != 0:
sys.exit(result.returncode)
def is_executable(path):
return path.is_file() and os.access(path, os.X_OK)
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"
uses_shared_base_image = False
if env("ROCKY_VM_BASE_IMAGE"):
base_image = Path(env("ROCKY_VM_BASE_IMAGE"))
else:
if not is_executable(image_downloader):
die(f"shared image helper is unavailable: {image_downloader}")
result = subprocess.run(
[str(image_downloader), "--print-path", "rocky9"],
stdout=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
sys.exit(result.returncode)
base_image = Path(result.stdout.rstrip("\n"))
uses_shared_base_image = True
state_dir = Path(env("ROCKY_VM_STATE_DIR") or script_dir / "vm" / "rocky9")
disk_size = env("ROCKY_VM_DISK_SIZE") or "20G"
def normalize_arch(arch):
if arch in ("arm64", "aarch64"):
return "arm64"
if arch in ("x86_64", "amd64"):
return "amd64"
die(f"unsupported architecture: {arch}")
native_arch = normalize_arch(platform.machine())
host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch)
qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
qemu_config_template = Path(
env("ROCKY_VM_QEMU_CONFIG") or script_dir / "qemu" / "rocky9" / "config.ini")
host_os = platform.system().lower()
if host_os == "darwin" and host_arch == "amd64":
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
qemu_platform_config = script_dir / "qemu" / "rocky9" / f"{host_os}-{host_arch}.ini"
forwards_file = Path(
env("ROCKY_VM_FORWARDS") or script_dir / "qemu" / "rocky9" / "forwards.conf")
def pick_firmware(override, *candidates):
if override:
return Path(override)
for candidate in candidates:
if Path(candidate).is_file():
return Path(candidate)
return Path(candidates[0])
if host_arch == "arm64":
uefi_code = pick_firmware(
env("ROCKY_VM_UEFI_CODE"),
"/usr/share/AAVMF/AAVMF_CODE.fd",
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
"/opt/homebrew/share/qemu/edk2-aarch64-code.fd",
"/usr/local/share/qemu/edk2-aarch64-code.fd",
)
uefi_vars_template = pick_firmware(
env("ROCKY_VM_UEFI_VARS"),
"/usr/share/AAVMF/AAVMF_VARS.fd",
"/usr/share/edk2/aarch64/vars-template-pflash.raw",
"/opt/homebrew/share/qemu/edk2-arm-vars.fd",
"/usr/local/share/qemu/edk2-arm-vars.fd",
)
else:
uefi_code = pick_firmware(
env("ROCKY_VM_UEFI_CODE"),
"/usr/share/OVMF/OVMF_CODE_4M.fd",
"/usr/share/edk2/ovmf/OVMF_CODE.fd",
"/opt/homebrew/share/qemu/edk2-x86_64-code.fd",
"/usr/local/share/qemu/edk2-x86_64-code.fd",
)
uefi_vars_template = pick_firmware(
env("ROCKY_VM_UEFI_VARS"),
"/usr/share/OVMF/OVMF_VARS_4M.fd",
"/usr/share/edk2/ovmf/OVMF_VARS.fd",
"/opt/homebrew/share/qemu/edk2-i386-vars.fd",
"/usr/local/share/qemu/edk2-i386-vars.fd",
)
daemonize = False
download_only = False
extra_args = []
def usage(stream=sys.stdout):
stream.write(f"""\
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
Start a Rocky Linux 9 GenericCloud Base virtual machine. The launcher obtains a missing
base image through the shared image helper and creates a writable overlay
in the state directory, leaving the base image unchanged. The VM hardware
(memory, CPUs, ...) is defined in qemu/rocky9/config.ini, which is
rendered to qemu.ini in the state directory and loaded with QEMU's -readconfig,
followed by qemu/rocky9/<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g.
`-- -m 4G -smp 4`.
Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are
read from qemu/rocky9/forwards.conf.
Options:
--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
(default: vm/rocky9; overrides ROCKY_VM_STATE_DIR)
-h, --help Show this help
Environment variables:
ROCKY_VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper)
ROCKY_VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk)
ROCKY_VM_STATE_DIR Runtime state directory (default: vm/rocky9)
ROCKY_VM_DISK_IMAGE Writable overlay path
ROCKY_VM_DISK_SIZE New overlay virtual capacity (default: 20G)
VM_IMAGE_ARCH Guest/host architecture: arm64 or amd64 (default: auto-detected from the host)
ROCKY_VM_UEFI_CODE UEFI read-only firmware path (auto-detected for the selected architecture)
ROCKY_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
ROCKY_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
ROCKY_VM_QEMU_CONFIG QEMU config template rendered into the state directory and
passed to -readconfig (default: qemu/rocky9/config.ini)
ROCKY_VM_FORWARDS Port forward file (default: qemu/rocky9/forwards.conf)
""")
args = sys.argv[1:]
while args:
arg = args.pop(0)
if arg == "--daemon":
daemonize = True
elif arg == "--download-only":
download_only = 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"):
usage()
sys.exit(0)
elif arg == "--":
extra_args.extend(args)
break
else:
print(f"Error: unknown option: {arg}", file=sys.stderr)
usage(sys.stderr)
sys.exit(2)
disk_image = Path(env("ROCKY_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
seed_image = Path(env("ROCKY_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"
qemu_config = state_dir / "qemu.ini"
if not download_only:
for command_name in (qemu_binary, "qemu-img"):
if shutil.which(command_name) is None:
die(f"{command_name} is not installed.")
if not base_image.is_file():
if uses_shared_base_image:
run([str(image_downloader), "rocky9"])
else:
die(f"Rocky Linux base image was not found: {base_image}")
if not base_image.is_file():
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):
if not firmware_file.is_file():
die(f"required UEFI firmware was not found: {firmware_file}")
creating = not disk_image.is_file()
if creating and not seed_image.is_file():
seed_builder = script_dir / "seediso" / "rocky9" / "build-seed.sh"
if not is_executable(seed_builder):
die(f"seed image is missing and builder is unavailable: {seed_builder}")
print(f"Seed image not found; building {seed_image}...", flush=True)
run([str(seed_builder), str(seed_image)])
state_dir.mkdir(parents=True, exist_ok=True)
# QEMU requires the code and vars pflash images to be exactly the same size.
# Homebrew's edk2 firmware ships code/vars pairs at mismatched sizes (true for
# both the aarch64 and x86_64 pairs), so pad per-VM copies of whichever one is
# smaller up to match the other.
uefi_code_flash = state_dir / "OVMF_CODE_4M.fd"
if not uefi_code_flash.is_file() or not uefi_vars.is_file():
flash_size = max(uefi_code.stat().st_size, uefi_vars_template.stat().st_size)
for source, target in ((uefi_code, uefi_code_flash), (uefi_vars_template, uefi_vars)):
if not target.is_file():
shutil.copyfile(source, target)
os.truncate(target, flash_size)
if pid_file.is_file():
old_pid = pid_file.read_text().strip()
if old_pid.isdigit():
try:
os.kill(int(old_pid), 0)
except OSError:
pass
else:
die(f"the Rocky Linux virtual machine is already running (PID {old_pid}).")
pid_file.unlink(missing_ok=True)
if creating:
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
"-b", str(base_image), str(disk_image), disk_size])
print(f"Created writable disk overlay: {disk_image}")
is_macos = platform.system() == "Darwin"
display_backend = env("ROCKY_VM_DISPLAY") or ("cocoa" if is_macos else "gtk")
# Most of the VM definition lives in the QEMU config template; fill in the
# file paths and hand the rendered file to QEMU with -readconfig.
config_values = {
"uefi_code": uefi_code_flash,
"uefi_vars": uefi_vars,
"disk_image": disk_image,
}
for key, value in config_values.items():
# QEMU's config file parser has no escape syntax for quoted values.
if any(char in str(value) for char in '"\n'):
die(f"{key} must not contain double quotes or newlines: {value}")
for config_file in (qemu_config_template, qemu_platform_config):
if not config_file.is_file():
die(f"QEMU config file was not found: {config_file}")
try:
qemu_config_text = string.Template(qemu_config_template.read_text()).substitute(
{key: str(value) for key, value in config_values.items()})
except (KeyError, ValueError) as error:
die(f"invalid placeholder in {qemu_config_template}: {error}")
qemu_config.write_text(qemu_config_text)
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(forwards_file))
qemu_args = [
"-readconfig", str(qemu_config),
"-readconfig", str(qemu_platform_config),
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
"-cpu", "max",
# A config file section cannot hold two hostfwd entries, so the network
# that the NIC in the config file uses is defined here. -nic none stops
# QEMU from adding its default NIC as well.
"-netdev", vmforwards.netdev_argument(forwards),
"-nic", "none",
"-display", display_backend,
]
if creating:
# cloud-init reads the seed ISO on the first boot only (the users, keys,
# network and packages it sets up stay on the disk), so it is attached only
# when the disk is created.
qemu_args += ["-drive", f"file={seed_image},if=none,id=seed0,format=raw,readonly=on",
"-device", "virtio-blk-pci,drive=seed0"]
print(f"Starting Rocky Linux 9 ({host_arch}) from {qemu_config_template} "
f"and {qemu_platform_config}.", flush=True)
vmforwards.print_forwards(forwards)
if daemonize:
log_file.write_text("")
qemu_args += [
"-serial", f"file:{log_file}",
"-monitor", "none",
"-daemonize",
"-pidfile", str(pid_file),
]
run([qemu_binary, *qemu_args, *extra_args])
print(f"VM started (PID {pid_file.read_text().strip()}); console log: {log_file}")
else:
os.execvp(qemu_binary, [qemu_binary, *qemu_args, *extra_args])
+2 -2
View File
@@ -97,8 +97,8 @@ scripts on the desktop). If the disk exists, start Windows normally, without the
seed ISO. seed ISO.
Make the base image once with vm-images/build-windows-base.sh; it installs Make the base image once with vm-images/build-windows-base.sh; it installs
Windows from the ISO and generalizes the result. The base image contains only Windows from the ISO and generalizes the result. The base image contains only
Windows (no account, no SSH, no VirtIO or other hardware-specific drivers), so Windows plus the VirtIO drivers (no account, no SSH, no other
the devices in qemu/windows11 are ones Windows has inbox drivers for. hardware-specific drivers).
The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in
qemu/windows11/config.ini, which is rendered to qemu.ini in the state directory and qemu/windows11/config.ini, which is rendered to qemu.ini in the state directory and
loaded with QEMU's -readconfig, followed by qemu/windows11/<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and loaded with QEMU's -readconfig, followed by qemu/windows11/<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and
+1
View File
@@ -7,6 +7,7 @@ Each operating system has an independent NoCloud configuration and generated
seediso/ seediso/
├── debian13/ ├── debian13/
├── almalinux10/ ├── almalinux10/
├── rocky9/
├── fedora44/ ├── fedora44/
└── ubuntu2604/ └── ubuntu2604/
``` ```
+2
View File
@@ -0,0 +1,2 @@
seed.iso
*.iso
+17
View File
@@ -0,0 +1,17 @@
# Rocky Linux 9 seed.iso
This directory contains the cloud-init NoCloud configuration used by
`run-rocky9.py`.
`build-seed.sh` injects your SSH public key into `user-data` (the `@SSH_PUBKEY@`
placeholder): `$SSH_PUBKEY_FILE`, else the first `~/.ssh/id_*.pub`, else the first
key offered by `ssh-agent`.
After changing `user-data`, `meta-data`, or `network-config`, remove the old
`seed.iso` and rebuild it:
```bash
./seediso/rocky9/build-seed.sh
```
Use a unique `instance-id` when cloning this configuration for another VM.
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
output="${1:-${script_dir}/seed.iso}"
files=(user-data meta-data)
# Public key injected into user-data: $SSH_PUBKEY_FILE, else the first
# ~/.ssh/id_*.pub, else the first key offered by ssh-agent.
pubkey=""
if [[ -n "${SSH_PUBKEY_FILE:-}" ]]; then
pubkey="$(head -n1 -- "${SSH_PUBKEY_FILE}")"
else
for f in "${HOME}"/.ssh/id_ed25519.pub "${HOME}"/.ssh/id_*.pub; do
if [[ -f "$f" ]]; then pubkey="$(head -n1 -- "$f")"; break; fi
done
if [[ -z "$pubkey" ]] && command -v ssh-add >/dev/null 2>&1; then
pubkey="$(ssh-add -L 2>/dev/null | head -n1 || true)"
fi
fi
if [[ "$pubkey" != ssh-* && "$pubkey" != ecdsa-* && "$pubkey" != sk-* ]]; then
printf 'Error: no SSH public key found; set SSH_PUBKEY_FILE.\n' >&2
exit 1
fi
work="$(mktemp -d)"
trap 'rm -rf -- "${work}"' EXIT
template="$(<"${script_dir}/user-data")"
printf '%s\n' "${template//@SSH_PUBKEY@/${pubkey}}" > "${work}/user-data"
cp -- "${script_dir}/meta-data" "${work}/meta-data"
if [[ -f "${script_dir}/network-config" ]]; then
cp -- "${script_dir}/network-config" "${work}/network-config"
fi
mkdir -p "$(dirname -- "$output")"
if [[ -f "${script_dir}/network-config" ]]; then
files+=(network-config)
fi
if command -v cloud-localds >/dev/null 2>&1; then
args=()
if [[ -f "${script_dir}/network-config" ]]; then
args+=(--network-config="${work}/network-config")
fi
cloud-localds "${args[@]}" "${output}" \
"${work}/user-data" "${work}/meta-data"
elif command -v xorriso >/dev/null 2>&1; then
xorriso -as mkisofs -quiet -volid cidata -joliet -rock \
-output "${output}" "${files[@]/#/${work}/}"
elif command -v genisoimage >/dev/null 2>&1; then
genisoimage -quiet -output "${output}" -volid cidata -joliet -rock \
"${files[@]/#/${work}/}"
elif command -v mkisofs >/dev/null 2>&1; then
mkisofs -quiet -output "${output}" -volid cidata -joliet -rock \
"${files[@]/#/${work}/}"
else
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
exit 1
fi
printf 'Created %s\n' "${output}"
+2
View File
@@ -0,0 +1,2 @@
instance-id: rocky9-host-001
local-hostname: rocky9-host
+10
View File
@@ -0,0 +1,10 @@
version: 2
ethernets:
all-en:
match:
name: "en*"
dhcp4: true
all-eth:
match:
name: "eth*"
dhcp4: true
+18
View File
@@ -0,0 +1,18 @@
#cloud-config
preserve_hostname: false
manage_etc_hosts: true
ssh_pwauth: true
users:
- name: guofang
gecos: Guofang
groups: [wheel]
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD: ALL"
lock_passwd: false
ssh_authorized_keys:
- @SSH_PUBKEY@
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
chpasswd:
expire: false
+1 -1
View File
@@ -23,7 +23,7 @@ set "QT_ARCH=win64_%QT_DIR%"
rem aqt has no Arm64 build; the x64 one runs under emulation on Arm64 Windows. rem aqt has no Arm64 build; the x64 one runs under emulation on Arm64 Windows.
echo Downloading aqtinstall... echo Downloading aqtinstall...
curl.exe -L --fail -o "%AQT%" https://github.com/miurahr/aqtinstall/releases/download/v3.3.0/aqt_x64.exe curl.exe -L --fail -o "%AQT%" https://wangguofang.net/github.com/miurahr/aqtinstall/releases/download/v3.3.0/aqt_x64.exe
if errorlevel 1 ( if errorlevel 1 (
echo Download failed. echo Download failed.
if not defined INSTALL_ALL pause if not defined INSTALL_ALL pause