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"]
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
./run-debian13.py
./run-almalinux10.py
./run-rocky9.py
./run-fedora44.py
./run-ubuntu2604.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
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
./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
`./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
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
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
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
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
starts do not mount the seed ISO.
Because the image has no VirtIO drivers, the VM only uses devices that Windows
has inbox drivers for: an IDE disk and e1000e NIC on amd64, an NVMe disk, e1000e
NIC and ramfb display on arm64 (a fixed-size framebuffer there). The SPICE
The base image carries the VirtIO drivers (`build-windows-base.sh` installs them
from the virtio-win ISO before generalizing), so on arm64 the VM uses virtio-net and
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
clipboard integration and the guest agent are not available unless you install
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
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
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
+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"
bootindex = "1"
# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image
# contains no VirtIO drivers, so the network card is an Intel e1000e and the
# display is ramfb (a fixed-size framebuffer).
# The base image carries the VirtIO drivers (build-windows-base.sh installs them
# from the virtio-win ISO), so the VM uses virtio-net and virtio-gpu. virtio-gpu
# 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"]
driver = "e1000e"
driver = "virtio-net-pci"
netdev = "net0"
[device "gpu0"]
driver = "ramfb"
driver = "virtio-gpu-pci"
xres = "2304"
yres = "1440"
[device "kbd0"]
driver = "usb-kbd"
+10 -5
View File
@@ -18,15 +18,20 @@
serial = "win11disk"
bootindex = "1"
# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image
# contains no VirtIO drivers, so the network card is an Intel e1000e and the
# display is ramfb (a fixed-size framebuffer).
# The base image carries the VirtIO drivers (build-windows-base.sh installs them
# from the virtio-win ISO), so the VM uses virtio-net and virtio-gpu. virtio-gpu
# 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"]
driver = "e1000e"
driver = "virtio-net-pci"
netdev = "net0"
[device "gpu0"]
driver = "ramfb"
driver = "virtio-gpu-pci"
xres = "2048"
yres = "1280"
[device "kbd0"]
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.
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 (no account, no SSH, no VirtIO or other hardware-specific drivers), so
the devices in qemu/windows11 are ones Windows has inbox drivers for.
Windows plus the VirtIO drivers (no account, no SSH, no other
hardware-specific drivers).
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
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/
├── debian13/
├── almalinux10/
├── rocky9/
├── fedora44/
└── 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.
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 (
echo Download failed.
if not defined INSTALL_ALL pause