Compare commits
12
Commits
b149dd4bc3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd79eaa407 | ||
|
|
007ba992af | ||
|
|
70d595d61e | ||
|
|
428d99108b | ||
|
|
fc8134ecf9 | ||
|
|
d639e1f785 | ||
|
|
33dbc91fa0 | ||
|
|
152006f57d | ||
|
|
0ca1390895 | ||
|
|
48c4477ec4 | ||
|
|
a920ba0ca5 | ||
|
|
fe7250870e |
No files matched your search
+1
-1
@@ -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
|
||||||
@@ -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,8 +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, and the VirtIO tools ISO
|
base image for Debian, AlmaLinux, Rocky Linux, Fedora and Ubuntu:
|
||||||
for Windows 11 (whose installation ISO and base QCOW2 are supplied manually):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./run-fedora44.py --download-only
|
./run-fedora44.py --download-only
|
||||||
@@ -102,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:
|
||||||
@@ -165,43 +186,49 @@ from the shared Windows 11 QCOW2 base image:
|
|||||||
./run-windows11.py
|
./run-windows11.py
|
||||||
```
|
```
|
||||||
|
|
||||||
It creates a persistent 64G disk at `vm/windows11/disk.qcow2`, automatically
|
It creates a persistent 64G disk at `vm/windows11/disk.qcow2` as a QCOW2
|
||||||
partitions it, completes OOBE, creates the local administrator `guofang` with
|
overlay on the base image and nothing else: the launcher only creates the
|
||||||
the initial password `wang`, and enables Remote Desktop on
|
differential disk and starts QEMU. The base image is generic and holds nothing
|
||||||
`0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on the host itself). During the first login it also silently installs the VirtIO
|
but Windows (see `vm-images/README.md`): no account, no SSH, no other
|
||||||
drivers (the guest tools installer on amd64, the Arm64 driver packages from the
|
hardware-specific drivers, no machine identity. Like a cloud image, each VM is
|
||||||
VirtIO ISO with `pnputil` on arm64), without which the guest has no network on
|
configured on its first boot, from the seed ISO.
|
||||||
arm64. The installer includes the VirtIO drivers, SPICE vdagent, and QEMU Guest
|
|
||||||
Agent. The launcher exposes the vdagent channel for host/guest clipboard
|
Whenever the disk does not exist yet, the launcher also mounts the seed ISO
|
||||||
integration and the QEMU Guest Agent at `vm/windows11/qemu-guest-agent.sock`.
|
`vm/windows11/seed.iso` (beside the disk; built from `seediso/windows11/` when
|
||||||
Change the password after first login. The OpenSSH Server is installed during
|
missing or outdated, with the OpenSSH zip and the host's SSH public key,
|
||||||
the first logon from the Win32-OpenSSH zip for the host architecture, which the
|
`$SSH_PUBLIC_KEY_FILE` or the first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`,
|
||||||
seed ISO carries (downloaded by `vm-images/download-images.sh openssh-win`), so
|
`id_rsa.pub`). Windows reads the `Unattend.xml` in the root of that CD-ROM
|
||||||
Windows Update is not needed. Connect with `ssh guofang@localhost -p 2222`; the
|
during its specialize and OOBE passes: host name `WIN11`, time zone, Remote
|
||||||
seed ISO also carries the host's SSH public key (`$SSH_PUBLIC_KEY_FILE`, else the
|
Desktop on `0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on
|
||||||
first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, `id_rsa.pub`) for passwordless
|
the host itself), the local administrator `guofang` (initial password `wang`;
|
||||||
administrator login.
|
change it after first login) and automatic logon. At the first logon it runs
|
||||||
|
`Configure-Windows.ps1` from the seed ISO, which installs the OpenSSH Server
|
||||||
|
from the zip with the SSH key (connect with `ssh guofang@localhost -p 2222`),
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
The Windows VM hardware (8G memory, 8 CPUs, system disk, devices)
|
The Windows VM hardware (8G memory, 8 CPUs, system disk, devices)
|
||||||
is defined in `qemu/windows11/config.ini`, with the accelerators and machine type in
|
is defined in `qemu/windows11/config.ini`, with the accelerators and machine type in
|
||||||
`qemu/windows11/<os>-<arch>.ini`; the launcher renders the former to
|
`qemu/windows11/<os>-<arch>.ini`; the launcher renders the former to
|
||||||
`vm/windows11/qemu.ini` and loads both with `-readconfig`. Only the network
|
`vm/windows11/qemu.ini` and loads both with `-readconfig`. Only the network
|
||||||
(see Port forwards) and the installation media are still set by the launcher. QEMU arguments after
|
(see Port forwards) and the seed ISO are still set by the launcher. QEMU arguments after
|
||||||
`--` override the config files, for example `./run-windows11.py -- -m 4G -smp 4`.
|
`--` override the config files, for example `./run-windows11.py -- -m 4G -smp 4`.
|
||||||
The generated answer-file ISO is stored beside the disk as
|
|
||||||
`vm/windows11/seed.iso`.
|
|
||||||
|
|
||||||
To install from the Windows ISO instead, use `./run-windows11.py
|
The launcher has no installation options and stops with a hint if the base image
|
||||||
--install-from-iso`. This choice only applies while creating a missing system
|
is missing. Make the base image once with `vm-images/build-windows-base.sh`,
|
||||||
disk; subsequent starts always boot the existing system disk normally. During
|
which is self-contained in `vm-images/` (its own answer file, QEMU config and
|
||||||
ISO installation, the launcher resolves its tools ISO through the shared image
|
installation logic, written in bash): it installs Windows unattended from the
|
||||||
helper and downloads it when missing. Override the base QCOW2, source ISO,
|
ISO into a scratch disk, runs `sysprep` as its last step and stores the disk as
|
||||||
tools ISO, RDP port, or SSH port with the `WIN11_VM_` environment
|
the read-only base image. Override the base QCOW2, seed ISO, RDP port, or SSH
|
||||||
variables shown by `./run-windows11.py --help`. For example, when the VirtIO ISO
|
port with the `WIN11_VM_` environment variables shown by
|
||||||
is elsewhere:
|
`./run-windows11.py --help`.
|
||||||
|
|
||||||
```bash
|
|
||||||
WIN11_VM_TOOLS_ISO=/custom/path/virtio-win.iso ./run-windows11.py --install-from-iso
|
|
||||||
```
|
|
||||||
|
|
||||||
## Port forwards
|
## Port forwards
|
||||||
|
|
||||||
@@ -217,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
|
||||||
|
|||||||
@@ -39,17 +39,6 @@
|
|||||||
driver = "virtio-blk-pci"
|
driver = "virtio-blk-pci"
|
||||||
drive = "disk0"
|
drive = "disk0"
|
||||||
|
|
||||||
# cloud-init seed ISO.
|
|
||||||
[drive "seed0"]
|
|
||||||
if = "none"
|
|
||||||
format = "raw"
|
|
||||||
readonly = "on"
|
|
||||||
file = "${seed_image}"
|
|
||||||
|
|
||||||
[device "seed0-dev"]
|
|
||||||
driver = "virtio-blk-pci"
|
|
||||||
drive = "seed0"
|
|
||||||
|
|
||||||
# The NIC is attached to the user-mode network "net0", which the launcher
|
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||||
# defines with -netdev from qemu/debian13/forwards.conf because a config file
|
# defines with -netdev from qemu/debian13/forwards.conf because a config file
|
||||||
# section cannot hold more than one hostfwd.
|
# section cannot hold more than one hostfwd.
|
||||||
|
|||||||
@@ -39,17 +39,6 @@
|
|||||||
driver = "virtio-blk-pci"
|
driver = "virtio-blk-pci"
|
||||||
drive = "disk0"
|
drive = "disk0"
|
||||||
|
|
||||||
# cloud-init seed ISO.
|
|
||||||
[drive "seed0"]
|
|
||||||
if = "none"
|
|
||||||
format = "raw"
|
|
||||||
readonly = "on"
|
|
||||||
file = "${seed_image}"
|
|
||||||
|
|
||||||
[device "seed0-dev"]
|
|
||||||
driver = "virtio-blk-pci"
|
|
||||||
drive = "seed0"
|
|
||||||
|
|
||||||
# The NIC is attached to the user-mode network "net0", which the launcher
|
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||||
# defines with -netdev from qemu/fedora44/forwards.conf because a config file
|
# defines with -netdev from qemu/fedora44/forwards.conf because a config file
|
||||||
# section cannot hold more than one hostfwd.
|
# section cannot hold more than one hostfwd.
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT
|
||||||
|
SSH 2226 22
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -39,17 +39,6 @@
|
|||||||
driver = "virtio-blk-pci"
|
driver = "virtio-blk-pci"
|
||||||
drive = "disk0"
|
drive = "disk0"
|
||||||
|
|
||||||
# cloud-init seed ISO.
|
|
||||||
[drive "seed0"]
|
|
||||||
if = "none"
|
|
||||||
format = "raw"
|
|
||||||
readonly = "on"
|
|
||||||
file = "${seed_image}"
|
|
||||||
|
|
||||||
[device "seed0-dev"]
|
|
||||||
driver = "virtio-blk-pci"
|
|
||||||
drive = "seed0"
|
|
||||||
|
|
||||||
# The NIC is attached to the user-mode network "net0", which the launcher
|
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||||
# defines with -netdev from qemu/ubuntu2604/forwards.conf because a config file
|
# defines with -netdev from qemu/ubuntu2604/forwards.conf because a config file
|
||||||
# section cannot hold more than one hostfwd.
|
# section cannot hold more than one hostfwd.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# run-windows11.py fills in the file path placeholders below, writes the result
|
# run-windows11.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
|
# to <state dir>/qemu.ini and passes it to QEMU with -readconfig. Options that
|
||||||
# have no -readconfig equivalent (-cpu, -display) and the installation media
|
# have no -readconfig equivalent (-cpu, -display) and the network are still added
|
||||||
# attached by --install-from-iso are still added by the launcher.
|
# by the launcher.
|
||||||
|
|
||||||
[name]
|
[name]
|
||||||
guest = "windows11"
|
guest = "windows11"
|
||||||
|
|||||||
@@ -7,31 +7,28 @@
|
|||||||
[accel]
|
[accel]
|
||||||
accel = "tcg"
|
accel = "tcg"
|
||||||
|
|
||||||
# run-windows11.py adds kernel-irqchip=off unless installing with
|
# run-windows11.py adds kernel-irqchip=off (see there).
|
||||||
# --install-from-img (see there).
|
|
||||||
[machine]
|
[machine]
|
||||||
type = "virt"
|
type = "virt"
|
||||||
|
|
||||||
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
||||||
# installation media of --install-from-iso) both have inbox Windows drivers.
|
# installation media) both have inbox Windows drivers.
|
||||||
[device "system_disk-dev"]
|
[device "system_disk-dev"]
|
||||||
driver = "nvme"
|
driver = "nvme"
|
||||||
drive = "system_disk"
|
drive = "system_disk"
|
||||||
serial = "win11disk"
|
serial = "win11disk"
|
||||||
bootindex = "1"
|
bootindex = "1"
|
||||||
|
|
||||||
# Windows has no inbox driver for virtio-net; the guest tools installed by
|
# The base image carries the VirtIO drivers (build-windows-base.sh installs them
|
||||||
# Configure-Windows.ps1 provide it.
|
# 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"]
|
[device "nic0"]
|
||||||
driver = "virtio-net-pci"
|
driver = "virtio-net-pci"
|
||||||
netdev = "net0"
|
netdev = "net0"
|
||||||
|
|
||||||
# Unlike q35, "virt" has no default GPU or keyboard. virtio-gpu is the only
|
|
||||||
# display, driven by viogpudo (installed by Configure-Windows.ps1). 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 "gpu0"]
|
[device "gpu0"]
|
||||||
driver = "virtio-gpu-pci"
|
driver = "virtio-gpu-pci"
|
||||||
xres = "2304"
|
xres = "2304"
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
type = "q35"
|
type = "q35"
|
||||||
|
|
||||||
# The system disk is attached through IDE so that Windows can boot before the
|
# The system disk is attached through IDE so that Windows can boot before the
|
||||||
# VirtIO storage driver is installed; the installation media of
|
# VirtIO storage driver is installed.
|
||||||
# --install-from-iso use ide.1 to ide.3.
|
|
||||||
[device "system_disk-dev"]
|
[device "system_disk-dev"]
|
||||||
driver = "ide-hd"
|
driver = "ide-hd"
|
||||||
drive = "system_disk"
|
drive = "system_disk"
|
||||||
|
|||||||
@@ -11,24 +11,23 @@
|
|||||||
type = "virt"
|
type = "virt"
|
||||||
|
|
||||||
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
||||||
# installation media of --install-from-iso) both have inbox Windows drivers.
|
# installation media) both have inbox Windows drivers.
|
||||||
[device "system_disk-dev"]
|
[device "system_disk-dev"]
|
||||||
driver = "nvme"
|
driver = "nvme"
|
||||||
drive = "system_disk"
|
drive = "system_disk"
|
||||||
serial = "win11disk"
|
serial = "win11disk"
|
||||||
bootindex = "1"
|
bootindex = "1"
|
||||||
|
|
||||||
# Windows has no inbox driver for virtio-net; the guest tools installed by
|
# The base image carries the VirtIO drivers (build-windows-base.sh installs them
|
||||||
# Configure-Windows.ps1 provide it.
|
# 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"]
|
[device "nic0"]
|
||||||
driver = "virtio-net-pci"
|
driver = "virtio-net-pci"
|
||||||
netdev = "net0"
|
netdev = "net0"
|
||||||
|
|
||||||
# Unlike q35, "virt" has no default GPU or keyboard. virtio-gpu is the only
|
|
||||||
# display, driven by viogpudo (installed by Configure-Windows.ps1). 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 and 2048x1280 but not 2560x1600.
|
|
||||||
[device "gpu0"]
|
[device "gpu0"]
|
||||||
driver = "virtio-gpu-pci"
|
driver = "virtio-gpu-pci"
|
||||||
xres = "2048"
|
xres = "2048"
|
||||||
|
|||||||
+10
-4
@@ -208,7 +208,8 @@ 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}")
|
||||||
|
|
||||||
if not seed_image.is_file():
|
creating = not disk_image.is_file()
|
||||||
|
if creating and not seed_image.is_file():
|
||||||
seed_builder = script_dir / "seediso" / "almalinux10" / "build-seed.sh"
|
seed_builder = script_dir / "seediso" / "almalinux10" / "build-seed.sh"
|
||||||
if not is_executable(seed_builder):
|
if not is_executable(seed_builder):
|
||||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||||
@@ -240,7 +241,7 @@ if pid_file.is_file():
|
|||||||
die(f"the AlmaLinux virtual machine is already running (PID {old_pid}).")
|
die(f"the AlmaLinux virtual machine is already running (PID {old_pid}).")
|
||||||
pid_file.unlink(missing_ok=True)
|
pid_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
if not disk_image.is_file():
|
if creating:
|
||||||
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
||||||
"-b", str(base_image), str(disk_image), disk_size])
|
"-b", str(base_image), str(disk_image), disk_size])
|
||||||
print(f"Created writable disk overlay: {disk_image}")
|
print(f"Created writable disk overlay: {disk_image}")
|
||||||
@@ -261,12 +262,17 @@ qemu_args = [
|
|||||||
"-drive", f"if=pflash,format=raw,file={uefi_vars}",
|
"-drive", f"if=pflash,format=raw,file={uefi_vars}",
|
||||||
"-drive", f"file={disk_image},if=none,id=disk0,format=qcow2",
|
"-drive", f"file={disk_image},if=none,id=disk0,format=qcow2",
|
||||||
"-device", "virtio-blk-pci,drive=disk0",
|
"-device", "virtio-blk-pci,drive=disk0",
|
||||||
"-drive", f"file={seed_image},if=none,id=seed0,format=raw,readonly=on",
|
|
||||||
"-device", "virtio-blk-pci,drive=seed0",
|
|
||||||
"-netdev", vmforwards.netdev_argument(forwards),
|
"-netdev", vmforwards.netdev_argument(forwards),
|
||||||
"-device", "virtio-net-pci,netdev=net0",
|
"-device", "virtio-net-pci,netdev=net0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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"]
|
||||||
|
|
||||||
if machine_type == "virt":
|
if machine_type == "virt":
|
||||||
# Unlike q35, the "virt" machine has no default GPU or USB input devices.
|
# Unlike q35, the "virt" machine has no default GPU or USB input devices.
|
||||||
qemu_args += [
|
qemu_args += [
|
||||||
|
|||||||
+10
-3
@@ -208,7 +208,8 @@ 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}")
|
||||||
|
|
||||||
if not seed_image.is_file():
|
creating = not disk_image.is_file()
|
||||||
|
if creating and not seed_image.is_file():
|
||||||
seed_builder = script_dir / "seediso" / "debian13" / "build-seed.sh"
|
seed_builder = script_dir / "seediso" / "debian13" / "build-seed.sh"
|
||||||
if not is_executable(seed_builder):
|
if not is_executable(seed_builder):
|
||||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||||
@@ -240,7 +241,7 @@ if pid_file.is_file():
|
|||||||
die(f"the virtual machine is already running (PID {old_pid}).")
|
die(f"the virtual machine is already running (PID {old_pid}).")
|
||||||
pid_file.unlink(missing_ok=True)
|
pid_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
if not disk_image.is_file():
|
if creating:
|
||||||
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
||||||
"-b", str(base_image), str(disk_image), disk_size])
|
"-b", str(base_image), str(disk_image), disk_size])
|
||||||
print(f"Created writable disk overlay: {disk_image}")
|
print(f"Created writable disk overlay: {disk_image}")
|
||||||
@@ -254,7 +255,6 @@ config_values = {
|
|||||||
"uefi_code": uefi_code_flash,
|
"uefi_code": uefi_code_flash,
|
||||||
"uefi_vars": uefi_vars,
|
"uefi_vars": uefi_vars,
|
||||||
"disk_image": disk_image,
|
"disk_image": disk_image,
|
||||||
"seed_image": seed_image,
|
|
||||||
}
|
}
|
||||||
for key, value in config_values.items():
|
for key, value in config_values.items():
|
||||||
# QEMU's config file parser has no escape syntax for quoted values.
|
# QEMU's config file parser has no escape syntax for quoted values.
|
||||||
@@ -285,6 +285,13 @@ qemu_args = [
|
|||||||
"-display", display_backend,
|
"-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 {host_arch} VM from {qemu_config_template} "
|
print(f"Starting {host_arch} VM from {qemu_config_template} "
|
||||||
f"and {qemu_platform_config}.", flush=True)
|
f"and {qemu_platform_config}.", flush=True)
|
||||||
vmforwards.print_forwards(forwards)
|
vmforwards.print_forwards(forwards)
|
||||||
|
|||||||
+10
-3
@@ -210,7 +210,8 @@ 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}")
|
||||||
|
|
||||||
if not seed_image.is_file():
|
creating = not disk_image.is_file()
|
||||||
|
if creating and not seed_image.is_file():
|
||||||
seed_builder = script_dir / "seediso" / "fedora44" / "build-seed.sh"
|
seed_builder = script_dir / "seediso" / "fedora44" / "build-seed.sh"
|
||||||
if not is_executable(seed_builder):
|
if not is_executable(seed_builder):
|
||||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||||
@@ -242,7 +243,7 @@ if pid_file.is_file():
|
|||||||
die(f"the Fedora virtual machine is already running (PID {old_pid}).")
|
die(f"the Fedora virtual machine is already running (PID {old_pid}).")
|
||||||
pid_file.unlink(missing_ok=True)
|
pid_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
if not disk_image.is_file():
|
if creating:
|
||||||
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
||||||
"-b", str(base_image), str(disk_image), disk_size])
|
"-b", str(base_image), str(disk_image), disk_size])
|
||||||
print(f"Created writable disk overlay: {disk_image}")
|
print(f"Created writable disk overlay: {disk_image}")
|
||||||
@@ -256,7 +257,6 @@ config_values = {
|
|||||||
"uefi_code": uefi_code_flash,
|
"uefi_code": uefi_code_flash,
|
||||||
"uefi_vars": uefi_vars,
|
"uefi_vars": uefi_vars,
|
||||||
"disk_image": disk_image,
|
"disk_image": disk_image,
|
||||||
"seed_image": seed_image,
|
|
||||||
}
|
}
|
||||||
for key, value in config_values.items():
|
for key, value in config_values.items():
|
||||||
# QEMU's config file parser has no escape syntax for quoted values.
|
# QEMU's config file parser has no escape syntax for quoted values.
|
||||||
@@ -287,6 +287,13 @@ qemu_args = [
|
|||||||
"-display", display_backend,
|
"-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 Fedora 44 ({host_arch}) from {qemu_config_template} "
|
print(f"Starting Fedora 44 ({host_arch}) from {qemu_config_template} "
|
||||||
f"and {qemu_platform_config}.", flush=True)
|
f"and {qemu_platform_config}.", flush=True)
|
||||||
vmforwards.print_forwards(forwards)
|
vmforwards.print_forwards(forwards)
|
||||||
|
|||||||
Executable
+312
@@ -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])
|
||||||
+10
-3
@@ -210,7 +210,8 @@ 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}")
|
||||||
|
|
||||||
if not seed_image.is_file():
|
creating = not disk_image.is_file()
|
||||||
|
if creating and not seed_image.is_file():
|
||||||
seed_builder = script_dir / "seediso" / "ubuntu2604" / "build-seed.sh"
|
seed_builder = script_dir / "seediso" / "ubuntu2604" / "build-seed.sh"
|
||||||
if not is_executable(seed_builder):
|
if not is_executable(seed_builder):
|
||||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||||
@@ -242,7 +243,7 @@ if pid_file.is_file():
|
|||||||
die(f"the Ubuntu virtual machine is already running (PID {old_pid}).")
|
die(f"the Ubuntu virtual machine is already running (PID {old_pid}).")
|
||||||
pid_file.unlink(missing_ok=True)
|
pid_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
if not disk_image.is_file():
|
if creating:
|
||||||
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
||||||
"-b", str(base_image), str(disk_image), disk_size])
|
"-b", str(base_image), str(disk_image), disk_size])
|
||||||
print(f"Created writable disk overlay: {disk_image}")
|
print(f"Created writable disk overlay: {disk_image}")
|
||||||
@@ -256,7 +257,6 @@ config_values = {
|
|||||||
"uefi_code": uefi_code_flash,
|
"uefi_code": uefi_code_flash,
|
||||||
"uefi_vars": uefi_vars,
|
"uefi_vars": uefi_vars,
|
||||||
"disk_image": disk_image,
|
"disk_image": disk_image,
|
||||||
"seed_image": seed_image,
|
|
||||||
}
|
}
|
||||||
for key, value in config_values.items():
|
for key, value in config_values.items():
|
||||||
# QEMU's config file parser has no escape syntax for quoted values.
|
# QEMU's config file parser has no escape syntax for quoted values.
|
||||||
@@ -287,6 +287,13 @@ qemu_args = [
|
|||||||
"-display", display_backend,
|
"-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 Ubuntu 26.04 ({host_arch}) from {qemu_config_template} "
|
print(f"Starting Ubuntu 26.04 ({host_arch}) from {qemu_config_template} "
|
||||||
f"and {qemu_platform_config}.", flush=True)
|
f"and {qemu_platform_config}.", flush=True)
|
||||||
vmforwards.print_forwards(forwards)
|
vmforwards.print_forwards(forwards)
|
||||||
|
|||||||
+296
-399
@@ -51,16 +51,6 @@ def find_image_downloader():
|
|||||||
return image_downloader
|
return image_downloader
|
||||||
|
|
||||||
|
|
||||||
state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / "vm" / "windows11")
|
|
||||||
disk_size = env("WIN11_VM_DISK_SIZE") or "64G"
|
|
||||||
rdp_port = env("WIN11_VM_RDP_PORT")
|
|
||||||
ssh_port = env("WIN11_VM_SSH_PORT")
|
|
||||||
forwards_file = Path(
|
|
||||||
env("WIN11_VM_FORWARDS") or script_dir / "qemu" / "windows11" / "forwards.conf")
|
|
||||||
qemu_config_template = Path(
|
|
||||||
env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11" / "config.ini")
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_arch(arch):
|
def normalize_arch(arch):
|
||||||
if arch in ("arm64", "aarch64"):
|
if arch in ("arm64", "aarch64"):
|
||||||
return "arm64"
|
return "arm64"
|
||||||
@@ -69,142 +59,6 @@ def normalize_arch(arch):
|
|||||||
die(f"unsupported architecture: {arch}")
|
die(f"unsupported architecture: {arch}")
|
||||||
|
|
||||||
|
|
||||||
daemonize = False
|
|
||||||
install_from_iso = False
|
|
||||||
install_from_img = False
|
|
||||||
download_only = False
|
|
||||||
seed_only = False
|
|
||||||
extra_args = []
|
|
||||||
arch_option = None
|
|
||||||
vnc_listen = None
|
|
||||||
|
|
||||||
|
|
||||||
def usage(stream=sys.stdout):
|
|
||||||
stream.write(f"""\
|
|
||||||
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--seed-only] [--vnc[=ADDR:N]] [--install-from-iso | --install-from-img]
|
|
||||||
[--arch ARCH] [--state-dir DIR] [--rdp-port PORT] [--ssh-port PORT] [-- QEMU_ARGUMENT ...]
|
|
||||||
|
|
||||||
If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from
|
|
||||||
the Windows 11 QCOW2 base image.
|
|
||||||
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
|
|
||||||
architecture, --arch or VM_IMAGE_ARCH). Later QEMU arguments override them, e.g. -- -m 4G -smp 4.
|
|
||||||
Use --install-from-iso to perform an automatic ISO installation instead, or
|
|
||||||
--install-from-img to install from a disk image built from the ISO.
|
|
||||||
If it exists, start Windows normally without attaching installation media.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--daemon Run QEMU in the background
|
|
||||||
--download-only Download the VirtIO tools ISO and the Windows 11 ISO
|
|
||||||
(for --arch) if they are missing, then exit; needs
|
|
||||||
Node.js and Chromium for the latter, see vm-images/README.md
|
|
||||||
(the base QCOW2 is supplied manually)
|
|
||||||
--seed-only Build the seed ISO (answer file and setup scripts) and exit,
|
|
||||||
without starting the VM or fetching the Windows and VirtIO ISOs
|
|
||||||
--install-from-iso Install from ISO when the system disk is missing
|
|
||||||
--install-from-img Like --install-from-iso, but install from the disk image
|
|
||||||
made by vm-images/build-windows-install-disk.sh, which
|
|
||||||
is the ISO path ending in .img instead of .iso.
|
|
||||||
It is attached as an NVMe disk, much faster than the
|
|
||||||
emulated USB CD-ROM, and is never modified
|
|
||||||
--vnc[=ADDR:N] Serve the screen over VNC instead of opening a window, on
|
|
||||||
ADDR (default: 127.0.0.1) at TCP port 5900+N (default N: 0).
|
|
||||||
Overrides WIN11_VM_DISPLAY
|
|
||||||
--arch ARCH Guest architecture: arm64 or amd64 (default: the host's;
|
|
||||||
overrides VM_IMAGE_ARCH). A guest that differs from the
|
|
||||||
host runs under slow TCG emulation
|
|
||||||
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
|
||||||
(default: vm/windows11; overrides WIN11_VM_STATE_DIR)
|
|
||||||
--rdp-port PORT Host port of the RDP rule in the port forward file
|
|
||||||
(default: 3389; overrides WIN11_VM_RDP_PORT)
|
|
||||||
--ssh-port PORT Host port of the SSH rule in the port forward file
|
|
||||||
(default: 2222; overrides WIN11_VM_SSH_PORT)
|
|
||||||
-h, --help Show this help
|
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images)
|
|
||||||
WIN11_VM_INSTALL_ISO Windows 11 installation ISO used with --install-from-iso
|
|
||||||
(--install-from-img uses the same path ending in .img)
|
|
||||||
WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper)
|
|
||||||
WIN11_VM_RDP_PORT Host RDP port (default: from the port forward file, 3389)
|
|
||||||
WIN11_VM_SSH_PORT Host SSH port (default: from the port forward file, 2222)
|
|
||||||
WIN11_VM_FORWARDS Port forward file (default: qemu/windows11/forwards.conf)
|
|
||||||
WIN11_VM_STATE_DIR Runtime directory (default: vm/windows11)
|
|
||||||
WIN11_VM_DISK_IMAGE System disk path
|
|
||||||
WIN11_VM_DISK_SIZE New system disk size (default: 64G)
|
|
||||||
WIN11_VM_SEED_ISO Answer-file ISO path (default: alongside the VM disk)
|
|
||||||
WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset)
|
|
||||||
WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset)
|
|
||||||
WIN11_VM_QEMU_CONFIG QEMU config template passed to -readconfig
|
|
||||||
(default: qemu/windows11/config.ini)
|
|
||||||
WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS,
|
|
||||||
gtk elsewhere)
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
args = sys.argv[1:]
|
|
||||||
while args:
|
|
||||||
arg = args.pop(0)
|
|
||||||
if arg == "--daemon":
|
|
||||||
daemonize = True
|
|
||||||
elif arg == "--download-only":
|
|
||||||
download_only = True
|
|
||||||
elif arg == "--seed-only":
|
|
||||||
seed_only = True
|
|
||||||
elif arg == "--vnc" or arg.startswith("--vnc="):
|
|
||||||
vnc_listen = arg.partition("=")[2] or "127.0.0.1:0"
|
|
||||||
if ":" not in vnc_listen:
|
|
||||||
vnc_listen += ":0"
|
|
||||||
elif arg == "--install-from-iso":
|
|
||||||
install_from_iso = True
|
|
||||||
elif arg == "--install-from-img":
|
|
||||||
install_from_img = 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 ("--rdp-port", "--ssh-port") or arg.startswith(("--rdp-port=", "--ssh-port=")):
|
|
||||||
name, sep, value = arg.partition("=")
|
|
||||||
if not sep:
|
|
||||||
if not args:
|
|
||||||
die(f"{name} requires a port argument.", 2)
|
|
||||||
value = args.pop(0)
|
|
||||||
if not (value.isascii() and value.isdigit() and 1 <= int(value) <= 65535):
|
|
||||||
die(f"{name} must be a port number from 1 to 65535: {value}", 2)
|
|
||||||
if name == "--rdp-port":
|
|
||||||
rdp_port = value
|
|
||||||
else:
|
|
||||||
ssh_port = value
|
|
||||||
elif arg == "--arch":
|
|
||||||
if not args:
|
|
||||||
die(f"{arg} requires an architecture argument.", 2)
|
|
||||||
arch_option = args.pop(0)
|
|
||||||
elif arg.startswith("--arch="):
|
|
||||||
arch_option = 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)
|
|
||||||
|
|
||||||
host_arch = normalize_arch(arch_option or env("VM_IMAGE_ARCH") or platform.machine())
|
|
||||||
# The shared image helper and build-seed.sh pick the architecture from here.
|
|
||||||
os.environ["VM_IMAGE_ARCH"] = host_arch
|
|
||||||
qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
|
|
||||||
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" / "windows11" / f"{host_os}-{host_arch}.ini"
|
|
||||||
|
|
||||||
|
|
||||||
def pick_firmware(override, *candidates):
|
def pick_firmware(override, *candidates):
|
||||||
if override:
|
if override:
|
||||||
return Path(override)
|
return Path(override)
|
||||||
@@ -214,15 +68,148 @@ def pick_firmware(override, *candidates):
|
|||||||
return Path(candidates[0])
|
return Path(candidates[0])
|
||||||
|
|
||||||
|
|
||||||
if host_arch == "arm64":
|
class Options:
|
||||||
uefi_code = pick_firmware(
|
"""Command-line options."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.daemonize = False
|
||||||
|
self.vnc_listen = None
|
||||||
|
self.state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / "vm" / "windows11")
|
||||||
|
self.rdp_port = env("WIN11_VM_RDP_PORT")
|
||||||
|
self.ssh_port = env("WIN11_VM_SSH_PORT")
|
||||||
|
self.arch_option = None
|
||||||
|
self.extra_args = []
|
||||||
|
|
||||||
|
|
||||||
|
def usage(stream=sys.stdout):
|
||||||
|
stream.write(f"""\
|
||||||
|
Usage: {Path(sys.argv[0]).name} [--daemon] [--vnc[=ADDR:N]]
|
||||||
|
[--arch ARCH] [--state-dir DIR] [--rdp-port PORT] [--ssh-port PORT] [-- QEMU_ARGUMENT ...]
|
||||||
|
|
||||||
|
If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it as a
|
||||||
|
QCOW2 overlay on the Windows 11 base image, which starts in seconds and runs the
|
||||||
|
specialize and OOBE passes of its answer file on the first boot, and mount the
|
||||||
|
seed ISO (DIR/seed.iso, built from seediso/windows11 if missing or outdated):
|
||||||
|
Windows reads its Unattend.xml from the root of that CD-ROM during the specialize
|
||||||
|
and OOBE passes: host name, account, auto logon, Remote Desktop, then at the
|
||||||
|
first logon Configure-Windows.ps1 (OpenSSH and the SSH key, settings, installer
|
||||||
|
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 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
|
||||||
|
architecture, --arch or VM_IMAGE_ARCH). Later QEMU arguments override them, e.g. -- -m 4G -smp 4.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--daemon Run QEMU in the background
|
||||||
|
--vnc[=ADDR:N] Serve the screen over VNC instead of opening a window, on
|
||||||
|
ADDR (default: 127.0.0.1) at TCP port 5900+N (default N: 0).
|
||||||
|
Overrides WIN11_VM_DISPLAY
|
||||||
|
--arch ARCH Guest architecture: arm64 or amd64 (default: the host's;
|
||||||
|
overrides VM_IMAGE_ARCH). A guest that differs from the
|
||||||
|
host runs under slow TCG emulation
|
||||||
|
--state-dir DIR Directory for the VM disk and runtime files
|
||||||
|
(default: vm/windows11; overrides WIN11_VM_STATE_DIR)
|
||||||
|
--rdp-port PORT Host port of the RDP rule in the port forward file
|
||||||
|
(default: 3389; overrides WIN11_VM_RDP_PORT)
|
||||||
|
--ssh-port PORT Host port of the SSH rule in the port forward file
|
||||||
|
(default: 2222; overrides WIN11_VM_SSH_PORT)
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images)
|
||||||
|
WIN11_VM_RDP_PORT Host RDP port (default: from the port forward file, 3389)
|
||||||
|
WIN11_VM_SSH_PORT Host SSH port (default: from the port forward file, 2222)
|
||||||
|
WIN11_VM_FORWARDS Port forward file (default: qemu/windows11/forwards.conf)
|
||||||
|
WIN11_VM_STATE_DIR Runtime directory (default: vm/windows11)
|
||||||
|
WIN11_VM_DISK_IMAGE System disk path
|
||||||
|
WIN11_VM_DISK_SIZE New system disk size (default: 64G)
|
||||||
|
WIN11_VM_SEED_ISO Seed ISO path (default: alongside the VM disk)
|
||||||
|
WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset)
|
||||||
|
WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset)
|
||||||
|
WIN11_VM_QEMU_CONFIG QEMU config template passed to -readconfig
|
||||||
|
(default: qemu/windows11/config.ini)
|
||||||
|
WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS,
|
||||||
|
gtk elsewhere)
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(args):
|
||||||
|
"""Parse the command line."""
|
||||||
|
options = Options()
|
||||||
|
args = list(args)
|
||||||
|
while args:
|
||||||
|
arg = args.pop(0)
|
||||||
|
if arg == "--daemon":
|
||||||
|
options.daemonize = True
|
||||||
|
elif arg == "--vnc" or arg.startswith("--vnc="):
|
||||||
|
options.vnc_listen = arg.partition("=")[2] or "127.0.0.1:0"
|
||||||
|
if ":" not in options.vnc_listen:
|
||||||
|
options.vnc_listen += ":0"
|
||||||
|
elif arg == "--state-dir":
|
||||||
|
if not args:
|
||||||
|
die(f"{arg} requires a directory argument.", 2)
|
||||||
|
options.state_dir = Path(args.pop(0))
|
||||||
|
elif arg.startswith("--state-dir="):
|
||||||
|
options.state_dir = Path(arg.split("=", 1)[1])
|
||||||
|
elif arg in ("--rdp-port", "--ssh-port") or arg.startswith(("--rdp-port=", "--ssh-port=")):
|
||||||
|
name, sep, value = arg.partition("=")
|
||||||
|
if not sep:
|
||||||
|
if not args:
|
||||||
|
die(f"{name} requires a port argument.", 2)
|
||||||
|
value = args.pop(0)
|
||||||
|
if not (value.isascii() and value.isdigit() and 1 <= int(value) <= 65535):
|
||||||
|
die(f"{name} must be a port number from 1 to 65535: {value}", 2)
|
||||||
|
if name == "--rdp-port":
|
||||||
|
options.rdp_port = value
|
||||||
|
else:
|
||||||
|
options.ssh_port = value
|
||||||
|
elif arg == "--arch":
|
||||||
|
if not args:
|
||||||
|
die(f"{arg} requires an architecture argument.", 2)
|
||||||
|
options.arch_option = args.pop(0)
|
||||||
|
elif arg.startswith("--arch="):
|
||||||
|
options.arch_option = arg.split("=", 1)[1]
|
||||||
|
elif arg in ("-h", "--help"):
|
||||||
|
usage()
|
||||||
|
sys.exit(0)
|
||||||
|
elif arg == "--":
|
||||||
|
options.extra_args.extend(args)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"Error: unknown option: {arg}", file=sys.stderr)
|
||||||
|
usage(sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
class Platform:
|
||||||
|
"""Guest architecture, host system and the UEFI firmware that fit them."""
|
||||||
|
|
||||||
|
def __init__(self, arch_option):
|
||||||
|
self.arch = normalize_arch(arch_option or env("VM_IMAGE_ARCH") or platform.machine())
|
||||||
|
# The shared image helper and build-seed.sh pick the architecture from here.
|
||||||
|
os.environ["VM_IMAGE_ARCH"] = self.arch
|
||||||
|
self.qemu_binary = "qemu-system-aarch64" if self.arch == "arm64" else "qemu-system-x86_64"
|
||||||
|
self.os = platform.system().lower()
|
||||||
|
if self.os == "darwin" and self.arch == "amd64":
|
||||||
|
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
||||||
|
self.config_template = Path(
|
||||||
|
env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11" / "config.ini")
|
||||||
|
self.platform_config = script_dir / "qemu" / "windows11" / f"{self.os}-{self.arch}.ini"
|
||||||
|
if self.arch == "arm64":
|
||||||
|
self.uefi_code = pick_firmware(
|
||||||
env("WIN11_VM_UEFI_CODE"),
|
env("WIN11_VM_UEFI_CODE"),
|
||||||
"/usr/share/AAVMF/AAVMF_CODE.fd",
|
"/usr/share/AAVMF/AAVMF_CODE.fd",
|
||||||
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
|
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
|
||||||
"/opt/homebrew/share/qemu/edk2-aarch64-code.fd",
|
"/opt/homebrew/share/qemu/edk2-aarch64-code.fd",
|
||||||
"/usr/local/share/qemu/edk2-aarch64-code.fd",
|
"/usr/local/share/qemu/edk2-aarch64-code.fd",
|
||||||
)
|
)
|
||||||
uefi_vars_template = pick_firmware(
|
self.uefi_vars_template = pick_firmware(
|
||||||
env("WIN11_VM_UEFI_VARS"),
|
env("WIN11_VM_UEFI_VARS"),
|
||||||
"/usr/share/AAVMF/AAVMF_VARS.fd",
|
"/usr/share/AAVMF/AAVMF_VARS.fd",
|
||||||
"/usr/share/edk2/aarch64/vars-template-pflash.raw",
|
"/usr/share/edk2/aarch64/vars-template-pflash.raw",
|
||||||
@@ -230,64 +217,147 @@ if host_arch == "arm64":
|
|||||||
"/usr/local/share/qemu/edk2-arm-vars.fd",
|
"/usr/local/share/qemu/edk2-arm-vars.fd",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
uefi_code = pick_firmware(
|
self.uefi_code = pick_firmware(
|
||||||
env("WIN11_VM_UEFI_CODE"),
|
env("WIN11_VM_UEFI_CODE"),
|
||||||
"/usr/share/OVMF/OVMF_CODE_4M.fd",
|
"/usr/share/OVMF/OVMF_CODE_4M.fd",
|
||||||
"/usr/share/edk2/ovmf/OVMF_CODE.fd",
|
"/usr/share/edk2/ovmf/OVMF_CODE.fd",
|
||||||
"/opt/homebrew/share/qemu/edk2-x86_64-code.fd",
|
"/opt/homebrew/share/qemu/edk2-x86_64-code.fd",
|
||||||
"/usr/local/share/qemu/edk2-x86_64-code.fd",
|
"/usr/local/share/qemu/edk2-x86_64-code.fd",
|
||||||
)
|
)
|
||||||
uefi_vars_template = pick_firmware(
|
self.uefi_vars_template = pick_firmware(
|
||||||
env("WIN11_VM_UEFI_VARS"),
|
env("WIN11_VM_UEFI_VARS"),
|
||||||
"/usr/share/OVMF/OVMF_VARS_4M.fd",
|
"/usr/share/OVMF/OVMF_VARS_4M.fd",
|
||||||
"/usr/share/edk2/ovmf/OVMF_VARS.fd",
|
"/usr/share/edk2/ovmf/OVMF_VARS.fd",
|
||||||
"/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",
|
||||||
)
|
)
|
||||||
if install_from_img:
|
|
||||||
install_from_iso = True
|
|
||||||
|
|
||||||
if download_only:
|
|
||||||
# The VirtIO tools ISO and the Windows installation ISO are the media the
|
class Runtime:
|
||||||
# launcher downloads itself; the base QCOW2 is supplied manually.
|
"""Paths of the files a VM keeps in its state directory."""
|
||||||
image_downloader = None
|
|
||||||
if env("WIN11_VM_TOOLS_ISO"):
|
def __init__(self, state_dir):
|
||||||
tools_iso = Path(env("WIN11_VM_TOOLS_ISO"))
|
self.state_dir = state_dir
|
||||||
if not tools_iso.is_file():
|
self.disk_image = Path(env("WIN11_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
|
||||||
die(f"VirtIO Windows tools ISO was not found: {tools_iso}")
|
self.uefi_vars = state_dir / "OVMF_VARS_4M.fd"
|
||||||
|
self.pid_file = state_dir / "qemu.pid"
|
||||||
|
self.log_file = state_dir / "console.log"
|
||||||
|
self.monitor_socket = state_dir / "qemu-monitor.sock"
|
||||||
|
self.guest_agent_socket = state_dir / "qemu-guest-agent.sock"
|
||||||
|
self.qemu_config = state_dir / "qemu.ini"
|
||||||
|
|
||||||
|
|
||||||
|
def check_tools_and_firmware(plat, need_qemu_img=True):
|
||||||
|
if shutil.which(plat.qemu_binary) is None:
|
||||||
|
die(f"{plat.qemu_binary} is not installed.")
|
||||||
|
if need_qemu_img and shutil.which("qemu-img") is None:
|
||||||
|
die("qemu-img is not installed.")
|
||||||
|
for firmware_file in (plat.uefi_code, plat.uefi_vars_template):
|
||||||
|
if not firmware_file.is_file():
|
||||||
|
die(f"required UEFI firmware was not found: {firmware_file}")
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_runtime(plat, rt):
|
||||||
|
"""Create the state directory and clear the leftovers of an earlier run."""
|
||||||
|
rt.state_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
if rt.pid_file.is_file():
|
||||||
|
old_pid = rt.pid_file.read_text().strip()
|
||||||
|
if old_pid.isdigit():
|
||||||
|
try:
|
||||||
|
os.kill(int(old_pid), 0)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
image_downloader = find_image_downloader()
|
die(f"Windows 11 VM is already running (PID {old_pid}).")
|
||||||
tools_iso = Path(capture([str(image_downloader), "--print-path", "virtio-win"]))
|
rt.pid_file.unlink(missing_ok=True)
|
||||||
if not tools_iso.is_file():
|
rt.monitor_socket.unlink(missing_ok=True)
|
||||||
run([str(image_downloader), "virtio-win"])
|
rt.guest_agent_socket.unlink(missing_ok=True)
|
||||||
if not tools_iso.is_file():
|
if not rt.uefi_vars.is_file():
|
||||||
die(f"downloader did not create: {tools_iso}")
|
shutil.copyfile(plat.uefi_vars_template, rt.uefi_vars)
|
||||||
print(f"VirtIO Windows tools ISO is ready: {tools_iso}")
|
|
||||||
if env("WIN11_VM_INSTALL_ISO"):
|
|
||||||
install_iso = Path(env("WIN11_VM_INSTALL_ISO"))
|
def display_backend(vnc_listen):
|
||||||
if not install_iso.is_file():
|
"""cocoa shows the guest at its own resolution. Append ,zoom-to-fit=on (via
|
||||||
die(f"Windows 11 installation ISO was not found: {install_iso}")
|
WIN11_VM_DISPLAY) to stretch a low-resolution screen to the window."""
|
||||||
|
backend = (f"vnc={vnc_listen}" if vnc_listen else env("WIN11_VM_DISPLAY") or (
|
||||||
|
"cocoa" if platform.system() == "Darwin" else "gtk"))
|
||||||
|
if vnc_listen:
|
||||||
|
vnc_host, _, vnc_display = vnc_listen.rpartition(":")
|
||||||
|
if not vnc_display.isdigit():
|
||||||
|
die(f"--vnc display number must be an integer: {vnc_listen}", 2)
|
||||||
|
print(f"VNC: connect to {vnc_host or '127.0.0.1'}:{5900 + int(vnc_display)}")
|
||||||
|
return backend
|
||||||
|
|
||||||
|
|
||||||
|
def common_qemu_args(plat, rt, options):
|
||||||
|
"""Render the config template and return the QEMU arguments shared by both
|
||||||
|
launchers, plus the port forwards (for the start-up message).
|
||||||
|
|
||||||
|
Most of the VM definition lives in the QEMU config template; the file
|
||||||
|
paths are filled in and the rendered file is handed to QEMU with -readconfig.
|
||||||
|
"""
|
||||||
|
config_values = {
|
||||||
|
"uefi_code": plat.uefi_code,
|
||||||
|
"uefi_vars": rt.uefi_vars,
|
||||||
|
"disk_image": rt.disk_image,
|
||||||
|
"guest_agent_socket": rt.guest_agent_socket,
|
||||||
|
"monitor_socket": rt.monitor_socket,
|
||||||
|
}
|
||||||
|
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 (plat.config_template, plat.platform_config):
|
||||||
|
if not config_file.is_file():
|
||||||
|
die(f"QEMU config file was not found: {config_file}")
|
||||||
|
try:
|
||||||
|
config_text = string.Template(plat.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 {plat.config_template}: {error}")
|
||||||
|
rt.qemu_config.write_text(config_text)
|
||||||
|
|
||||||
|
# Each rule of the port forward file is one hostfwd entry of the network.
|
||||||
|
forwards_file = Path(
|
||||||
|
env("WIN11_VM_FORWARDS") or script_dir / "qemu" / "windows11" / "forwards.conf")
|
||||||
|
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(
|
||||||
|
forwards_file,
|
||||||
|
{label: port for label, port in (("RDP", options.rdp_port), ("SSH", options.ssh_port))
|
||||||
|
if port}))
|
||||||
|
|
||||||
|
qemu_args = [
|
||||||
|
"-readconfig", str(rt.qemu_config),
|
||||||
|
"-readconfig", str(plat.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(options.vnc_listen),
|
||||||
|
]
|
||||||
|
return qemu_args, forwards
|
||||||
|
|
||||||
|
|
||||||
|
def start_qemu(plat, rt, qemu_args, extra_args, forwards, daemonize, banner):
|
||||||
|
print(banner)
|
||||||
|
print("Account: guofang / password; " + "; ".join(
|
||||||
|
f"{forward.label}: 0.0.0.0:{forward.host_port}" for forward in forwards),
|
||||||
|
flush=True)
|
||||||
|
if daemonize:
|
||||||
|
rt.log_file.write_text("")
|
||||||
|
qemu_args += ["-serial", f"file:{rt.log_file}", "-daemonize", "-pidfile", str(rt.pid_file)]
|
||||||
|
run([plat.qemu_binary, *qemu_args, *extra_args])
|
||||||
|
print(f"VM started (PID {rt.pid_file.read_text().strip()}); console log: {rt.log_file}")
|
||||||
else:
|
else:
|
||||||
image_downloader = image_downloader or find_image_downloader()
|
os.execvp(plat.qemu_binary, [plat.qemu_binary, *qemu_args, *extra_args])
|
||||||
run([str(image_downloader), "windows11"])
|
|
||||||
install_iso = Path(capture([str(image_downloader), "--print-path", "windows11", "--iso"]))
|
|
||||||
print(f"Windows 11 installation ISO is ready: {install_iso}")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
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"
|
|
||||||
qemu_config = state_dir / "qemu.ini"
|
|
||||||
|
|
||||||
|
|
||||||
def build_seed_iso(image_downloader, force=False):
|
def build_seed_iso(seed_iso):
|
||||||
"""Build the seed ISO, unless it is newer than all of its sources and not forced."""
|
"""Build the seed ISO, unless it is newer than all of its sources."""
|
||||||
seed_dir = script_dir / "seediso" / "windows11"
|
seed_dir = script_dir / "seediso" / "windows11"
|
||||||
seed_sources = [seed_dir / "Autounattend.xml", seed_dir / "Configure-Windows.ps1",
|
seed_sources = [seed_dir / "Unattend.xml", seed_dir / "Configure-Windows.ps1",
|
||||||
*sorted(seed_dir.glob("[0-9][0-9]-*.bat")),
|
*sorted(seed_dir.glob("[0-9][0-9]-*.bat")),
|
||||||
*sorted(seed_dir.glob("[0-9][0-9]-*.ps1"))]
|
*sorted(seed_dir.glob("[0-9][0-9]-*.ps1"))]
|
||||||
|
|
||||||
@@ -300,222 +370,49 @@ def build_seed_iso(image_downloader, force=False):
|
|||||||
|
|
||||||
# The seed carries the OpenSSH zip for this architecture; build-seed.sh
|
# The seed carries the OpenSSH zip for this architecture; build-seed.sh
|
||||||
# downloads it when missing.
|
# downloads it when missing.
|
||||||
seed_sources.append(Path(capture(
|
openssh_zip = Path(capture(
|
||||||
[str(image_downloader), "--print-path", "openssh-win"])))
|
[str(find_image_downloader()), "--print-path", "openssh-win"]))
|
||||||
|
seed_sources.append(openssh_zip)
|
||||||
|
|
||||||
def newer_than_seed(source):
|
if not seed_iso.is_file() or not openssh_zip.is_file() or any(
|
||||||
# Mirrors bash `-nt`: true if source exists and is newer than seed_iso.
|
source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime
|
||||||
return source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime
|
for source in seed_sources):
|
||||||
|
run([str(seed_dir / "build-seed.sh"), str(seed_iso), plat.arch])
|
||||||
if force or not seed_iso.is_file() or not seed_sources[-1].is_file() \
|
|
||||||
or any(newer_than_seed(s) for s in seed_sources):
|
|
||||||
run([str(script_dir / "seediso" / "windows11" / "build-seed.sh"), str(seed_iso), host_arch])
|
|
||||||
|
|
||||||
|
|
||||||
if seed_only:
|
options = parse_args(sys.argv[1:])
|
||||||
build_seed_iso(find_image_downloader(), force=True)
|
plat = Platform(options.arch_option)
|
||||||
print(f"Seed ISO is ready: {seed_iso}")
|
rt = Runtime(options.state_dir)
|
||||||
sys.exit(0)
|
disk_size = env("WIN11_VM_DISK_SIZE") or "64G"
|
||||||
|
|
||||||
installing = False
|
creating = not rt.disk_image.is_file()
|
||||||
installing_from_iso = False
|
if creating:
|
||||||
if not disk_image.is_file():
|
|
||||||
installing = True
|
|
||||||
installing_from_iso = install_from_iso
|
|
||||||
image_downloader = find_image_downloader()
|
|
||||||
if install_from_iso:
|
|
||||||
install_iso = Path(
|
|
||||||
env("WIN11_VM_INSTALL_ISO")
|
|
||||||
or capture([str(image_downloader), "--print-path", "windows11", "--iso"]))
|
|
||||||
if install_from_img:
|
|
||||||
install_iso = install_iso.with_suffix(".img")
|
|
||||||
uses_shared_tools_iso = False
|
|
||||||
if env("WIN11_VM_TOOLS_ISO"):
|
|
||||||
tools_iso = Path(env("WIN11_VM_TOOLS_ISO"))
|
|
||||||
else:
|
|
||||||
tools_iso = Path(capture([str(image_downloader), "--print-path", "virtio-win"]))
|
|
||||||
uses_shared_tools_iso = True
|
|
||||||
else:
|
|
||||||
base_image = Path(env("WIN11_VM_BASE_IMAGE")
|
base_image = Path(env("WIN11_VM_BASE_IMAGE")
|
||||||
or capture([str(image_downloader), "--print-path", "windows11"]))
|
or capture([str(find_image_downloader()), "--print-path", "windows11"]))
|
||||||
|
if not base_image.is_file():
|
||||||
|
die(f"Windows 11 base QCOW2 image was not found: {base_image}\n"
|
||||||
|
"Make it once with vm-images/build-windows-base.sh")
|
||||||
|
|
||||||
if shutil.which(qemu_binary) is None:
|
check_tools_and_firmware(plat, need_qemu_img=creating)
|
||||||
die(f"{qemu_binary} is not installed.")
|
prepare_runtime(plat, rt)
|
||||||
if installing:
|
if creating:
|
||||||
if shutil.which("qemu-img") is None:
|
seed_iso = Path(env("WIN11_VM_SEED_ISO") or rt.disk_image.parent / "seed.iso")
|
||||||
die("qemu-img is not installed.")
|
build_seed_iso(seed_iso)
|
||||||
if installing_from_iso:
|
if creating:
|
||||||
if not install_iso.is_file():
|
|
||||||
if install_from_img:
|
|
||||||
die(f"Windows 11 installation disk image was not found: {install_iso}\n"
|
|
||||||
"Build it with vm-images/build-windows-install-disk.sh ISO")
|
|
||||||
die(f"Windows 11 ISO was not found: {install_iso}")
|
|
||||||
elif not base_image.is_file():
|
|
||||||
die(f"Windows 11 base QCOW2 image was not found: {base_image}")
|
|
||||||
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}")
|
|
||||||
|
|
||||||
state_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
if installing_from_iso:
|
|
||||||
if not tools_iso.is_file():
|
|
||||||
if uses_shared_tools_iso:
|
|
||||||
run([str(image_downloader), "virtio-win"])
|
|
||||||
else:
|
|
||||||
die(f"VirtIO Windows tools ISO was not found: {tools_iso}")
|
|
||||||
if not tools_iso.is_file():
|
|
||||||
die(f"downloader did not create: {tools_iso}")
|
|
||||||
|
|
||||||
build_seed_iso(image_downloader)
|
|
||||||
|
|
||||||
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"Windows 11 VM is already running (PID {old_pid}).")
|
|
||||||
pid_file.unlink(missing_ok=True)
|
|
||||||
monitor_socket.unlink(missing_ok=True)
|
|
||||||
guest_agent_socket.unlink(missing_ok=True)
|
|
||||||
if not uefi_vars.is_file():
|
|
||||||
shutil.copyfile(uefi_vars_template, uefi_vars)
|
|
||||||
if installing:
|
|
||||||
if installing_from_iso:
|
|
||||||
run(["qemu-img", "create", "-q", "-f", "qcow2", str(disk_image), disk_size])
|
|
||||||
else:
|
|
||||||
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
||||||
"-b", str(base_image), str(disk_image), disk_size])
|
"-b", str(base_image), str(rt.disk_image), disk_size])
|
||||||
print(f"Created Windows 11 system disk: {disk_image}")
|
print(f"Created Windows 11 system disk: {rt.disk_image}")
|
||||||
|
|
||||||
# cocoa shows the guest at its own resolution. Append ,zoom-to-fit=on (via
|
qemu_args, forwards = common_qemu_args(plat, rt, options)
|
||||||
# WIN11_VM_DISPLAY) to stretch a low-resolution screen to the window.
|
if plat.os == "darwin" and plat.arch == "arm64":
|
||||||
display_backend = (f"vnc={vnc_listen}" if vnc_listen else env("WIN11_VM_DISPLAY") or (
|
# The HVF in-kernel GICv3 stalls Windows on its USB and NVMe devices
|
||||||
"cocoa" if platform.system() == "Darwin" else "gtk"))
|
# (utmapp/UTM#7970), so use the emulated interrupt controller.
|
||||||
if vnc_listen:
|
|
||||||
vnc_host, _, vnc_display = vnc_listen.rpartition(":")
|
|
||||||
if not vnc_display.isdigit():
|
|
||||||
die(f"--vnc display number must be an integer: {vnc_listen}", 2)
|
|
||||||
print(f"VNC: connect to {vnc_host or '127.0.0.1'}:{5900 + int(vnc_display)}")
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
"uefi_vars": uefi_vars,
|
|
||||||
"disk_image": disk_image,
|
|
||||||
"guest_agent_socket": guest_agent_socket,
|
|
||||||
"monitor_socket": monitor_socket,
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Each rule of the port forward file is one hostfwd entry of the network.
|
|
||||||
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(
|
|
||||||
forwards_file, {label: port for label, port in (("RDP", rdp_port), ("SSH", ssh_port)) if port}))
|
|
||||||
|
|
||||||
# Neither Windows Setup nor the base image has the viogpudo driver yet, and
|
|
||||||
# without it the virtio-gpu in the config file shows "display output is not
|
|
||||||
# active". ramfb shows the screen until Configure-Windows.ps1 installs it. It
|
|
||||||
# must be created before the virtio-gpu: the display window shows the first GPU.
|
|
||||||
qemu_args = ["-device", "ramfb"] if installing and host_arch == "arm64" else []
|
|
||||||
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,
|
|
||||||
# "-display", "none", "-spice", "port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off", "-device", "virtio-vga",
|
|
||||||
]
|
|
||||||
if host_os == "darwin" and host_arch == "arm64" and not install_from_img:
|
|
||||||
# With the HVF in-kernel GICv3, QEMU's usb-storage races on the status
|
|
||||||
# packet and Windows Setup stalls 10 s after every burst of reads
|
|
||||||
# (utmapp/UTM#7970), so use the emulated interrupt controller; upstream
|
|
||||||
# QEMU only gets the fix from the UTM fork. The NVMe install disk of
|
|
||||||
# --install-from-img does not use usb-storage, and hangs with the emulated
|
|
||||||
# controller.
|
|
||||||
qemu_args += ["-machine", "kernel-irqchip=off"]
|
qemu_args += ["-machine", "kernel-irqchip=off"]
|
||||||
if installing_from_iso:
|
if creating:
|
||||||
# q35 has IDE for the installation media; "virt" does not, so use USB.
|
# q35 has IDE (ide.0 is the system disk); "virt" does not, so use USB.
|
||||||
for media_id, media_file, boot_index, ide_bus in (
|
qemu_args += ["-drive", f"file={seed_iso},if=none,id=seed_media,media=cdrom,readonly=on",
|
||||||
("install_media", install_iso, 2, "ide.1"),
|
"-device", "ide-cd,drive=seed_media,bus=ide.1" if plat.arch == "amd64"
|
||||||
("answer_media", seed_iso, 3, "ide.2"),
|
else "usb-storage,drive=seed_media"]
|
||||||
("tools_media", tools_iso, 4, "ide.3")):
|
start_qemu(plat, rt, qemu_args, options.extra_args, forwards, options.daemonize,
|
||||||
device = (f"ide-cd,drive={media_id},bus={ide_bus},bootindex={boot_index}"
|
"Starting Windows 11 from the QCOW2 base image." if creating
|
||||||
if host_arch == "amd64"
|
else "Starting Windows 11 normally.")
|
||||||
else f"usb-storage,drive={media_id},bootindex={boot_index}")
|
|
||||||
if media_id == "install_media" and install_from_img:
|
|
||||||
# snapshot=on: NVMe cannot report write protection, and Windows
|
|
||||||
# writes to the volume it mounts, so a read-only drive makes it fail.
|
|
||||||
# The writes go to a temporary overlay and the image stays unchanged.
|
|
||||||
qemu_args += [
|
|
||||||
"-drive", f"file={media_file},if=none,id={media_id},format=raw,snapshot=on",
|
|
||||||
"-device", f"nvme,drive={media_id},serial=installer,bootindex={boot_index}",
|
|
||||||
]
|
|
||||||
continue
|
|
||||||
qemu_args += [
|
|
||||||
"-drive", f"file={media_file},if=none,id={media_id},media=cdrom,readonly=on",
|
|
||||||
"-device", device,
|
|
||||||
]
|
|
||||||
|
|
||||||
if installing_from_iso:
|
|
||||||
print("Starting Windows 11 automatic installation.")
|
|
||||||
elif installing:
|
|
||||||
print("Starting Windows 11 from the QCOW2 base image.")
|
|
||||||
else:
|
|
||||||
print("Starting Windows 11 normally.")
|
|
||||||
print("Account: guofang / password; " + "; ".join(
|
|
||||||
f"{forward.label}: 0.0.0.0:{forward.host_port}" for forward in forwards),
|
|
||||||
flush=True)
|
|
||||||
|
|
||||||
if installing_from_iso and not install_from_img:
|
|
||||||
# The Windows ISO boots through bootmgr, which waits for a key ("Press any
|
|
||||||
# key to boot from CD or DVD...") and gives up after a few seconds. Nobody
|
|
||||||
# is there to press it, so a detached helper taps the space bar through the
|
|
||||||
# QEMU monitor for the first 30 s. It exits by itself; later key presses
|
|
||||||
# are harmless because Setup is unattended.
|
|
||||||
key_script = (
|
|
||||||
"import socket, sys, time\n"
|
|
||||||
"path = sys.argv[1]\n"
|
|
||||||
"end = time.time() + 30\n"
|
|
||||||
"while time.time() < end:\n"
|
|
||||||
" try:\n"
|
|
||||||
" s = socket.socket(socket.AF_UNIX)\n"
|
|
||||||
" s.connect(path)\n"
|
|
||||||
" break\n"
|
|
||||||
" except OSError:\n"
|
|
||||||
" time.sleep(0.5)\n"
|
|
||||||
"else:\n"
|
|
||||||
" sys.exit(0)\n"
|
|
||||||
"while time.time() < end:\n"
|
|
||||||
" s.sendall(b'sendkey spc\\n')\n"
|
|
||||||
" time.sleep(1)\n")
|
|
||||||
subprocess.Popen([sys.executable, "-c", key_script, str(monitor_socket)],
|
|
||||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
|
||||||
|
|
||||||
if daemonize:
|
|
||||||
log_file.write_text("")
|
|
||||||
qemu_args += ["-serial", f"file:{log_file}", "-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])
|
|
||||||
@@ -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/
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
seed.iso
|
||||||
|
*.iso
|
||||||
@@ -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.
|
||||||
Executable
+62
@@ -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}"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
instance-id: rocky9-host-001
|
||||||
|
local-hostname: rocky9-host
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
ethernets:
|
||||||
|
all-en:
|
||||||
|
match:
|
||||||
|
name: "en*"
|
||||||
|
dhcp4: true
|
||||||
|
all-eth:
|
||||||
|
match:
|
||||||
|
name: "eth*"
|
||||||
|
dhcp4: true
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
|
||||||
<settings pass="windowsPE">
|
|
||||||
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64"
|
|
||||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
|
||||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
|
||||||
<SetupUILanguage>
|
|
||||||
<UILanguage>en-US</UILanguage>
|
|
||||||
</SetupUILanguage>
|
|
||||||
<InputLocale>en-US</InputLocale>
|
|
||||||
<SystemLocale>en-US</SystemLocale>
|
|
||||||
<UILanguage>en-US</UILanguage>
|
|
||||||
<UserLocale>en-US</UserLocale>
|
|
||||||
</component>
|
|
||||||
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64"
|
|
||||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
|
||||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
|
||||||
<DynamicUpdate>
|
|
||||||
<Enable>false</Enable>
|
|
||||||
<WillShowUI>Never</WillShowUI>
|
|
||||||
</DynamicUpdate>
|
|
||||||
<DiskConfiguration>
|
|
||||||
<Disk wcm:action="add">
|
|
||||||
<DiskID>0</DiskID>
|
|
||||||
<WillWipeDisk>true</WillWipeDisk>
|
|
||||||
<CreatePartitions>
|
|
||||||
<CreatePartition wcm:action="add">
|
|
||||||
<Order>1</Order>
|
|
||||||
<Type>EFI</Type>
|
|
||||||
<Size>100</Size>
|
|
||||||
</CreatePartition>
|
|
||||||
<CreatePartition wcm:action="add">
|
|
||||||
<Order>2</Order>
|
|
||||||
<Type>MSR</Type>
|
|
||||||
<Size>16</Size>
|
|
||||||
</CreatePartition>
|
|
||||||
<CreatePartition wcm:action="add">
|
|
||||||
<Order>3</Order>
|
|
||||||
<Type>Primary</Type>
|
|
||||||
<Extend>true</Extend>
|
|
||||||
</CreatePartition>
|
|
||||||
</CreatePartitions>
|
|
||||||
<ModifyPartitions>
|
|
||||||
<ModifyPartition wcm:action="add">
|
|
||||||
<Order>1</Order>
|
|
||||||
<PartitionID>1</PartitionID>
|
|
||||||
<Format>FAT32</Format>
|
|
||||||
<Label>System</Label>
|
|
||||||
</ModifyPartition>
|
|
||||||
<ModifyPartition wcm:action="add">
|
|
||||||
<Order>2</Order>
|
|
||||||
<PartitionID>3</PartitionID>
|
|
||||||
<Format>NTFS</Format>
|
|
||||||
<Label>Windows</Label>
|
|
||||||
<Letter>C</Letter>
|
|
||||||
</ModifyPartition>
|
|
||||||
</ModifyPartitions>
|
|
||||||
</Disk>
|
|
||||||
<WillShowUI>OnError</WillShowUI>
|
|
||||||
</DiskConfiguration>
|
|
||||||
<ImageInstall>
|
|
||||||
<OSImage>
|
|
||||||
<InstallFrom>
|
|
||||||
<MetaData wcm:action="add">
|
|
||||||
<Key>/IMAGE/INDEX</Key>
|
|
||||||
<Value>6</Value>
|
|
||||||
</MetaData>
|
|
||||||
</InstallFrom>
|
|
||||||
<InstallTo>
|
|
||||||
<DiskID>0</DiskID>
|
|
||||||
<PartitionID>3</PartitionID>
|
|
||||||
</InstallTo>
|
|
||||||
<WillShowUI>OnError</WillShowUI>
|
|
||||||
</OSImage>
|
|
||||||
</ImageInstall>
|
|
||||||
<UserData>
|
|
||||||
<AcceptEula>true</AcceptEula>
|
|
||||||
<ProductKey>
|
|
||||||
<Key>W269N-WFGWX-YVC9B-4J6C9-T83GX</Key>
|
|
||||||
<WillShowUI>Never</WillShowUI>
|
|
||||||
</ProductKey>
|
|
||||||
</UserData>
|
|
||||||
<RunSynchronous>
|
|
||||||
<RunSynchronousCommand wcm:action="add">
|
|
||||||
<Order>1</Order>
|
|
||||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
|
|
||||||
</RunSynchronousCommand>
|
|
||||||
<RunSynchronousCommand wcm:action="add">
|
|
||||||
<Order>2</Order>
|
|
||||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
|
|
||||||
</RunSynchronousCommand>
|
|
||||||
<RunSynchronousCommand wcm:action="add">
|
|
||||||
<Order>3</Order>
|
|
||||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
|
|
||||||
</RunSynchronousCommand>
|
|
||||||
</RunSynchronous>
|
|
||||||
</component>
|
|
||||||
</settings>
|
|
||||||
<settings pass="specialize">
|
|
||||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
|
|
||||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
||||||
<ComputerName>WIN11</ComputerName>
|
|
||||||
<TimeZone>China Standard Time</TimeZone>
|
|
||||||
</component>
|
|
||||||
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager"
|
|
||||||
processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral"
|
|
||||||
versionScope="nonSxS">
|
|
||||||
<fDenyTSConnections>false</fDenyTSConnections>
|
|
||||||
</component>
|
|
||||||
<component name="Networking-MPSSVC-Svc" processorArchitecture="amd64"
|
|
||||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
||||||
<FirewallGroups>
|
|
||||||
<FirewallGroup xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
|
||||||
wcm:action="add" wcm:keyValue="RemoteDesktop">
|
|
||||||
<Active>true</Active>
|
|
||||||
<Group>Remote Desktop</Group>
|
|
||||||
<Profile>all</Profile>
|
|
||||||
</FirewallGroup>
|
|
||||||
</FirewallGroups>
|
|
||||||
</component>
|
|
||||||
</settings>
|
|
||||||
<settings pass="oobeSystem">
|
|
||||||
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64"
|
|
||||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
||||||
<InputLocale>en-US</InputLocale>
|
|
||||||
<SystemLocale>en-US</SystemLocale>
|
|
||||||
<UILanguage>en-US</UILanguage>
|
|
||||||
<UserLocale>en-US</UserLocale>
|
|
||||||
</component>
|
|
||||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
|
|
||||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
|
||||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
|
||||||
<OOBE>
|
|
||||||
<HideEULAPage>true</HideEULAPage>
|
|
||||||
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
|
||||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
|
||||||
<ProtectYourPC>3</ProtectYourPC>
|
|
||||||
</OOBE>
|
|
||||||
<UserAccounts>
|
|
||||||
<LocalAccounts>
|
|
||||||
<LocalAccount wcm:action="add">
|
|
||||||
<Name>guofang</Name>
|
|
||||||
<Group>Administrators</Group>
|
|
||||||
<DisplayName>Guofang</DisplayName>
|
|
||||||
<Password>
|
|
||||||
<Value>wang</Value>
|
|
||||||
<PlainText>true</PlainText>
|
|
||||||
</Password>
|
|
||||||
</LocalAccount>
|
|
||||||
</LocalAccounts>
|
|
||||||
</UserAccounts>
|
|
||||||
<AutoLogon>
|
|
||||||
<Enabled>true</Enabled>
|
|
||||||
<LogonCount>1</LogonCount>
|
|
||||||
<Username>guofang</Username>
|
|
||||||
<Password>
|
|
||||||
<Value>wang</Value>
|
|
||||||
<PlainText>true</PlainText>
|
|
||||||
</Password>
|
|
||||||
</AutoLogon>
|
|
||||||
<FirstLogonCommands>
|
|
||||||
<SynchronousCommand wcm:action="add">
|
|
||||||
<Order>1</Order>
|
|
||||||
<Description>Configure Windows from the answer-media script</Description>
|
|
||||||
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -InstallVirtIO -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -InstallOpenSSH -NoPause"</CommandLine>
|
|
||||||
<RequiresUserInput>false</RequiresUserInput>
|
|
||||||
</SynchronousCommand>
|
|
||||||
</FirstLogonCommands>
|
|
||||||
</component>
|
|
||||||
</settings>
|
|
||||||
</unattend>
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
param(
|
param(
|
||||||
[switch]$InstallVirtIO,
|
|
||||||
[switch]$EnableAutomaticLogon,
|
[switch]$EnableAutomaticLogon,
|
||||||
[switch]$DisableAutoUpdate,
|
[switch]$DisableAutoUpdate,
|
||||||
[switch]$CopyInstallers,
|
[switch]$CopyInstallers,
|
||||||
@@ -35,72 +34,6 @@ if (-not (Test-Administrator)) {
|
|||||||
exit $elevated.ExitCode
|
exit $elevated.ExitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
# The guest tools installer is an x86 program and the VirtIO ISO has no Arm64
|
|
||||||
# package for it, so on Arm64 Windows the Arm64 drivers are installed from their
|
|
||||||
# folders on the ISO instead. NetKVM is the network driver; without it the guest
|
|
||||||
# has no network.
|
|
||||||
function Install-VirtIOArm64Drivers {
|
|
||||||
$roots = (Get-PSDrive -PSProvider FileSystem).Root
|
|
||||||
$drivers = @(
|
|
||||||
@{ Folder = 'NetKVM'; Inf = 'netkvm.inf' },
|
|
||||||
@{ Folder = 'vioserial'; Inf = 'vioser.inf' },
|
|
||||||
@{ Folder = 'Balloon'; Inf = 'balloon.inf' },
|
|
||||||
@{ Folder = 'viorng'; Inf = 'viorng.inf' },
|
|
||||||
@{ Folder = 'vioinput'; Inf = 'vioinput.inf' },
|
|
||||||
@{ Folder = 'viogpudo'; Inf = 'viogpudo.inf' }
|
|
||||||
)
|
|
||||||
|
|
||||||
$installed = 0
|
|
||||||
foreach ($driver in $drivers) {
|
|
||||||
$inf = $roots |
|
|
||||||
ForEach-Object { Join-Path $_ "$($driver.Folder)\w11\ARM64\$($driver.Inf)" } |
|
|
||||||
Where-Object { Test-Path -LiteralPath $_ } |
|
|
||||||
Select-Object -First 1
|
|
||||||
if (-not $inf) {
|
|
||||||
if ($driver.Folder -eq 'NetKVM') {
|
|
||||||
throw 'The Arm64 NetKVM driver was not found on the attached media.'
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
& pnputil.exe /add-driver $inf /install | Out-Host
|
|
||||||
# 3010 asks for a restart; 259 means that no device uses the driver.
|
|
||||||
if ($LASTEXITCODE -notin 0, 259, 3010) {
|
|
||||||
throw "pnputil failed to install $inf (exit code $LASTEXITCODE)."
|
|
||||||
}
|
|
||||||
$installed++
|
|
||||||
}
|
|
||||||
Write-Host "Installed $installed VirtIO Arm64 driver packages."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Install-VirtIOGuestTools {
|
|
||||||
if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') {
|
|
||||||
Install-VirtIOArm64Drivers
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$installer = Get-ChildItem `
|
|
||||||
-Path (Get-PSDrive -PSProvider FileSystem).Root `
|
|
||||||
-Filter 'virtio-win-guest-tools.exe' `
|
|
||||||
-File `
|
|
||||||
-ErrorAction SilentlyContinue |
|
|
||||||
Select-Object -First 1
|
|
||||||
|
|
||||||
if (-not $installer) {
|
|
||||||
throw 'virtio-win-guest-tools.exe was not found on the attached media.'
|
|
||||||
}
|
|
||||||
|
|
||||||
$process = Start-Process `
|
|
||||||
-FilePath $installer.FullName `
|
|
||||||
-ArgumentList '/quiet', '/norestart' `
|
|
||||||
-Wait `
|
|
||||||
-PassThru
|
|
||||||
|
|
||||||
if ($process.ExitCode -notin 0, 3010) {
|
|
||||||
throw "VirtIO guest tools installer exited with code $($process.ExitCode)."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Enable-AutomaticLogon {
|
function Enable-AutomaticLogon {
|
||||||
$winlogonPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
$winlogonPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||||
|
|
||||||
@@ -205,9 +138,6 @@ function Install-OpenSSHServer {
|
|||||||
|
|
||||||
$exitCode = 0
|
$exitCode = 0
|
||||||
try {
|
try {
|
||||||
if ($InstallVirtIO) {
|
|
||||||
Install-VirtIOGuestTools
|
|
||||||
}
|
|
||||||
if ($EnableAutomaticLogon) {
|
if ($EnableAutomaticLogon) {
|
||||||
Enable-AutomaticLogon
|
Enable-AutomaticLogon
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||||
|
<settings pass="specialize">
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||||
|
<ComputerName>WIN11</ComputerName>
|
||||||
|
<TimeZone>China Standard Time</TimeZone>
|
||||||
|
</component>
|
||||||
|
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager"
|
||||||
|
processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral"
|
||||||
|
versionScope="nonSxS">
|
||||||
|
<fDenyTSConnections>false</fDenyTSConnections>
|
||||||
|
</component>
|
||||||
|
<component name="Networking-MPSSVC-Svc" processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||||
|
<FirewallGroups>
|
||||||
|
<FirewallGroup xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||||
|
wcm:action="add" wcm:keyValue="RemoteDesktop">
|
||||||
|
<Active>true</Active>
|
||||||
|
<Group>Remote Desktop</Group>
|
||||||
|
<Profile>all</Profile>
|
||||||
|
</FirewallGroup>
|
||||||
|
</FirewallGroups>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
<settings pass="oobeSystem">
|
||||||
|
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||||
|
<InputLocale>en-US</InputLocale>
|
||||||
|
<SystemLocale>en-US</SystemLocale>
|
||||||
|
<UILanguage>en-US</UILanguage>
|
||||||
|
<UserLocale>en-US</UserLocale>
|
||||||
|
</component>
|
||||||
|
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
|
||||||
|
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||||
|
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||||
|
<OOBE>
|
||||||
|
<HideEULAPage>true</HideEULAPage>
|
||||||
|
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||||
|
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||||
|
<ProtectYourPC>3</ProtectYourPC>
|
||||||
|
</OOBE>
|
||||||
|
<UserAccounts>
|
||||||
|
<LocalAccounts>
|
||||||
|
<LocalAccount wcm:action="add">
|
||||||
|
<Name>guofang</Name>
|
||||||
|
<Group>Administrators</Group>
|
||||||
|
<DisplayName>Guofang</DisplayName>
|
||||||
|
<Password>
|
||||||
|
<Value>wang</Value>
|
||||||
|
<PlainText>true</PlainText>
|
||||||
|
</Password>
|
||||||
|
</LocalAccount>
|
||||||
|
</LocalAccounts>
|
||||||
|
</UserAccounts>
|
||||||
|
<AutoLogon>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<LogonCount>1</LogonCount>
|
||||||
|
<Username>guofang</Username>
|
||||||
|
<Password>
|
||||||
|
<Value>wang</Value>
|
||||||
|
<PlainText>true</PlainText>
|
||||||
|
</Password>
|
||||||
|
</AutoLogon>
|
||||||
|
<FirstLogonCommands>
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>1</Order>
|
||||||
|
<Description>Configure Windows from the answer-media script</Description>
|
||||||
|
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -InstallOpenSSH -NoPause"</CommandLine>
|
||||||
|
<RequiresUserInput>false</RequiresUserInput>
|
||||||
|
</SynchronousCommand>
|
||||||
|
</FirstLogonCommands>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
</unattend>
|
||||||
@@ -32,10 +32,10 @@ fi
|
|||||||
work_dir="$(mktemp -d)"
|
work_dir="$(mktemp -d)"
|
||||||
trap 'rm -rf "$work_dir"' EXIT
|
trap 'rm -rf "$work_dir"' EXIT
|
||||||
sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${answer_arch}\"/" \
|
sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${answer_arch}\"/" \
|
||||||
"$script_dir/Autounattend.xml" >"$work_dir/Autounattend.xml"
|
"$script_dir/Unattend.xml" >"$work_dir/Unattend.xml"
|
||||||
|
|
||||||
seed_files=(
|
seed_files=(
|
||||||
"$work_dir/Autounattend.xml"
|
"$work_dir/Unattend.xml"
|
||||||
"$script_dir/Configure-Windows.ps1"
|
"$script_dir/Configure-Windows.ps1"
|
||||||
"$openssh_zip"
|
"$openssh_zip"
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
Submodule vm-images updated: 1dc40655df...384a4ade9f.
Reference in new issue
Block a user