Compare commits
23
Commits
fccc7986d9
..
main
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/git/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
|
||||||
@@ -26,12 +27,12 @@ original qcow2 image as its backing file. The base image therefore stays
|
|||||||
unchanged. Run `./run-debian13.py --help` for the supported environment
|
unchanged. Run `./run-debian13.py --help` for the supported environment
|
||||||
variables.
|
variables.
|
||||||
|
|
||||||
The Debian VM hardware (8G memory, 8 CPUs, drives, network) is
|
The Debian VM hardware (8G memory, 8 CPUs, drives) is
|
||||||
defined in the QEMU config file `qemu/debian13.ini`; the machine type and
|
defined in the QEMU config file `qemu/debian13/config.ini`; the machine type and
|
||||||
architecture-specific devices are in `qemu/debian13-<os>-<arch>.ini`
|
architecture-specific devices are in `qemu/debian13/<os>-<arch>.ini`
|
||||||
(os `linux` or `darwin`, arch `amd64` or `arm64`), together with the
|
(os `linux` or `darwin`, arch `amd64` or `arm64`), together with the
|
||||||
accelerators. Edit these files to change the settings. The
|
accelerators. Edit these files to change the settings. The
|
||||||
launcher fills in the `${...}` file path placeholders of `qemu/debian13.ini`,
|
launcher fills in the `${...}` file path placeholders of `qemu/debian13/config.ini`,
|
||||||
writes the result to `vm/debian13/qemu.ini` and starts QEMU with
|
writes the result to `vm/debian13/qemu.ini` and starts QEMU with
|
||||||
`-readconfig` for that file followed by the one for the host system and guest architecture.
|
`-readconfig` for that file followed by the one for the host system and guest architecture.
|
||||||
QEMU arguments given after `--` override the config files, for example
|
QEMU arguments given after `--` override the config files, for example
|
||||||
@@ -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:
|
||||||
@@ -122,9 +143,9 @@ On the first boot, cloud-init updates package metadata and installs Fedora's
|
|||||||
GNOME package group through its native package module, then reboots the guest.
|
GNOME package group through its native package module, then reboots the guest.
|
||||||
Keep the VM running until this initial installation and reboot have completed.
|
Keep the VM running until this initial installation and reboot have completed.
|
||||||
|
|
||||||
Like the Debian VM, the Fedora VM hardware (8G memory, 8 CPUs, drives, SSH
|
Like the Debian VM, the Fedora VM hardware (8G memory, 8 CPUs, drives) is
|
||||||
port `2224`) is defined in `qemu/fedora44.ini`, with the accelerators, machine type and
|
defined in `qemu/fedora44/config.ini`, with the accelerators, machine type and
|
||||||
platform-specific devices in `qemu/fedora44-<os>-<arch>.ini`. Other Fedora settings use the `FEDORA_VM_` prefix.
|
platform-specific devices in `qemu/fedora44/<os>-<arch>.ini`. Other Fedora settings use the `FEDORA_VM_` prefix.
|
||||||
Run `./run-fedora44.py --help` for the complete list of overrides.
|
Run `./run-fedora44.py --help` for the complete list of overrides.
|
||||||
|
|
||||||
## Ubuntu 26.04
|
## Ubuntu 26.04
|
||||||
@@ -150,9 +171,9 @@ On the first boot, cloud-init installs `ubuntu-desktop-minimal` (GNOME) and
|
|||||||
guest. Keep the VM running until this initial installation and reboot have
|
guest. Keep the VM running until this initial installation and reboot have
|
||||||
completed.
|
completed.
|
||||||
|
|
||||||
The VM hardware (8G memory, 8 CPUs, drives, SSH port `2225`) is defined in
|
The VM hardware (8G memory, 8 CPUs, drives) is defined in
|
||||||
`qemu/ubuntu2604.ini`, with the accelerators, machine type and platform-specific
|
`qemu/ubuntu2604/config.ini`, with the accelerators, machine type and platform-specific
|
||||||
devices in `qemu/ubuntu2604-<os>-<arch>.ini`. Other Ubuntu
|
devices in `qemu/ubuntu2604/<os>-<arch>.ini`. Other Ubuntu
|
||||||
settings use the `UBUNTU_VM_` prefix. Run `./run-ubuntu2604.py --help` for the
|
settings use the `UBUNTU_VM_` prefix. Run `./run-ubuntu2604.py --help` for the
|
||||||
complete list of overrides.
|
complete list of overrides.
|
||||||
|
|
||||||
@@ -165,40 +186,67 @@ 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
|
||||||
`localhost:3389`. 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.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
|
||||||
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`.
|
||||||
|
|
||||||
|
## Port forwards
|
||||||
|
|
||||||
|
The host ports forwarded to the guest are not in the QEMU config files (a
|
||||||
|
config file section cannot hold more than one `hostfwd`) but in one file per
|
||||||
|
VM, `qemu/<vm>/forwards.conf`, which the launcher turns into the `-netdev`
|
||||||
|
argument. Each line is `LABEL HOST_PORT GUEST_PORT`; `#` starts a comment:
|
||||||
|
|
||||||
```bash
|
|
||||||
WIN11_VM_TOOLS_ISO=/custom/path/virtio-win.iso ./run-windows11.py --install-from-iso
|
|
||||||
```
|
```
|
||||||
|
# qemu/windows11/forwards.conf
|
||||||
|
RDP 3389 3389
|
||||||
|
SSH 2222 22
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a line to forward another port. A rule whose host port is already in use
|
||||||
|
is skipped with a warning. `ALMA_VM_FORWARDS`, `ROCKY_VM_FORWARDS`, `FEDORA_VM_FORWARDS`,
|
||||||
|
`UBUNTU_VM_FORWARDS`, `VM_FORWARDS` (Debian) and `WIN11_VM_FORWARDS` select
|
||||||
|
another file. `ALMA_VM_SSH_PORT`, `WIN11_VM_RDP_PORT`, `WIN11_VM_SSH_PORT`
|
||||||
|
and `--rdp-port`/`--ssh-port` still replace the host port of the rule with the
|
||||||
|
label `SSH` or `RDP`. The launchers import `vmforwards.py` from the repository
|
||||||
|
root.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT
|
||||||
|
SSH 2223 22
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# QEMU configuration for the Debian 13 virtual machine, shared by all
|
# QEMU configuration for the Debian 13 virtual machine, shared by all
|
||||||
# architectures. The accelerators, machine type and
|
# architectures. The accelerators, machine type and
|
||||||
# platform-specific devices are in debian13-<os>-<arch>.ini (os is linux or darwin),
|
# platform-specific devices are in <os>-<arch>.ini (os is linux or darwin),
|
||||||
# which QEMU reads after this file.
|
# which QEMU reads after this file.
|
||||||
#
|
#
|
||||||
# run-debian13.py fills in the file path placeholders below, writes the result
|
# run-debian13.py fills in the file path placeholders below, writes the result
|
||||||
@@ -39,21 +39,9 @@
|
|||||||
driver = "virtio-blk-pci"
|
driver = "virtio-blk-pci"
|
||||||
drive = "disk0"
|
drive = "disk0"
|
||||||
|
|
||||||
# cloud-init seed ISO.
|
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||||
[drive "seed0"]
|
# defines with -netdev from qemu/debian13/forwards.conf because a config file
|
||||||
if = "none"
|
# section cannot hold more than one hostfwd.
|
||||||
format = "raw"
|
|
||||||
readonly = "on"
|
|
||||||
file = "${seed_image}"
|
|
||||||
|
|
||||||
[device "seed0-dev"]
|
|
||||||
driver = "virtio-blk-pci"
|
|
||||||
drive = "seed0"
|
|
||||||
|
|
||||||
# User-mode networking with guest SSH forwarded to the host.
|
|
||||||
[netdev "net0"]
|
|
||||||
type = "user"
|
|
||||||
hostfwd = "tcp::2222-:22"
|
|
||||||
|
|
||||||
[device "nic0"]
|
[device "nic0"]
|
||||||
driver = "virtio-net-pci"
|
driver = "virtio-net-pci"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# macOS host, arm64 part of the Debian 13 QEMU configuration; read after debian13.ini.
|
# macOS host, arm64 part of the Debian 13 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: HVF on macOS, else emulation.
|
# Accelerators are tried in order: HVF on macOS, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT
|
||||||
|
SSH 2222 22
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Linux host, amd64 part of the Debian 13 QEMU configuration; read after debian13.ini.
|
# Linux host, amd64 part of the Debian 13 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Linux host, arm64 part of the Debian 13 QEMU configuration; read after debian13.ini.
|
# Linux host, arm64 part of the Debian 13 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# QEMU configuration for the Fedora 44 virtual machine, shared by all
|
# QEMU configuration for the Fedora 44 virtual machine, shared by all
|
||||||
# architectures. The accelerators, machine type and
|
# architectures. The accelerators, machine type and
|
||||||
# platform-specific devices are in fedora44-<os>-<arch>.ini (os is linux or darwin),
|
# platform-specific devices are in <os>-<arch>.ini (os is linux or darwin),
|
||||||
# which QEMU reads after this file.
|
# which QEMU reads after this file.
|
||||||
#
|
#
|
||||||
# run-fedora44.py fills in the file path placeholders below, writes the result
|
# run-fedora44.py fills in the file path placeholders below, writes the result
|
||||||
@@ -39,21 +39,9 @@
|
|||||||
driver = "virtio-blk-pci"
|
driver = "virtio-blk-pci"
|
||||||
drive = "disk0"
|
drive = "disk0"
|
||||||
|
|
||||||
# cloud-init seed ISO.
|
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||||
[drive "seed0"]
|
# defines with -netdev from qemu/fedora44/forwards.conf because a config file
|
||||||
if = "none"
|
# section cannot hold more than one hostfwd.
|
||||||
format = "raw"
|
|
||||||
readonly = "on"
|
|
||||||
file = "${seed_image}"
|
|
||||||
|
|
||||||
[device "seed0-dev"]
|
|
||||||
driver = "virtio-blk-pci"
|
|
||||||
drive = "seed0"
|
|
||||||
|
|
||||||
# User-mode networking with guest SSH forwarded to the host.
|
|
||||||
[netdev "net0"]
|
|
||||||
type = "user"
|
|
||||||
hostfwd = "tcp::2224-:22"
|
|
||||||
|
|
||||||
[device "nic0"]
|
[device "nic0"]
|
||||||
driver = "virtio-net-pci"
|
driver = "virtio-net-pci"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# macOS host, arm64 part of the Fedora 44 QEMU configuration; read after fedora44.ini.
|
# macOS host, arm64 part of the Fedora 44 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: HVF on macOS, else emulation.
|
# Accelerators are tried in order: HVF on macOS, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT
|
||||||
|
SSH 2224 22
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Linux host, amd64 part of the Fedora 44 QEMU configuration; read after fedora44.ini.
|
# Linux host, amd64 part of the Fedora 44 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Linux host, arm64 part of the Fedora 44 QEMU configuration; read after fedora44.ini.
|
# Linux host, arm64 part of the Fedora 44 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -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"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# QEMU configuration for the Ubuntu 26.04 virtual machine, shared by all
|
# QEMU configuration for the Ubuntu 26.04 virtual machine, shared by all
|
||||||
# architectures. The accelerators, machine type and
|
# architectures. The accelerators, machine type and
|
||||||
# platform-specific devices are in ubuntu2604-<os>-<arch>.ini (os is linux or darwin),
|
# platform-specific devices are in <os>-<arch>.ini (os is linux or darwin),
|
||||||
# which QEMU reads after this file.
|
# which QEMU reads after this file.
|
||||||
#
|
#
|
||||||
# run-ubuntu2604.py fills in the file path placeholders below, writes the result
|
# run-ubuntu2604.py fills in the file path placeholders below, writes the result
|
||||||
@@ -39,21 +39,9 @@
|
|||||||
driver = "virtio-blk-pci"
|
driver = "virtio-blk-pci"
|
||||||
drive = "disk0"
|
drive = "disk0"
|
||||||
|
|
||||||
# cloud-init seed ISO.
|
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||||
[drive "seed0"]
|
# defines with -netdev from qemu/ubuntu2604/forwards.conf because a config file
|
||||||
if = "none"
|
# section cannot hold more than one hostfwd.
|
||||||
format = "raw"
|
|
||||||
readonly = "on"
|
|
||||||
file = "${seed_image}"
|
|
||||||
|
|
||||||
[device "seed0-dev"]
|
|
||||||
driver = "virtio-blk-pci"
|
|
||||||
drive = "seed0"
|
|
||||||
|
|
||||||
# User-mode networking with guest SSH forwarded to the host.
|
|
||||||
[netdev "net0"]
|
|
||||||
type = "user"
|
|
||||||
hostfwd = "tcp::2225-:22"
|
|
||||||
|
|
||||||
[device "nic0"]
|
[device "nic0"]
|
||||||
driver = "virtio-net-pci"
|
driver = "virtio-net-pci"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# macOS host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini.
|
# macOS host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: HVF on macOS, else emulation.
|
# Accelerators are tried in order: HVF on macOS, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT
|
||||||
|
SSH 2225 22
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Linux host, amd64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini.
|
# Linux host, amd64 part of the Ubuntu 26.04 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Linux host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini.
|
# Linux host, arm64 part of the Ubuntu 26.04 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# macOS host, arm64 part of the Windows 11 QEMU configuration; read after windows11.ini.
|
|
||||||
|
|
||||||
# Accelerators are tried in order: HVF on macOS, else emulation.
|
|
||||||
[accel]
|
|
||||||
accel = "hvf"
|
|
||||||
|
|
||||||
[accel]
|
|
||||||
accel = "tcg"
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
[machine]
|
|
||||||
type = "virt"
|
|
||||||
kernel-irqchip = "off"
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
[device "system_disk-dev"]
|
|
||||||
driver = "nvme"
|
|
||||||
drive = "system_disk"
|
|
||||||
serial = "win11disk"
|
|
||||||
bootindex = "1"
|
|
||||||
|
|
||||||
# Windows has no inbox driver for virtio-net; the guest tools installed by
|
|
||||||
# Configure-Windows.ps1 provide it.
|
|
||||||
[device "nic0"]
|
|
||||||
driver = "virtio-net-pci"
|
|
||||||
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"]
|
|
||||||
driver = "virtio-gpu-pci"
|
|
||||||
xres = "2304"
|
|
||||||
yres = "1440"
|
|
||||||
|
|
||||||
[device "kbd0"]
|
|
||||||
driver = "usb-kbd"
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Linux host, arm64 part of the Windows 11 QEMU configuration; read after windows11.ini.
|
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
|
||||||
[accel]
|
|
||||||
accel = "kvm"
|
|
||||||
|
|
||||||
[accel]
|
|
||||||
accel = "tcg"
|
|
||||||
|
|
||||||
[machine]
|
|
||||||
type = "virt"
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
[device "system_disk-dev"]
|
|
||||||
driver = "nvme"
|
|
||||||
drive = "system_disk"
|
|
||||||
serial = "win11disk"
|
|
||||||
bootindex = "1"
|
|
||||||
|
|
||||||
# Windows has no inbox driver for virtio-net; the guest tools installed by
|
|
||||||
# Configure-Windows.ps1 provide it.
|
|
||||||
[device "nic0"]
|
|
||||||
driver = "virtio-net-pci"
|
|
||||||
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"]
|
|
||||||
driver = "virtio-gpu-pci"
|
|
||||||
xres = "2048"
|
|
||||||
yres = "1280"
|
|
||||||
|
|
||||||
[device "kbd0"]
|
|
||||||
driver = "usb-kbd"
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# QEMU configuration for the Windows 11 virtual machine. The accelerators and machine type
|
# QEMU configuration for the Windows 11 virtual machine. The accelerators and machine type
|
||||||
# are in windows11-<os>-<arch>.ini (os is linux or darwin), which QEMU reads after
|
# are in <os>-<arch>.ini (os is linux or darwin), which QEMU reads after
|
||||||
# this file.
|
# this file.
|
||||||
#
|
#
|
||||||
# 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"
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
|
|
||||||
# System disk image; the device that exposes it (and the NIC, which is
|
# System disk image; the device that exposes it (and the NIC, which is
|
||||||
# attached to the user-mode network "net0" that the launcher defines with
|
# attached to the user-mode network "net0" that the launcher defines with
|
||||||
# -netdev because a config file section cannot hold more than one hostfwd) is in
|
# -netdev from qemu/windows11/forwards.conf because a config file section cannot hold
|
||||||
# the architecture file.
|
# more than one hostfwd) is in the architecture file.
|
||||||
[drive "system_disk"]
|
[drive "system_disk"]
|
||||||
if = "none"
|
if = "none"
|
||||||
format = "qcow2"
|
format = "qcow2"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# macOS host, arm64 part of the Windows 11 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
|
# Accelerators are tried in order: HVF on macOS, else emulation.
|
||||||
|
[accel]
|
||||||
|
accel = "hvf"
|
||||||
|
|
||||||
|
[accel]
|
||||||
|
accel = "tcg"
|
||||||
|
|
||||||
|
# run-windows11.py adds kernel-irqchip=off (see there).
|
||||||
|
[machine]
|
||||||
|
type = "virt"
|
||||||
|
|
||||||
|
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
||||||
|
# installation media) both have inbox Windows drivers.
|
||||||
|
[device "system_disk-dev"]
|
||||||
|
driver = "nvme"
|
||||||
|
drive = "system_disk"
|
||||||
|
serial = "win11disk"
|
||||||
|
bootindex = "1"
|
||||||
|
|
||||||
|
# The base image carries the VirtIO drivers (build-windows-base.sh installs them
|
||||||
|
# from the virtio-win ISO), so the VM uses virtio-net and virtio-gpu. virtio-gpu
|
||||||
|
# is the only display, driven by viogpudo; ramfb is not added because Windows
|
||||||
|
# would then see a second adapter with a fixed-mode monitor. xres/yres set the
|
||||||
|
# preferred mode in the EDID (QEMU defaults to 1280x800); viogpudo accepted
|
||||||
|
# 1920x1200, 2048x1280 and 2304x1440 but not 2560x1440 or 2560x1600.
|
||||||
|
[device "nic0"]
|
||||||
|
driver = "virtio-net-pci"
|
||||||
|
netdev = "net0"
|
||||||
|
|
||||||
|
[device "gpu0"]
|
||||||
|
driver = "virtio-gpu-pci"
|
||||||
|
xres = "2304"
|
||||||
|
yres = "1440"
|
||||||
|
|
||||||
|
[device "kbd0"]
|
||||||
|
driver = "usb-kbd"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Host ports forwarded to the guest: LABEL HOST_PORT GUEST_PORT
|
||||||
|
# --rdp-port and --ssh-port (or WIN11_VM_RDP_PORT and WIN11_VM_SSH_PORT)
|
||||||
|
# replace the host port of the RDP and SSH rules.
|
||||||
|
RDP 3389 3389
|
||||||
|
SSH 2222 22
|
||||||
|
DSH 3080 3080
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Linux host, amd64 part of the Windows 11 QEMU configuration; read after windows11.ini.
|
# Linux host, amd64 part of the Windows 11 QEMU configuration; read after config.ini.
|
||||||
|
|
||||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||||
[accel]
|
[accel]
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Linux host, arm64 part of the Windows 11 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"
|
||||||
|
|
||||||
|
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
||||||
|
# installation media) both have inbox Windows drivers.
|
||||||
|
[device "system_disk-dev"]
|
||||||
|
driver = "nvme"
|
||||||
|
drive = "system_disk"
|
||||||
|
serial = "win11disk"
|
||||||
|
bootindex = "1"
|
||||||
|
|
||||||
|
# The base image carries the VirtIO drivers (build-windows-base.sh installs them
|
||||||
|
# from the virtio-win ISO), so the VM uses virtio-net and virtio-gpu. virtio-gpu
|
||||||
|
# is the only display, driven by viogpudo; ramfb is not added because Windows
|
||||||
|
# would then see a second adapter with a fixed-mode monitor. xres/yres set the
|
||||||
|
# preferred mode in the EDID (QEMU defaults to 1280x800); viogpudo accepted
|
||||||
|
# 1920x1200, 2048x1280 and 2304x1440 but not 2560x1440 or 2560x1600.
|
||||||
|
[device "nic0"]
|
||||||
|
driver = "virtio-net-pci"
|
||||||
|
netdev = "net0"
|
||||||
|
|
||||||
|
[device "gpu0"]
|
||||||
|
driver = "virtio-gpu-pci"
|
||||||
|
xres = "2048"
|
||||||
|
yres = "1280"
|
||||||
|
|
||||||
|
[device "kbd0"]
|
||||||
|
driver = "usb-kbd"
|
||||||
+27
-9
@@ -8,6 +8,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import vmforwards
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
env = os.environ.get
|
env = os.environ.get
|
||||||
|
|
||||||
@@ -55,7 +57,9 @@ state_dir = Path(env("ALMA_VM_STATE_DIR") or script_dir / "vm" / "almalinux10")
|
|||||||
disk_size = env("ALMA_VM_DISK_SIZE") or "20G"
|
disk_size = env("ALMA_VM_DISK_SIZE") or "20G"
|
||||||
memory = env("ALMA_VM_MEMORY") or "8G"
|
memory = env("ALMA_VM_MEMORY") or "8G"
|
||||||
cpus = env("ALMA_VM_CPUS") or "8"
|
cpus = env("ALMA_VM_CPUS") or "8"
|
||||||
ssh_port = env("ALMA_VM_SSH_PORT") or "2223"
|
ssh_port = env("ALMA_VM_SSH_PORT")
|
||||||
|
forwards_file = Path(
|
||||||
|
env("ALMA_VM_FORWARDS") or script_dir / "qemu" / "almalinux10" / "forwards.conf")
|
||||||
|
|
||||||
|
|
||||||
def normalize_arch(arch):
|
def normalize_arch(arch):
|
||||||
@@ -127,6 +131,8 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [
|
|||||||
Start an AlmaLinux 10 Generic Cloud virtual machine. The launcher obtains a
|
Start an AlmaLinux 10 Generic Cloud virtual machine. The launcher obtains a
|
||||||
missing base image through ../vm-images/download-images.sh and creates a
|
missing base image through ../vm-images/download-images.sh and creates a
|
||||||
writable overlay in the state directory, leaving the base image unchanged.
|
writable overlay in the state directory, leaving the base image unchanged.
|
||||||
|
Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are read
|
||||||
|
from qemu/almalinux10/forwards.conf.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--daemon Run in the background while keeping the graphical window
|
--daemon Run in the background while keeping the graphical window
|
||||||
@@ -138,7 +144,9 @@ Options:
|
|||||||
Environment variables:
|
Environment variables:
|
||||||
ALMA_VM_MEMORY Guest memory (default: 8G)
|
ALMA_VM_MEMORY Guest memory (default: 8G)
|
||||||
ALMA_VM_CPUS Virtual CPU count (default: 8)
|
ALMA_VM_CPUS Virtual CPU count (default: 8)
|
||||||
ALMA_VM_SSH_PORT Host port forwarded to guest SSH (default: 2223)
|
ALMA_VM_SSH_PORT Host port of the SSH rule in the port forward file
|
||||||
|
(default: from the file, 2223)
|
||||||
|
ALMA_VM_FORWARDS Port forward file (default: qemu/almalinux10/forwards.conf)
|
||||||
ALMA_VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper)
|
ALMA_VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper)
|
||||||
ALMA_VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk)
|
ALMA_VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk)
|
||||||
ALMA_VM_STATE_DIR Runtime state directory (default: vm/almalinux10)
|
ALMA_VM_STATE_DIR Runtime state directory (default: vm/almalinux10)
|
||||||
@@ -200,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}")
|
||||||
@@ -232,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}")
|
||||||
@@ -240,6 +249,9 @@ if not disk_image.is_file():
|
|||||||
is_macos = platform.system() == "Darwin"
|
is_macos = platform.system() == "Darwin"
|
||||||
display_backend = env("ALMA_VM_DISPLAY") or ("cocoa" if is_macos else "gtk")
|
display_backend = env("ALMA_VM_DISPLAY") or ("cocoa" if is_macos else "gtk")
|
||||||
|
|
||||||
|
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(
|
||||||
|
forwards_file, {"SSH": ssh_port} if ssh_port else None))
|
||||||
|
|
||||||
qemu_args = [
|
qemu_args = [
|
||||||
"-name", "almalinux10",
|
"-name", "almalinux10",
|
||||||
"-machine", machine_type,
|
"-machine", machine_type,
|
||||||
@@ -250,11 +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",
|
"-netdev", vmforwards.netdev_argument(forwards),
|
||||||
"-device", "virtio-blk-pci,drive=seed0",
|
"-device", "virtio-net-pci,netdev=net0",
|
||||||
"-nic", f"user,model=virtio-net-pci,hostfwd=tcp::{ssh_port}-:22",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 += [
|
||||||
@@ -274,8 +292,8 @@ else:
|
|||||||
qemu_args += ["-accel", "tcg", "-cpu", "max"]
|
qemu_args += ["-accel", "tcg", "-cpu", "max"]
|
||||||
accelerator = "TCG"
|
accelerator = "TCG"
|
||||||
|
|
||||||
print(f"Starting AlmaLinux 10 ({host_arch}) with {accelerator} acceleration; "
|
print(f"Starting AlmaLinux 10 ({host_arch}) with {accelerator} acceleration.", flush=True)
|
||||||
f"SSH is forwarded to localhost:{ssh_port}.", flush=True)
|
vmforwards.print_forwards(forwards)
|
||||||
|
|
||||||
if daemonize:
|
if daemonize:
|
||||||
log_file.write_text("")
|
log_file.write_text("")
|
||||||
|
|||||||
+30
-17
@@ -3,13 +3,14 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import vmforwards
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
env = os.environ.get
|
env = os.environ.get
|
||||||
|
|
||||||
@@ -68,11 +69,13 @@ def normalize_arch(arch):
|
|||||||
native_arch = normalize_arch(platform.machine())
|
native_arch = normalize_arch(platform.machine())
|
||||||
host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch)
|
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_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
|
||||||
qemu_config_template = Path(env("VM_QEMU_CONFIG") or script_dir / "qemu" / "debian13.ini")
|
qemu_config_template = Path(env("VM_QEMU_CONFIG") or script_dir / "qemu" / "debian13" / "config.ini")
|
||||||
host_os = platform.system().lower()
|
host_os = platform.system().lower()
|
||||||
if host_os == "darwin" and host_arch == "amd64":
|
if host_os == "darwin" and host_arch == "amd64":
|
||||||
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
||||||
qemu_platform_config = script_dir / "qemu" / f"debian13-{host_os}-{host_arch}.ini"
|
qemu_platform_config = script_dir / "qemu" / "debian13" / f"{host_os}-{host_arch}.ini"
|
||||||
|
forwards_file = Path(
|
||||||
|
env("VM_FORWARDS") or script_dir / "qemu" / "debian13" / "forwards.conf")
|
||||||
|
|
||||||
|
|
||||||
def pick_firmware(override, *candidates):
|
def pick_firmware(override, *candidates):
|
||||||
@@ -125,10 +128,12 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [
|
|||||||
|
|
||||||
Start the Debian 13 virtual machine. A writable overlay is created in the state
|
Start the Debian 13 virtual machine. A writable overlay is created in the state
|
||||||
directory (default: vm/debian13/), so the downloaded base image is never
|
directory (default: vm/debian13/), so the downloaded base image is never
|
||||||
modified. The VM hardware (memory, CPUs, SSH port forward, ...) is defined in
|
modified. The VM hardware (memory, CPUs, ...) is defined in
|
||||||
qemu/debian13.ini, which is rendered to qemu.ini in the state directory and
|
qemu/debian13/config.ini, which is rendered to qemu.ini in the state directory and
|
||||||
loaded with QEMU's -readconfig, followed by qemu/debian13-<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64). Later
|
loaded with QEMU's -readconfig, followed by qemu/debian13/<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64). Later
|
||||||
QEMU arguments override them, e.g. `-- -m 4G -smp 4`.
|
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/debian13/forwards.conf.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--daemon Run in the background while keeping the graphical window
|
--daemon Run in the background while keeping the graphical window
|
||||||
@@ -148,7 +153,8 @@ Environment variables:
|
|||||||
VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
|
VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
|
||||||
VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
|
VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
|
||||||
VM_QEMU_CONFIG QEMU config template rendered into the state directory and
|
VM_QEMU_CONFIG QEMU config template rendered into the state directory and
|
||||||
passed to -readconfig (default: qemu/debian13.ini)
|
passed to -readconfig (default: qemu/debian13/config.ini)
|
||||||
|
VM_FORWARDS Port forward file (default: qemu/debian13/forwards.conf)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
@@ -202,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}")
|
||||||
@@ -234,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}")
|
||||||
@@ -248,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.
|
||||||
@@ -264,24 +270,31 @@ except (KeyError, ValueError) as error:
|
|||||||
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
||||||
qemu_config.write_text(qemu_config_text)
|
qemu_config.write_text(qemu_config_text)
|
||||||
|
|
||||||
|
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(forwards_file))
|
||||||
|
|
||||||
qemu_args = [
|
qemu_args = [
|
||||||
"-readconfig", str(qemu_config),
|
"-readconfig", str(qemu_config),
|
||||||
"-readconfig", str(qemu_platform_config),
|
"-readconfig", str(qemu_platform_config),
|
||||||
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
||||||
"-cpu", "max",
|
"-cpu", "max",
|
||||||
# The config file's [netdev] does not stop QEMU from adding its default
|
# A config file section cannot hold two hostfwd entries, so the network
|
||||||
# NIC, a second user network without the SSH port forward.
|
# 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",
|
"-nic", "none",
|
||||||
"-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)
|
||||||
# The SSH port forward lives in the config file, e.g. hostfwd = "tcp::2222-:22".
|
vmforwards.print_forwards(forwards)
|
||||||
ssh_forward = re.search(r'hostfwd\s*=\s*"tcp:[^:"]*:(\d+)-[^:"]*:22"', qemu_config_text)
|
|
||||||
if ssh_forward:
|
|
||||||
print(f"SSH is forwarded to localhost:{ssh_forward.group(1)}: "
|
|
||||||
f"ssh -p {ssh_forward.group(1)} guofang@localhost", flush=True)
|
|
||||||
|
|
||||||
if daemonize:
|
if daemonize:
|
||||||
log_file.write_text("")
|
log_file.write_text("")
|
||||||
|
|||||||
+29
-16
@@ -3,13 +3,14 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import vmforwards
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
env = os.environ.get
|
env = os.environ.get
|
||||||
|
|
||||||
@@ -69,11 +70,13 @@ native_arch = normalize_arch(platform.machine())
|
|||||||
host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch)
|
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_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
|
||||||
qemu_config_template = Path(
|
qemu_config_template = Path(
|
||||||
env("FEDORA_VM_QEMU_CONFIG") or script_dir / "qemu" / "fedora44.ini")
|
env("FEDORA_VM_QEMU_CONFIG") or script_dir / "qemu" / "fedora44" / "config.ini")
|
||||||
host_os = platform.system().lower()
|
host_os = platform.system().lower()
|
||||||
if host_os == "darwin" and host_arch == "amd64":
|
if host_os == "darwin" and host_arch == "amd64":
|
||||||
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
||||||
qemu_platform_config = script_dir / "qemu" / f"fedora44-{host_os}-{host_arch}.ini"
|
qemu_platform_config = script_dir / "qemu" / "fedora44" / f"{host_os}-{host_arch}.ini"
|
||||||
|
forwards_file = Path(
|
||||||
|
env("FEDORA_VM_FORWARDS") or script_dir / "qemu" / "fedora44" / "forwards.conf")
|
||||||
|
|
||||||
|
|
||||||
def pick_firmware(override, *candidates):
|
def pick_firmware(override, *candidates):
|
||||||
@@ -127,10 +130,12 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [
|
|||||||
Start a Fedora 44 Cloud Base virtual machine. The launcher obtains a missing
|
Start a Fedora 44 Cloud Base virtual machine. The launcher obtains a missing
|
||||||
base image through the shared image helper and creates a writable overlay
|
base image through the shared image helper and creates a writable overlay
|
||||||
in the state directory, leaving the base image unchanged. The VM hardware
|
in the state directory, leaving the base image unchanged. The VM hardware
|
||||||
(memory, CPUs, SSH port forward, ...) is defined in qemu/fedora44.ini, which is
|
(memory, CPUs, ...) is defined in qemu/fedora44/config.ini, which is
|
||||||
rendered to qemu.ini in the state directory and loaded with QEMU's -readconfig,
|
rendered to qemu.ini in the state directory and loaded with QEMU's -readconfig,
|
||||||
followed by qemu/fedora44-<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g.
|
followed by qemu/fedora44/<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g.
|
||||||
`-- -m 4G -smp 4`.
|
`-- -m 4G -smp 4`.
|
||||||
|
Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are
|
||||||
|
read from qemu/fedora44/forwards.conf.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--daemon Run in the background while keeping the graphical window
|
--daemon Run in the background while keeping the graphical window
|
||||||
@@ -150,7 +155,8 @@ Environment variables:
|
|||||||
FEDORA_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
|
FEDORA_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
|
||||||
FEDORA_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
|
FEDORA_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
|
||||||
FEDORA_VM_QEMU_CONFIG QEMU config template rendered into the state directory and
|
FEDORA_VM_QEMU_CONFIG QEMU config template rendered into the state directory and
|
||||||
passed to -readconfig (default: qemu/fedora44.ini)
|
passed to -readconfig (default: qemu/fedora44/config.ini)
|
||||||
|
FEDORA_VM_FORWARDS Port forward file (default: qemu/fedora44/forwards.conf)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
@@ -204,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}")
|
||||||
@@ -236,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}")
|
||||||
@@ -250,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.
|
||||||
@@ -266,24 +272,31 @@ except (KeyError, ValueError) as error:
|
|||||||
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
||||||
qemu_config.write_text(qemu_config_text)
|
qemu_config.write_text(qemu_config_text)
|
||||||
|
|
||||||
|
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(forwards_file))
|
||||||
|
|
||||||
qemu_args = [
|
qemu_args = [
|
||||||
"-readconfig", str(qemu_config),
|
"-readconfig", str(qemu_config),
|
||||||
"-readconfig", str(qemu_platform_config),
|
"-readconfig", str(qemu_platform_config),
|
||||||
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
||||||
"-cpu", "max",
|
"-cpu", "max",
|
||||||
# The config file's [netdev] does not stop QEMU from adding its default
|
# A config file section cannot hold two hostfwd entries, so the network
|
||||||
# NIC, a second user network without the SSH port forward.
|
# 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",
|
"-nic", "none",
|
||||||
"-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)
|
||||||
# The SSH port forward lives in the config file, e.g. hostfwd = "tcp::2224-:22".
|
vmforwards.print_forwards(forwards)
|
||||||
ssh_forward = re.search(r'hostfwd\s*=\s*"tcp:[^:"]*:(\d+)-[^:"]*:22"', qemu_config_text)
|
|
||||||
if ssh_forward:
|
|
||||||
print(f"SSH is forwarded to localhost:{ssh_forward.group(1)}: "
|
|
||||||
f"ssh -p {ssh_forward.group(1)} guofang@localhost", flush=True)
|
|
||||||
|
|
||||||
if daemonize:
|
if daemonize:
|
||||||
log_file.write_text("")
|
log_file.write_text("")
|
||||||
|
|||||||
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])
|
||||||
+31
-18
@@ -3,13 +3,14 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import vmforwards
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
env = os.environ.get
|
env = os.environ.get
|
||||||
|
|
||||||
@@ -69,11 +70,13 @@ native_arch = normalize_arch(platform.machine())
|
|||||||
host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch)
|
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_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
|
||||||
qemu_config_template = Path(
|
qemu_config_template = Path(
|
||||||
env("UBUNTU_VM_QEMU_CONFIG") or script_dir / "qemu" / "ubuntu2604.ini")
|
env("UBUNTU_VM_QEMU_CONFIG") or script_dir / "qemu" / "ubuntu2604" / "config.ini")
|
||||||
host_os = platform.system().lower()
|
host_os = platform.system().lower()
|
||||||
if host_os == "darwin" and host_arch == "amd64":
|
if host_os == "darwin" and host_arch == "amd64":
|
||||||
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
||||||
qemu_platform_config = script_dir / "qemu" / f"ubuntu2604-{host_os}-{host_arch}.ini"
|
qemu_platform_config = script_dir / "qemu" / "ubuntu2604" / f"{host_os}-{host_arch}.ini"
|
||||||
|
forwards_file = Path(
|
||||||
|
env("UBUNTU_VM_FORWARDS") or script_dir / "qemu" / "ubuntu2604" / "forwards.conf")
|
||||||
|
|
||||||
|
|
||||||
def pick_firmware(override, *candidates):
|
def pick_firmware(override, *candidates):
|
||||||
@@ -126,11 +129,13 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [
|
|||||||
|
|
||||||
Start an Ubuntu 26.04 LTS cloud image virtual machine. A writable overlay is
|
Start an Ubuntu 26.04 LTS cloud image virtual machine. A writable overlay is
|
||||||
created in the state directory (default: vm/ubuntu2604/), so the downloaded
|
created in the state directory (default: vm/ubuntu2604/), so the downloaded
|
||||||
base image is never modified. The VM hardware (memory, CPUs, SSH port forward,
|
base image is never modified. The VM hardware (memory, CPUs, ...) is defined
|
||||||
...) is defined in qemu/ubuntu2604.ini, which is rendered to qemu.ini in the
|
in qemu/ubuntu2604/config.ini, which is rendered to qemu.ini in the state directory
|
||||||
state directory and loaded with QEMU's -readconfig, followed by
|
and loaded with QEMU's -readconfig, followed by
|
||||||
qemu/ubuntu2604-<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g.
|
qemu/ubuntu2604/<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64). Later QEMU arguments override them, e.g.
|
||||||
`-- -m 4G -smp 4`.
|
`-- -m 4G -smp 4`.
|
||||||
|
Host port forwards, one "LABEL HOST_PORT GUEST_PORT" rule per line, are
|
||||||
|
read from qemu/ubuntu2604/forwards.conf.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--daemon Run in the background while keeping the graphical window
|
--daemon Run in the background while keeping the graphical window
|
||||||
@@ -150,7 +155,8 @@ Environment variables:
|
|||||||
UBUNTU_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
|
UBUNTU_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
|
||||||
UBUNTU_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
|
UBUNTU_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
|
||||||
UBUNTU_VM_QEMU_CONFIG QEMU config template rendered into the state directory and
|
UBUNTU_VM_QEMU_CONFIG QEMU config template rendered into the state directory and
|
||||||
passed to -readconfig (default: qemu/ubuntu2604.ini)
|
passed to -readconfig (default: qemu/ubuntu2604/config.ini)
|
||||||
|
UBUNTU_VM_FORWARDS Port forward file (default: qemu/ubuntu2604/forwards.conf)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
@@ -204,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}")
|
||||||
@@ -236,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}")
|
||||||
@@ -250,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.
|
||||||
@@ -266,24 +272,31 @@ except (KeyError, ValueError) as error:
|
|||||||
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
||||||
qemu_config.write_text(qemu_config_text)
|
qemu_config.write_text(qemu_config_text)
|
||||||
|
|
||||||
|
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(forwards_file))
|
||||||
|
|
||||||
qemu_args = [
|
qemu_args = [
|
||||||
"-readconfig", str(qemu_config),
|
"-readconfig", str(qemu_config),
|
||||||
"-readconfig", str(qemu_platform_config),
|
"-readconfig", str(qemu_platform_config),
|
||||||
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
||||||
"-cpu", "max",
|
"-cpu", "max",
|
||||||
# The config file's [netdev] does not stop QEMU from adding its default
|
# A config file section cannot hold two hostfwd entries, so the network
|
||||||
# NIC, a second user network without the SSH port forward.
|
# 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",
|
"-nic", "none",
|
||||||
"-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)
|
||||||
# The SSH port forward lives in the config file, e.g. hostfwd = "tcp::2225-:22".
|
vmforwards.print_forwards(forwards)
|
||||||
ssh_forward = re.search(r'hostfwd\s*=\s*"tcp:[^:"]*:(\d+)-[^:"]*:22"', qemu_config_text)
|
|
||||||
if ssh_forward:
|
|
||||||
print(f"SSH is forwarded to localhost:{ssh_forward.group(1)}: "
|
|
||||||
f"ssh -p {ssh_forward.group(1)} guofang@localhost", flush=True)
|
|
||||||
|
|
||||||
if daemonize:
|
if daemonize:
|
||||||
log_file.write_text("")
|
log_file.write_text("")
|
||||||
|
|||||||
+302
-373
@@ -4,12 +4,13 @@
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import vmforwards
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
env = os.environ.get
|
env = os.environ.get
|
||||||
|
|
||||||
@@ -34,17 +35,6 @@ def capture(command):
|
|||||||
return result.stdout.rstrip("\n")
|
return result.stdout.rstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
def port_in_use(port):
|
|
||||||
"""True if a host TCP port cannot be bound, as QEMU does for a hostfwd rule."""
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
||||||
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
try:
|
|
||||||
probe.bind(("", int(port)))
|
|
||||||
except OSError:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_executable(path):
|
def is_executable(path):
|
||||||
return path.is_file() and os.access(path, os.X_OK)
|
return path.is_file() and os.access(path, os.X_OK)
|
||||||
|
|
||||||
@@ -61,14 +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") or "3389"
|
|
||||||
ssh_port = env("WIN11_VM_SSH_PORT") or "2222"
|
|
||||||
qemu_config_template = Path(
|
|
||||||
env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11.ini")
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_arch(arch):
|
def normalize_arch(arch):
|
||||||
if arch in ("arm64", "aarch64"):
|
if arch in ("arm64", "aarch64"):
|
||||||
return "arm64"
|
return "arm64"
|
||||||
@@ -77,133 +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
|
|
||||||
|
|
||||||
|
|
||||||
def usage(stream=sys.stdout):
|
|
||||||
stream.write(f"""\
|
|
||||||
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--seed-only] [--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.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
|
|
||||||
--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 forwarded to guest RDP (default: 3389;
|
|
||||||
overrides WIN11_VM_RDP_PORT)
|
|
||||||
--ssh-port PORT Host port forwarded to guest SSH (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: 3389)
|
|
||||||
WIN11_VM_SSH_PORT Host SSH port (default: 2222)
|
|
||||||
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.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 == "--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" / f"windows11-{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)
|
||||||
@@ -213,81 +68,298 @@ 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",
|
||||||
"/opt/homebrew/share/qemu/edk2-arm-vars.fd",
|
"/opt/homebrew/share/qemu/edk2-arm-vars.fd",
|
||||||
"/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"))]
|
||||||
|
|
||||||
# build-seed.sh embeds the administrator SSH public key.
|
# build-seed.sh embeds the administrator SSH public key.
|
||||||
ssh_key = os.environ.get("SSH_PUBLIC_KEY_FILE") or next(
|
ssh_key = os.environ.get("SSH_PUBLIC_KEY_FILE") or next(
|
||||||
@@ -298,192 +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 = 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.
|
||||||
|
qemu_args += ["-machine", "kernel-irqchip=off"]
|
||||||
# Most of the VM definition lives in the QEMU config template; fill in the
|
if creating:
|
||||||
# file paths and hand the rendered file to QEMU with -readconfig.
|
# q35 has IDE (ide.0 is the system disk); "virt" does not, so use USB.
|
||||||
config_values = {
|
qemu_args += ["-drive", f"file={seed_iso},if=none,id=seed_media,media=cdrom,readonly=on",
|
||||||
"uefi_code": uefi_code,
|
"-device", "ide-cd,drive=seed_media,bus=ide.1" if plat.arch == "amd64"
|
||||||
"uefi_vars": uefi_vars,
|
else "usb-storage,drive=seed_media"]
|
||||||
"disk_image": disk_image,
|
start_qemu(plat, rt, qemu_args, options.extra_args, forwards, options.daemonize,
|
||||||
"guest_agent_socket": guest_agent_socket,
|
"Starting Windows 11 from the QCOW2 base image." if creating
|
||||||
"monitor_socket": monitor_socket,
|
else "Starting Windows 11 normally.")
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
# QEMU refuses to start when a forwarded host port is taken, so warn and leave
|
|
||||||
# that forward out instead.
|
|
||||||
forwards = []
|
|
||||||
forwarded_ports = []
|
|
||||||
for label, host_port, guest_port in (("RDP", rdp_port, 3389), ("SSH", ssh_port, 22)):
|
|
||||||
in_use = port_in_use(host_port)
|
|
||||||
forwarded_ports.append((label, host_port, not in_use))
|
|
||||||
if in_use:
|
|
||||||
print(f"Warning: host port {host_port} is already in use; "
|
|
||||||
f"{label} will not be forwarded to the guest.", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
forwards.append(f"hostfwd=tcp::{host_port}-:{guest_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", ",".join(["user,id=net0", *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 installing_from_iso:
|
|
||||||
# q35 has IDE for the installation media; "virt" does not, so use USB.
|
|
||||||
for media_id, media_file, boot_index, ide_bus in (
|
|
||||||
("install_media", install_iso, 2, "ide.1"),
|
|
||||||
("answer_media", seed_iso, 3, "ide.2"),
|
|
||||||
("tools_media", tools_iso, 4, "ide.3")):
|
|
||||||
device = (f"ide-cd,drive={media_id},bus={ide_bus},bootindex={boot_index}"
|
|
||||||
if host_arch == "amd64"
|
|
||||||
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"{label}: localhost:{port}" for label, port, forwarded in forwarded_ports if forwarded),
|
|
||||||
flush=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/
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
This directory contains the cloud-init NoCloud configuration used by
|
This directory contains the cloud-init NoCloud configuration used by
|
||||||
`run-almalinux10.py`.
|
`run-almalinux10.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
|
After changing `user-data`, `meta-data`, or `network-config`, remove the old
|
||||||
`seed.iso` and rebuild it:
|
`seed.iso` and rebuild it:
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,33 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
output="${1:-${script_dir}/seed.iso}"
|
output="${1:-${script_dir}/seed.iso}"
|
||||||
files=(user-data meta-data)
|
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")"
|
mkdir -p "$(dirname -- "$output")"
|
||||||
|
|
||||||
if [[ -f "${script_dir}/network-config" ]]; then
|
if [[ -f "${script_dir}/network-config" ]]; then
|
||||||
@@ -14,22 +41,22 @@ fi
|
|||||||
if command -v cloud-localds >/dev/null 2>&1; then
|
if command -v cloud-localds >/dev/null 2>&1; then
|
||||||
args=()
|
args=()
|
||||||
if [[ -f "${script_dir}/network-config" ]]; then
|
if [[ -f "${script_dir}/network-config" ]]; then
|
||||||
args+=(--network-config="${script_dir}/network-config")
|
args+=(--network-config="${work}/network-config")
|
||||||
fi
|
fi
|
||||||
cloud-localds "${args[@]}" "$output" \
|
cloud-localds "${args[@]}" "${output}" \
|
||||||
"${script_dir}/user-data" "${script_dir}/meta-data"
|
"${work}/user-data" "${work}/meta-data"
|
||||||
elif command -v xorriso >/dev/null 2>&1; then
|
elif command -v xorriso >/dev/null 2>&1; then
|
||||||
xorriso -as mkisofs -quiet -volid cidata -joliet -rock \
|
xorriso -as mkisofs -quiet -volid cidata -joliet -rock \
|
||||||
-output "$output" "${files[@]/#/${script_dir}/}"
|
-output "${output}" "${files[@]/#/${work}/}"
|
||||||
elif command -v genisoimage >/dev/null 2>&1; then
|
elif command -v genisoimage >/dev/null 2>&1; then
|
||||||
genisoimage -quiet -output "$output" -volid cidata -joliet -rock \
|
genisoimage -quiet -output "${output}" -volid cidata -joliet -rock \
|
||||||
"${files[@]/#/${script_dir}/}"
|
"${files[@]/#/${work}/}"
|
||||||
elif command -v mkisofs >/dev/null 2>&1; then
|
elif command -v mkisofs >/dev/null 2>&1; then
|
||||||
mkisofs -quiet -output "$output" -volid cidata -joliet -rock \
|
mkisofs -quiet -output "${output}" -volid cidata -joliet -rock \
|
||||||
"${files[@]/#/${script_dir}/}"
|
"${files[@]/#/${work}/}"
|
||||||
else
|
else
|
||||||
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
|
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'Created %s\n' "$output"
|
printf 'Created %s\n' "${output}"
|
||||||
@@ -10,6 +10,8 @@ users:
|
|||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
sudo: "ALL=(ALL) NOPASSWD: ALL"
|
sudo: "ALL=(ALL) NOPASSWD: ALL"
|
||||||
lock_passwd: false
|
lock_passwd: false
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- @SSH_PUBKEY@
|
||||||
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
|
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
|
||||||
|
|
||||||
chpasswd:
|
chpasswd:
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ This directory contains the cloud-init NoCloud configuration used by
|
|||||||
`run-fedora44.py`. On first boot, cloud-init's package module installs Fedora's
|
`run-fedora44.py`. On first boot, cloud-init's package module installs Fedora's
|
||||||
GNOME package group and reboots the guest.
|
GNOME package group and reboots the guest.
|
||||||
|
|
||||||
|
`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
|
After changing `user-data`, `meta-data`, or `network-config`, remove the old
|
||||||
`seed.iso` and rebuild it:
|
`seed.iso` and rebuild it:
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,58 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
output="${1:-${script_dir}/seed.iso}"
|
output="${1:-${script_dir}/seed.iso}"
|
||||||
files=(user-data meta-data)
|
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")"
|
mkdir -p "$(dirname -- "$output")"
|
||||||
|
|
||||||
if [[ -f "${script_dir}/network-config" ]]; then
|
if [[ -f "${script_dir}/network-config" ]]; then
|
||||||
files+=(network-config)
|
files+=(network-config)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v cloud-localds >/dev/null 2>&1; then
|
if command -v cloud-localds >/dev/null 2>&1; then
|
||||||
args=()
|
args=()
|
||||||
[[ -f "${script_dir}/network-config" ]] && args+=(--network-config="${script_dir}/network-config")
|
if [[ -f "${script_dir}/network-config" ]]; then
|
||||||
cloud-localds "${args[@]}" "$output" "${script_dir}/user-data" "${script_dir}/meta-data"
|
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
|
elif command -v xorriso >/dev/null 2>&1; then
|
||||||
xorriso -as mkisofs -quiet -volid cidata -joliet -rock -output "$output" "${files[@]/#/${script_dir}/}"
|
xorriso -as mkisofs -quiet -volid cidata -joliet -rock \
|
||||||
|
-output "${output}" "${files[@]/#/${work}/}"
|
||||||
elif command -v genisoimage >/dev/null 2>&1; then
|
elif command -v genisoimage >/dev/null 2>&1; then
|
||||||
genisoimage -quiet -output "$output" -volid cidata -joliet -rock "${files[@]/#/${script_dir}/}"
|
genisoimage -quiet -output "${output}" -volid cidata -joliet -rock \
|
||||||
|
"${files[@]/#/${work}/}"
|
||||||
elif command -v mkisofs >/dev/null 2>&1; then
|
elif command -v mkisofs >/dev/null 2>&1; then
|
||||||
mkisofs -quiet -output "$output" -volid cidata -joliet -rock "${files[@]/#/${script_dir}/}"
|
mkisofs -quiet -output "${output}" -volid cidata -joliet -rock \
|
||||||
|
"${files[@]/#/${work}/}"
|
||||||
else
|
else
|
||||||
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
|
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf 'Created %s\n' "$output"
|
printf 'Created %s\n' "${output}"
|
||||||
@@ -10,6 +10,8 @@ users:
|
|||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
sudo: "ALL=(ALL) NOPASSWD: ALL"
|
sudo: "ALL=(ALL) NOPASSWD: ALL"
|
||||||
lock_passwd: false
|
lock_passwd: false
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- @SSH_PUBKEY@
|
||||||
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
|
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
|
||||||
|
|
||||||
chpasswd:
|
chpasswd:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
title Windows update status
|
||||||
|
|
||||||
|
fltmc >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Requesting administrator rights...
|
||||||
|
powershell.exe -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp080-Get-UpdateStatus-Admin.ps1"
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Prints whether the guest is upgrading or downloading updates.
|
||||||
|
# Read-only. Run as Administrator for complete results (BITS jobs of all users,
|
||||||
|
# command lines of system processes); a standard user gets partial output.
|
||||||
|
param(
|
||||||
|
[int]$EventCount = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
|
function Write-Section([string]$Title) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "== $Title =="
|
||||||
|
}
|
||||||
|
|
||||||
|
function Out-Text($Value) {
|
||||||
|
$text = ($Value | Out-String).TrimEnd()
|
||||||
|
if ($text) { Write-Host $text } else { Write-Host '(none)' }
|
||||||
|
}
|
||||||
|
|
||||||
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$isAdmin = [Security.Principal.WindowsPrincipal]::new($identity).IsInRole(
|
||||||
|
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
|
||||||
|
Write-Section 'Host'
|
||||||
|
Write-Host ('Time: {0}' -f (Get-Date))
|
||||||
|
Write-Host ('Admin: {0}' -f $isAdmin)
|
||||||
|
if (-not $isAdmin) {
|
||||||
|
Write-Host 'Warning: not elevated, BITS jobs and process command lines may be incomplete.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Section 'Operating system'
|
||||||
|
Out-Text (Get-CimInstance Win32_OperatingSystem |
|
||||||
|
Select-Object Caption, Version, BuildNumber, LastBootUpTime | Format-List)
|
||||||
|
|
||||||
|
Write-Section 'Update policy'
|
||||||
|
$wuPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
|
||||||
|
$props = 'NoAutoUpdate', 'NoAutoRebootWithLoggedOnUsers', 'ExcludeWUDriversInQualityUpdate',
|
||||||
|
'TargetReleaseVersion', 'TargetReleaseVersionInfo'
|
||||||
|
foreach ($item in @(@{ Path = "$wuPath\AU" }, @{ Path = $wuPath })) {
|
||||||
|
$values = Get-ItemProperty -Path $item.Path -ErrorAction SilentlyContinue
|
||||||
|
foreach ($name in $props) {
|
||||||
|
if ($values -and $values.PSObject.Properties[$name]) {
|
||||||
|
Write-Host ('{0,-34} {1}' -f $name, $values.$name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Section 'Upgrade / pending-reboot markers'
|
||||||
|
$markers = [ordered]@{
|
||||||
|
'C:\$WINDOWS.~BT (upgrade staging)' = Test-Path -LiteralPath 'C:\$WINDOWS.~BT'
|
||||||
|
'C:\Windows\WinSxS\pending.xml' = Test-Path -LiteralPath 'C:\Windows\WinSxS\pending.xml'
|
||||||
|
'WindowsUpdate RebootRequired' = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
|
||||||
|
'Component Based Servicing RebootPending' = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
|
||||||
|
}
|
||||||
|
foreach ($key in $markers.Keys) {
|
||||||
|
Write-Host ('{0,-42} {1}' -f $key, $markers[$key])
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Section 'Update services'
|
||||||
|
Out-Text (Get-Service wuauserv, UsoSvc, TrustedInstaller, bits, WaaSMedicSvc, DoSvc `
|
||||||
|
-ErrorAction SilentlyContinue |
|
||||||
|
Select-Object Name, Status, StartType | Format-Table -AutoSize)
|
||||||
|
|
||||||
|
Write-Section 'Update scheduled tasks'
|
||||||
|
Out-Text (Get-ScheduledTask `
|
||||||
|
-TaskPath '\Microsoft\Windows\UpdateOrchestrator\*', '\Microsoft\Windows\WindowsUpdate\*' `
|
||||||
|
-ErrorAction SilentlyContinue |
|
||||||
|
Select-Object TaskName, State | Format-Table -AutoSize)
|
||||||
|
|
||||||
|
Write-Section 'Update-related processes'
|
||||||
|
$pattern = 'setup|TiWorker|TrustedInstaller|UsoClient|MoSetup|SetupHost|SetupPrep|MusNotification|wusa|dism|EdgeUpdate|Installer'
|
||||||
|
Out-Text (Get-CimInstance Win32_Process |
|
||||||
|
Where-Object { $_.Name -match $pattern -or $_.CommandLine -match 'wuauserv|UsoSvc|DoSvc' } |
|
||||||
|
Select-Object ProcessId, Name, CreationDate, CommandLine | Format-List)
|
||||||
|
|
||||||
|
Write-Section 'BITS download jobs'
|
||||||
|
Out-Text (Get-BitsTransfer -AllUsers -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object DisplayName, JobState, BytesTransferred, BytesTotal | Format-Table -AutoSize)
|
||||||
|
|
||||||
|
Write-Section "Windows Update events (last $EventCount)"
|
||||||
|
Out-Text (Get-WinEvent -LogName 'Microsoft-Windows-WindowsUpdateClient/Operational' `
|
||||||
|
-MaxEvents $EventCount -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object TimeCreated, Id, Message | Format-List)
|
||||||
|
|
||||||
|
Write-Section 'Established HTTP/HTTPS connections by process'
|
||||||
|
Out-Text (Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.RemotePort -in 80, 443 } |
|
||||||
|
Group-Object OwningProcess |
|
||||||
|
ForEach-Object {
|
||||||
|
$process = Get-Process -Id $_.Name -ErrorAction SilentlyContinue
|
||||||
|
[pscustomobject]@{ Pid = $_.Name; Process = $process.Name; Connections = $_.Count }
|
||||||
|
} | Format-Table -AutoSize)
|
||||||
|
|
||||||
|
Write-Section 'Network receive rate (2 samples, 2 s apart)'
|
||||||
|
$counter = Get-Counter '\Network Interface(*)\Bytes Received/sec' `
|
||||||
|
-SampleInterval 2 -MaxSamples 2 -ErrorAction SilentlyContinue
|
||||||
|
if ($counter) {
|
||||||
|
Out-Text ($counter[-1].CounterSamples |
|
||||||
|
Select-Object InstanceName, @{ n = 'KBps'; e = { [int]($_.CookedValue / 1024) } } |
|
||||||
|
Format-Table -AutoSize)
|
||||||
|
} else {
|
||||||
|
Write-Host '(performance counters unavailable)'
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
title Toggle display scale
|
||||||
|
|
||||||
|
rem No argument: switch between 100% and 200%. Pass 100 or 200 to force a value.
|
||||||
|
rem Runs as the current user (no elevation), because the scale is a per-user setting.
|
||||||
|
rem The change takes effect after a restart; the script offers to restart at the end.
|
||||||
|
|
||||||
|
set "SCALE=%~1"
|
||||||
|
if not defined SCALE set "SCALE=0"
|
||||||
|
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp081-Toggle-Scale.ps1" -Scale %SCALE%
|
||||||
|
if errorlevel 1 (
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
choice /c YN /m "Restart now"
|
||||||
|
if errorlevel 2 exit /b 0
|
||||||
|
shutdown.exe /r /t 5
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Switches the display scale between 100% and 200% for the current user.
|
||||||
|
# With no argument it toggles; -Scale 100 or -Scale 200 forces a value.
|
||||||
|
# The scale is stored in the registry (LogPixels), so the change takes effect only after
|
||||||
|
# a restart. Use -Restart to reboot right away.
|
||||||
|
param(
|
||||||
|
[ValidateSet(0, 100, 200)]
|
||||||
|
[int]$Scale = 0,
|
||||||
|
[switch]$Restart
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$desktopKey = 'HKCU:\Control Panel\Desktop'
|
||||||
|
$metricsKey = 'HKCU:\Control Panel\Desktop\WindowMetrics'
|
||||||
|
|
||||||
|
$logPixels = (Get-ItemProperty -Path $desktopKey -Name LogPixels -ErrorAction SilentlyContinue).LogPixels
|
||||||
|
if (-not $logPixels) { $logPixels = 96 }
|
||||||
|
$current = [int][math]::Round($logPixels * 100 / 96)
|
||||||
|
|
||||||
|
$target = if ($Scale -ne 0) { $Scale } elseif ($current -eq 100) { 200 } else { 100 }
|
||||||
|
$dpi = [int]($target * 96 / 100)
|
||||||
|
|
||||||
|
Write-Host "Scale: $current% -> $target%"
|
||||||
|
|
||||||
|
# Win8DpiScaling=1 makes Windows use LogPixels instead of the per-monitor recommendation.
|
||||||
|
Set-ItemProperty -Path $desktopKey -Name LogPixels -Type DWord -Value $dpi
|
||||||
|
Set-ItemProperty -Path $desktopKey -Name Win8DpiScaling -Type DWord -Value 1
|
||||||
|
Set-ItemProperty -Path $metricsKey -Name AppliedDPI -Type DWord -Value $dpi
|
||||||
|
|
||||||
|
Write-Host 'Restart Windows for the new scale to take effect.'
|
||||||
|
if ($Restart) {
|
||||||
|
Write-Host 'Restarting in 5 seconds...'
|
||||||
|
shutdown.exe /r /t 5
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|
||||||
@@ -144,12 +77,14 @@ function Disable-AutomaticUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Copy-InstallerScripts {
|
function Copy-InstallerScripts {
|
||||||
$scripts = @(Get-ChildItem -Path $PSScriptRoot -Filter '*-Install-*.bat' -File)
|
# Numbered scripts (NN-*.bat / NN-*.ps1: installers and tools) go to Desktop\Tools.
|
||||||
|
$scripts = @(Get-ChildItem -Path $PSScriptRoot -File |
|
||||||
|
Where-Object { $_.Name -match '^\d\d-.*\.(bat|ps1)$' })
|
||||||
if ($scripts.Count -eq 0) {
|
if ($scripts.Count -eq 0) {
|
||||||
throw "No installer batch files were found next to $PSCommandPath."
|
throw "No installer batch files were found next to $PSCommandPath."
|
||||||
}
|
}
|
||||||
|
|
||||||
$target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Installers'
|
$target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Tools'
|
||||||
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
||||||
Copy-Item -Path $scripts.FullName -Destination $target -Force
|
Copy-Item -Path $scripts.FullName -Destination $target -Force
|
||||||
}
|
}
|
||||||
@@ -203,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"
|
||||||
)
|
)
|
||||||
@@ -43,9 +43,10 @@ if [[ -n "$ssh_key" ]]; then
|
|||||||
cp -- "$ssh_key" "$work_dir/authorized_keys"
|
cp -- "$ssh_key" "$work_dir/authorized_keys"
|
||||||
seed_files+=("$work_dir/authorized_keys")
|
seed_files+=("$work_dir/authorized_keys")
|
||||||
fi
|
fi
|
||||||
# Numbered installer scripts (10-Install-Git-Admin.bat, 20-..., in install order).
|
# Numbered scripts: installers (10-Install-Git-Admin.bat, 20-..., in install order)
|
||||||
|
# and tools (80-Get-UpdateStatus-Admin.bat/.ps1); all land in Desktop\Tools.
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
seed_files+=("$script_dir"/[0-9][0-9]-*.bat)
|
seed_files+=("$script_dir"/[0-9][0-9]-*.bat "$script_dir"/[0-9][0-9]-*.ps1)
|
||||||
shopt -u nullglob
|
shopt -u nullglob
|
||||||
|
|
||||||
mkdir -p "$(dirname -- "$output")"
|
mkdir -p "$(dirname -- "$output")"
|
||||||
|
|||||||
+1
-1
Submodule vm-images updated: 30342b4e1b...384a4ade9f.
@@ -0,0 +1,96 @@
|
|||||||
|
"""Host port forwards for the QEMU user-mode network, read from a .forwards file.
|
||||||
|
|
||||||
|
Each line of the file is "LABEL HOST_PORT GUEST_PORT"; "#" starts a comment.
|
||||||
|
A QEMU config file section cannot hold more than one hostfwd entry, so the
|
||||||
|
launchers build the -netdev argument from these rules instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
Forward = namedtuple("Forward", "label host_port guest_port")
|
||||||
|
|
||||||
|
|
||||||
|
def _die(message):
|
||||||
|
print(f"Error: {message}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _port(text, where):
|
||||||
|
if not (text.isascii() and text.isdigit() and 1 <= int(text) <= 65535):
|
||||||
|
_die(f"{where}: not a port number from 1 to 65535: {text}")
|
||||||
|
return int(text)
|
||||||
|
|
||||||
|
|
||||||
|
def port_in_use(port):
|
||||||
|
"""True if a host TCP port cannot be bound, as QEMU does for a hostfwd rule."""
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
||||||
|
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
try:
|
||||||
|
probe.bind(("", int(port)))
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def load_forwards(path, host_port_overrides=None):
|
||||||
|
"""Read the rules in a .forwards file.
|
||||||
|
|
||||||
|
host_port_overrides maps a label to a host port that replaces the one in
|
||||||
|
the file, e.g. {"SSH": "2200"}.
|
||||||
|
"""
|
||||||
|
if not path.is_file():
|
||||||
|
_die(f"port forward file was not found: {path}")
|
||||||
|
forwards = []
|
||||||
|
for number, line in enumerate(path.read_text().splitlines(), 1):
|
||||||
|
fields = line.partition("#")[0].split()
|
||||||
|
if not fields:
|
||||||
|
continue
|
||||||
|
where = f"{path}:{number}"
|
||||||
|
if len(fields) != 3:
|
||||||
|
_die(f"{where}: expected LABEL HOST_PORT GUEST_PORT: {line.strip()}")
|
||||||
|
label, host_port, guest_port = fields
|
||||||
|
if any(forward.label == label for forward in forwards):
|
||||||
|
_die(f"{where}: duplicate label: {label}")
|
||||||
|
forwards.append(Forward(label, _port(host_port, where), _port(guest_port, where)))
|
||||||
|
for label, host_port in (host_port_overrides or {}).items():
|
||||||
|
if not any(forward.label == label for forward in forwards):
|
||||||
|
_die(f"{path} has no {label} rule to override the port of")
|
||||||
|
forwards = [forward._replace(host_port=_port(str(host_port), f"{label} port"))
|
||||||
|
if forward.label == label else forward for forward in forwards]
|
||||||
|
seen = set()
|
||||||
|
for forward in forwards:
|
||||||
|
if forward.host_port in seen:
|
||||||
|
_die(f"{path}: host port {forward.host_port} is used by more than one rule")
|
||||||
|
seen.add(forward.host_port)
|
||||||
|
return forwards
|
||||||
|
|
||||||
|
|
||||||
|
def usable_forwards(forwards):
|
||||||
|
"""Drop (with a warning) the rules whose host port is taken.
|
||||||
|
|
||||||
|
QEMU refuses to start when a forwarded host port is taken.
|
||||||
|
"""
|
||||||
|
usable = []
|
||||||
|
for forward in forwards:
|
||||||
|
if port_in_use(forward.host_port):
|
||||||
|
print(f"Warning: host port {forward.host_port} is already in use; "
|
||||||
|
f"{forward.label} will not be forwarded to the guest.", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
usable.append(forward)
|
||||||
|
return usable
|
||||||
|
|
||||||
|
|
||||||
|
def netdev_argument(forwards, netdev_id="net0"):
|
||||||
|
"""The value of -netdev for a user-mode network with the given rules."""
|
||||||
|
return ",".join([f"user,id={netdev_id}", *(
|
||||||
|
f"hostfwd=tcp::{forward.host_port}-:{forward.guest_port}" for forward in forwards)])
|
||||||
|
|
||||||
|
|
||||||
|
def print_forwards(forwards):
|
||||||
|
for forward in forwards:
|
||||||
|
print(f"{forward.label}: localhost:{forward.host_port} -> guest port "
|
||||||
|
f"{forward.guest_port}", flush=True)
|
||||||
|
if forward.guest_port == 22:
|
||||||
|
print(f" ssh -p {forward.host_port} guofang@localhost", flush=True)
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
## 结论
|
## 结论
|
||||||
|
|
||||||
- **根因(第五节)**:HVF 的内核态 GICv3 与 QEMU 的 `usb-storage` 有竞态,Windows Setup 每读一批数据就卡约 10 秒。
|
- **根因(第五节)**:HVF 的内核态 GICv3 与 QEMU 的 `usb-storage` 有竞态,Windows Setup 每读一批数据就卡约 10 秒。
|
||||||
在 `[machine]` 里加 `kernel-irqchip = "off"`(已写入 `qemu/windows11-darwin-arm64.ini`)后,
|
由 `run-windows11.py` 加 `-machine kernel-irqchip=off`(`--install-from-img` 时不加)后,
|
||||||
**同样走 USB 光驱的 `--install-from-iso` 也变快了**,和 UTM 的速度一致。
|
**同样走 USB 光驱的 `--install-from-iso` 也变快了**,和 UTM 的速度一致。
|
||||||
- 下面第一到五节是更早的分析:当时把慢归因于"模拟 USB 光驱吞吐低"(平均约 4 MB/s),
|
- 下面第一到五节是更早的分析:当时把慢归因于"模拟 USB 光驱吞吐低"(平均约 4 MB/s),
|
||||||
这个归因不准确,真实原因是上面的 10 秒停顿;那时测到的低吞吐就是停顿拉低的平均值。
|
这个归因不准确,真实原因是上面的 10 秒停顿;那时测到的低吞吐就是停顿拉低的平均值。
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 主机 | Apple M5 Max,arm64,macOS 26.4,18 核,48 GB 内存 |
|
| 主机 | Apple M5 Max,arm64,macOS 26.4,18 核,48 GB 内存 |
|
||||||
| QEMU | 11.1.2,`qemu-system-aarch64`,加速器 HVF |
|
| QEMU | 11.1.2,`qemu-system-aarch64`,加速器 HVF |
|
||||||
| 客户机配置 | `qemu/windows11.ini` + `qemu/windows11-darwin-arm64.ini`(`virt` 机型,8 vCPU,8G 内存,NVMe 系统盘,virtio-net,ramfb,usb-kbd/usb-tablet) |
|
| 客户机配置 | `qemu/windows11/config.ini` + `qemu/windows11/darwin-arm64.ini`(`virt` 机型,8 vCPU,8G 内存,NVMe 系统盘,virtio-net,ramfb,usb-kbd/usb-tablet) |
|
||||||
| 安装 ISO | `Windows11_Client_arm64_en-us_26300_9457.iso`(卷标 `CCCOMA_A64FRE_EN-US_DV9`,`install.wim` 7.6 GB,`boot.wim` 630 MB) |
|
| 安装 ISO | `Windows11_Client_arm64_en-us_26300_9457.iso`(卷标 `CCCOMA_A64FRE_EN-US_DV9`,`install.wim` 7.6 GB,`boot.wim` 630 MB) |
|
||||||
| 驱动 ISO | `virtio-win.iso`(含 ARM64 驱动目录,根目录只有 x64/x86 的 MSI) |
|
| 驱动 ISO | `virtio-win.iso`(含 ARM64 驱动目录,根目录只有 x64/x86 的 MSI) |
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ UTM 5.0.8 带有这个修复;Homebrew 的 QEMU 11.1.2 没有,所以上游版
|
|||||||
|
|
||||||
### 处理
|
### 处理
|
||||||
|
|
||||||
在 `qemu/windows11-darwin-arm64.ini` 的 `[machine]` 加 `kernel-irqchip = "off"`,改用模拟的中断控制器。
|
在 `qemu/windows11/darwin-arm64.ini` 的 `[machine]` 加 `kernel-irqchip = "off"`,改用模拟的中断控制器。
|
||||||
这一项改完后速度明显提升,已确认和 UTM 一致。
|
这一项改完后速度明显提升,已确认和 UTM 一致。
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
|
|||||||
Reference in new issue
Block a user