Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48c4477ec4 | ||
|
|
a920ba0ca5 | ||
|
|
fe7250870e |
No files matched your search
@@ -48,8 +48,7 @@ corresponding `*_STATE_DIR` environment variable:
|
||||
|
||||
With `--download-only`, a launcher only downloads the media it would fetch
|
||||
automatically and then exits without creating a VM disk or starting QEMU: the
|
||||
base image for Debian, AlmaLinux, Fedora and Ubuntu, and the VirtIO tools ISO
|
||||
for Windows 11 (whose installation ISO and base QCOW2 are supplied manually):
|
||||
base image for Debian, AlmaLinux, Fedora and Ubuntu:
|
||||
|
||||
```bash
|
||||
./run-fedora44.py --download-only
|
||||
@@ -165,43 +164,49 @@ from the shared Windows 11 QCOW2 base image:
|
||||
./run-windows11.py
|
||||
```
|
||||
|
||||
It creates a persistent 64G disk at `vm/windows11/disk.qcow2`, automatically
|
||||
partitions it, completes OOBE, creates the local administrator `guofang` with
|
||||
the initial password `wang`, and enables Remote Desktop on
|
||||
`0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on the host itself). During the first login it also silently installs the VirtIO
|
||||
drivers (the guest tools installer on amd64, the Arm64 driver packages from the
|
||||
VirtIO ISO with `pnputil` on arm64), without which the guest has no network on
|
||||
arm64. The installer includes the VirtIO drivers, SPICE vdagent, and QEMU Guest
|
||||
Agent. The launcher exposes the vdagent channel for host/guest clipboard
|
||||
integration and the QEMU Guest Agent at `vm/windows11/qemu-guest-agent.sock`.
|
||||
Change the password after first login. The OpenSSH Server is installed during
|
||||
the first logon from the Win32-OpenSSH zip for the host architecture, which the
|
||||
seed ISO carries (downloaded by `vm-images/download-images.sh openssh-win`), so
|
||||
Windows Update is not needed. Connect with `ssh guofang@localhost -p 2222`; the
|
||||
seed ISO also carries the host's SSH public key (`$SSH_PUBLIC_KEY_FILE`, else the
|
||||
first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, `id_rsa.pub`) for passwordless
|
||||
administrator login.
|
||||
It creates a persistent 64G disk at `vm/windows11/disk.qcow2` as a QCOW2
|
||||
overlay on the base image and nothing else: the launcher only creates the
|
||||
differential disk and starts QEMU. The base image is generic and holds nothing
|
||||
but Windows (see `vm-images/README.md`): no account, no SSH, no VirtIO or other
|
||||
hardware-specific drivers, no machine identity. Like a cloud image, each VM is
|
||||
configured on its first boot, from the seed ISO.
|
||||
|
||||
Whenever the disk does not exist yet, the launcher also mounts the seed ISO
|
||||
`vm/windows11/seed.iso` (beside the disk; built from `seediso/windows11/` when
|
||||
missing or outdated, with the OpenSSH zip and the host's SSH public key,
|
||||
`$SSH_PUBLIC_KEY_FILE` or the first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`,
|
||||
`id_rsa.pub`). Windows reads the `Unattend.xml` in the root of that CD-ROM
|
||||
during its specialize and OOBE passes: host name `WIN11`, time zone, Remote
|
||||
Desktop on `0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on
|
||||
the host itself), the local administrator `guofang` (initial password `wang`;
|
||||
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.
|
||||
|
||||
Because the image has no VirtIO drivers, the VM only uses devices that Windows
|
||||
has inbox drivers for: an IDE disk and e1000e NIC on amd64, an NVMe disk, e1000e
|
||||
NIC and ramfb display on arm64 (a fixed-size framebuffer there). The SPICE
|
||||
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)
|
||||
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
|
||||
`vm/windows11/qemu.ini` and loads both with `-readconfig`. Only the network
|
||||
(see Port forwards) and the installation media are still set by the launcher. QEMU arguments after
|
||||
(see Port forwards) and the seed ISO are still set by the launcher. QEMU arguments after
|
||||
`--` override the config files, for example `./run-windows11.py -- -m 4G -smp 4`.
|
||||
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
|
||||
--install-from-iso`. This choice only applies while creating a missing system
|
||||
disk; subsequent starts always boot the existing system disk normally. During
|
||||
ISO installation, the launcher resolves its tools ISO through the shared image
|
||||
helper and downloads it when missing. Override the base QCOW2, source ISO,
|
||||
tools ISO, RDP port, or SSH port with the `WIN11_VM_` environment
|
||||
variables shown by `./run-windows11.py --help`. For example, when the VirtIO ISO
|
||||
is elsewhere:
|
||||
|
||||
```bash
|
||||
WIN11_VM_TOOLS_ISO=/custom/path/virtio-win.iso ./run-windows11.py --install-from-iso
|
||||
```
|
||||
The launcher has no installation options and stops with a hint if the base image
|
||||
is missing. Make the base image once with `vm-images/build-windows-base.sh`,
|
||||
which is self-contained in `vm-images/` (its own answer file, QEMU config and
|
||||
installation logic, written in bash): it installs Windows unattended from the
|
||||
ISO into a scratch disk, runs `sysprep` as its last step and stores the disk as
|
||||
the read-only base image. Override the base QCOW2, seed ISO, RDP port, or SSH
|
||||
port with the `WIN11_VM_` environment variables shown by
|
||||
`./run-windows11.py --help`.
|
||||
|
||||
## Port forwards
|
||||
|
||||
|
||||
@@ -39,17 +39,6 @@
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "disk0"
|
||||
|
||||
# cloud-init seed ISO.
|
||||
[drive "seed0"]
|
||||
if = "none"
|
||||
format = "raw"
|
||||
readonly = "on"
|
||||
file = "${seed_image}"
|
||||
|
||||
[device "seed0-dev"]
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "seed0"
|
||||
|
||||
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||
# defines with -netdev from qemu/debian13/forwards.conf because a config file
|
||||
# section cannot hold more than one hostfwd.
|
||||
|
||||
@@ -39,17 +39,6 @@
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "disk0"
|
||||
|
||||
# cloud-init seed ISO.
|
||||
[drive "seed0"]
|
||||
if = "none"
|
||||
format = "raw"
|
||||
readonly = "on"
|
||||
file = "${seed_image}"
|
||||
|
||||
[device "seed0-dev"]
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "seed0"
|
||||
|
||||
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||
# defines with -netdev from qemu/fedora44/forwards.conf because a config file
|
||||
# section cannot hold more than one hostfwd.
|
||||
|
||||
@@ -39,17 +39,6 @@
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "disk0"
|
||||
|
||||
# cloud-init seed ISO.
|
||||
[drive "seed0"]
|
||||
if = "none"
|
||||
format = "raw"
|
||||
readonly = "on"
|
||||
file = "${seed_image}"
|
||||
|
||||
[device "seed0-dev"]
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "seed0"
|
||||
|
||||
# The NIC is attached to the user-mode network "net0", which the launcher
|
||||
# defines with -netdev from qemu/ubuntu2604/forwards.conf because a config file
|
||||
# section cannot hold more than one hostfwd.
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# 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
|
||||
# have no -readconfig equivalent (-cpu, -display) and the installation media
|
||||
# attached by --install-from-iso are still added by the launcher.
|
||||
# have no -readconfig equivalent (-cpu, -display) and the network are still added
|
||||
# by the launcher.
|
||||
|
||||
[name]
|
||||
guest = "windows11"
|
||||
|
||||
@@ -7,35 +7,27 @@
|
||||
[accel]
|
||||
accel = "tcg"
|
||||
|
||||
# run-windows11.py adds kernel-irqchip=off unless installing with
|
||||
# --install-from-img (see there).
|
||||
# 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 of --install-from-iso) both have inbox Windows drivers.
|
||||
# installation media) 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.
|
||||
# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image
|
||||
# contains no VirtIO drivers, so the network card is an Intel e1000e and the
|
||||
# display is ramfb (a fixed-size framebuffer).
|
||||
[device "nic0"]
|
||||
driver = "virtio-net-pci"
|
||||
driver = "e1000e"
|
||||
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"
|
||||
driver = "ramfb"
|
||||
|
||||
[device "kbd0"]
|
||||
driver = "usb-kbd"
|
||||
@@ -11,8 +11,7 @@
|
||||
type = "q35"
|
||||
|
||||
# The system disk is attached through IDE so that Windows can boot before the
|
||||
# VirtIO storage driver is installed; the installation media of
|
||||
# --install-from-iso use ide.1 to ide.3.
|
||||
# VirtIO storage driver is installed.
|
||||
[device "system_disk-dev"]
|
||||
driver = "ide-hd"
|
||||
drive = "system_disk"
|
||||
|
||||
@@ -11,28 +11,22 @@
|
||||
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.
|
||||
# installation media) 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.
|
||||
# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image
|
||||
# contains no VirtIO drivers, so the network card is an Intel e1000e and the
|
||||
# display is ramfb (a fixed-size framebuffer).
|
||||
[device "nic0"]
|
||||
driver = "virtio-net-pci"
|
||||
driver = "e1000e"
|
||||
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"
|
||||
driver = "ramfb"
|
||||
|
||||
[device "kbd0"]
|
||||
driver = "usb-kbd"
|
||||
+10
-4
@@ -208,7 +208,8 @@ 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}")
|
||||
|
||||
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"
|
||||
if not is_executable(seed_builder):
|
||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||
@@ -240,7 +241,7 @@ if pid_file.is_file():
|
||||
die(f"the AlmaLinux virtual machine is already running (PID {old_pid}).")
|
||||
pid_file.unlink(missing_ok=True)
|
||||
|
||||
if not disk_image.is_file():
|
||||
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}")
|
||||
@@ -261,12 +262,17 @@ qemu_args = [
|
||||
"-drive", f"if=pflash,format=raw,file={uefi_vars}",
|
||||
"-drive", f"file={disk_image},if=none,id=disk0,format=qcow2",
|
||||
"-device", "virtio-blk-pci,drive=disk0",
|
||||
"-drive", f"file={seed_image},if=none,id=seed0,format=raw,readonly=on",
|
||||
"-device", "virtio-blk-pci,drive=seed0",
|
||||
"-netdev", vmforwards.netdev_argument(forwards),
|
||||
"-device", "virtio-net-pci,netdev=net0",
|
||||
]
|
||||
|
||||
if creating:
|
||||
# cloud-init reads the seed ISO on the first boot only (the users, keys,
|
||||
# network and packages it sets up stay on the disk), so it is attached only
|
||||
# when the disk is created.
|
||||
qemu_args += ["-drive", f"file={seed_image},if=none,id=seed0,format=raw,readonly=on",
|
||||
"-device", "virtio-blk-pci,drive=seed0"]
|
||||
|
||||
if machine_type == "virt":
|
||||
# Unlike q35, the "virt" machine has no default GPU or USB input devices.
|
||||
qemu_args += [
|
||||
|
||||
+10
-3
@@ -208,7 +208,8 @@ 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}")
|
||||
|
||||
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"
|
||||
if not is_executable(seed_builder):
|
||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||
@@ -240,7 +241,7 @@ if pid_file.is_file():
|
||||
die(f"the virtual machine is already running (PID {old_pid}).")
|
||||
pid_file.unlink(missing_ok=True)
|
||||
|
||||
if not disk_image.is_file():
|
||||
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}")
|
||||
@@ -254,7 +255,6 @@ config_values = {
|
||||
"uefi_code": uefi_code_flash,
|
||||
"uefi_vars": uefi_vars,
|
||||
"disk_image": disk_image,
|
||||
"seed_image": seed_image,
|
||||
}
|
||||
for key, value in config_values.items():
|
||||
# QEMU's config file parser has no escape syntax for quoted values.
|
||||
@@ -285,6 +285,13 @@ qemu_args = [
|
||||
"-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} "
|
||||
f"and {qemu_platform_config}.", flush=True)
|
||||
vmforwards.print_forwards(forwards)
|
||||
|
||||
+10
-3
@@ -210,7 +210,8 @@ for firmware_file in (uefi_code, uefi_vars_template):
|
||||
if not firmware_file.is_file():
|
||||
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"
|
||||
if not is_executable(seed_builder):
|
||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||
@@ -242,7 +243,7 @@ if pid_file.is_file():
|
||||
die(f"the Fedora virtual machine is already running (PID {old_pid}).")
|
||||
pid_file.unlink(missing_ok=True)
|
||||
|
||||
if not disk_image.is_file():
|
||||
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}")
|
||||
@@ -256,7 +257,6 @@ config_values = {
|
||||
"uefi_code": uefi_code_flash,
|
||||
"uefi_vars": uefi_vars,
|
||||
"disk_image": disk_image,
|
||||
"seed_image": seed_image,
|
||||
}
|
||||
for key, value in config_values.items():
|
||||
# QEMU's config file parser has no escape syntax for quoted values.
|
||||
@@ -287,6 +287,13 @@ qemu_args = [
|
||||
"-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} "
|
||||
f"and {qemu_platform_config}.", flush=True)
|
||||
vmforwards.print_forwards(forwards)
|
||||
|
||||
+10
-3
@@ -210,7 +210,8 @@ for firmware_file in (uefi_code, uefi_vars_template):
|
||||
if not firmware_file.is_file():
|
||||
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"
|
||||
if not is_executable(seed_builder):
|
||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||
@@ -242,7 +243,7 @@ if pid_file.is_file():
|
||||
die(f"the Ubuntu virtual machine is already running (PID {old_pid}).")
|
||||
pid_file.unlink(missing_ok=True)
|
||||
|
||||
if not disk_image.is_file():
|
||||
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}")
|
||||
@@ -256,7 +257,6 @@ config_values = {
|
||||
"uefi_code": uefi_code_flash,
|
||||
"uefi_vars": uefi_vars,
|
||||
"disk_image": disk_image,
|
||||
"seed_image": seed_image,
|
||||
}
|
||||
for key, value in config_values.items():
|
||||
# QEMU's config file parser has no escape syntax for quoted values.
|
||||
@@ -287,6 +287,13 @@ qemu_args = [
|
||||
"-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} "
|
||||
f"and {qemu_platform_config}.", flush=True)
|
||||
vmforwards.print_forwards(forwards)
|
||||
|
||||
+296
-399
@@ -51,16 +51,6 @@ def find_image_downloader():
|
||||
return image_downloader
|
||||
|
||||
|
||||
state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / "vm" / "windows11")
|
||||
disk_size = env("WIN11_VM_DISK_SIZE") or "64G"
|
||||
rdp_port = env("WIN11_VM_RDP_PORT")
|
||||
ssh_port = env("WIN11_VM_SSH_PORT")
|
||||
forwards_file = Path(
|
||||
env("WIN11_VM_FORWARDS") or script_dir / "qemu" / "windows11" / "forwards.conf")
|
||||
qemu_config_template = Path(
|
||||
env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11" / "config.ini")
|
||||
|
||||
|
||||
def normalize_arch(arch):
|
||||
if arch in ("arm64", "aarch64"):
|
||||
return "arm64"
|
||||
@@ -69,142 +59,6 @@ def normalize_arch(arch):
|
||||
die(f"unsupported architecture: {arch}")
|
||||
|
||||
|
||||
daemonize = False
|
||||
install_from_iso = False
|
||||
install_from_img = False
|
||||
download_only = False
|
||||
seed_only = False
|
||||
extra_args = []
|
||||
arch_option = None
|
||||
vnc_listen = None
|
||||
|
||||
|
||||
def usage(stream=sys.stdout):
|
||||
stream.write(f"""\
|
||||
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--seed-only] [--vnc[=ADDR:N]] [--install-from-iso | --install-from-img]
|
||||
[--arch ARCH] [--state-dir DIR] [--rdp-port PORT] [--ssh-port PORT] [-- QEMU_ARGUMENT ...]
|
||||
|
||||
If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from
|
||||
the Windows 11 QCOW2 base image.
|
||||
The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in
|
||||
qemu/windows11/config.ini, which is rendered to qemu.ini in the state directory and
|
||||
loaded with QEMU's -readconfig, followed by qemu/windows11/<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and
|
||||
architecture, --arch or VM_IMAGE_ARCH). Later QEMU arguments override them, e.g. -- -m 4G -smp 4.
|
||||
Use --install-from-iso to perform an automatic ISO installation instead, or
|
||||
--install-from-img to install from a disk image built from the ISO.
|
||||
If it exists, start Windows normally without attaching installation media.
|
||||
|
||||
Options:
|
||||
--daemon Run QEMU in the background
|
||||
--download-only Download the VirtIO tools ISO and the Windows 11 ISO
|
||||
(for --arch) if they are missing, then exit; needs
|
||||
Node.js and Chromium for the latter, see vm-images/README.md
|
||||
(the base QCOW2 is supplied manually)
|
||||
--seed-only Build the seed ISO (answer file and setup scripts) and exit,
|
||||
without starting the VM or fetching the Windows and VirtIO ISOs
|
||||
--install-from-iso Install from ISO when the system disk is missing
|
||||
--install-from-img Like --install-from-iso, but install from the disk image
|
||||
made by vm-images/build-windows-install-disk.sh, which
|
||||
is the ISO path ending in .img instead of .iso.
|
||||
It is attached as an NVMe disk, much faster than the
|
||||
emulated USB CD-ROM, and is never modified
|
||||
--vnc[=ADDR:N] Serve the screen over VNC instead of opening a window, on
|
||||
ADDR (default: 127.0.0.1) at TCP port 5900+N (default N: 0).
|
||||
Overrides WIN11_VM_DISPLAY
|
||||
--arch ARCH Guest architecture: arm64 or amd64 (default: the host's;
|
||||
overrides VM_IMAGE_ARCH). A guest that differs from the
|
||||
host runs under slow TCG emulation
|
||||
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
||||
(default: vm/windows11; overrides WIN11_VM_STATE_DIR)
|
||||
--rdp-port PORT Host port of the RDP rule in the port forward file
|
||||
(default: 3389; overrides WIN11_VM_RDP_PORT)
|
||||
--ssh-port PORT Host port of the SSH rule in the port forward file
|
||||
(default: 2222; overrides WIN11_VM_SSH_PORT)
|
||||
-h, --help Show this help
|
||||
|
||||
Environment variables:
|
||||
WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images)
|
||||
WIN11_VM_INSTALL_ISO Windows 11 installation ISO used with --install-from-iso
|
||||
(--install-from-img uses the same path ending in .img)
|
||||
WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper)
|
||||
WIN11_VM_RDP_PORT Host RDP port (default: from the port forward file, 3389)
|
||||
WIN11_VM_SSH_PORT Host SSH port (default: from the port forward file, 2222)
|
||||
WIN11_VM_FORWARDS Port forward file (default: qemu/windows11/forwards.conf)
|
||||
WIN11_VM_STATE_DIR Runtime directory (default: vm/windows11)
|
||||
WIN11_VM_DISK_IMAGE System disk path
|
||||
WIN11_VM_DISK_SIZE New system disk size (default: 64G)
|
||||
WIN11_VM_SEED_ISO Answer-file ISO path (default: alongside the VM disk)
|
||||
WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset)
|
||||
WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset)
|
||||
WIN11_VM_QEMU_CONFIG QEMU config template passed to -readconfig
|
||||
(default: qemu/windows11/config.ini)
|
||||
WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS,
|
||||
gtk elsewhere)
|
||||
""")
|
||||
|
||||
|
||||
args = sys.argv[1:]
|
||||
while args:
|
||||
arg = args.pop(0)
|
||||
if arg == "--daemon":
|
||||
daemonize = True
|
||||
elif arg == "--download-only":
|
||||
download_only = True
|
||||
elif arg == "--seed-only":
|
||||
seed_only = True
|
||||
elif arg == "--vnc" or arg.startswith("--vnc="):
|
||||
vnc_listen = arg.partition("=")[2] or "127.0.0.1:0"
|
||||
if ":" not in vnc_listen:
|
||||
vnc_listen += ":0"
|
||||
elif arg == "--install-from-iso":
|
||||
install_from_iso = True
|
||||
elif arg == "--install-from-img":
|
||||
install_from_img = True
|
||||
elif arg == "--state-dir":
|
||||
if not args:
|
||||
die(f"{arg} requires a directory argument.", 2)
|
||||
state_dir = Path(args.pop(0))
|
||||
elif arg.startswith("--state-dir="):
|
||||
state_dir = Path(arg.split("=", 1)[1])
|
||||
elif arg in ("--rdp-port", "--ssh-port") or arg.startswith(("--rdp-port=", "--ssh-port=")):
|
||||
name, sep, value = arg.partition("=")
|
||||
if not sep:
|
||||
if not args:
|
||||
die(f"{name} requires a port argument.", 2)
|
||||
value = args.pop(0)
|
||||
if not (value.isascii() and value.isdigit() and 1 <= int(value) <= 65535):
|
||||
die(f"{name} must be a port number from 1 to 65535: {value}", 2)
|
||||
if name == "--rdp-port":
|
||||
rdp_port = value
|
||||
else:
|
||||
ssh_port = value
|
||||
elif arg == "--arch":
|
||||
if not args:
|
||||
die(f"{arg} requires an architecture argument.", 2)
|
||||
arch_option = args.pop(0)
|
||||
elif arg.startswith("--arch="):
|
||||
arch_option = arg.split("=", 1)[1]
|
||||
elif arg in ("-h", "--help"):
|
||||
usage()
|
||||
sys.exit(0)
|
||||
elif arg == "--":
|
||||
extra_args.extend(args)
|
||||
break
|
||||
else:
|
||||
print(f"Error: unknown option: {arg}", file=sys.stderr)
|
||||
usage(sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
host_arch = normalize_arch(arch_option or env("VM_IMAGE_ARCH") or platform.machine())
|
||||
# The shared image helper and build-seed.sh pick the architecture from here.
|
||||
os.environ["VM_IMAGE_ARCH"] = host_arch
|
||||
qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
|
||||
host_os = platform.system().lower()
|
||||
if host_os == "darwin" and host_arch == "amd64":
|
||||
die("amd64 guests are not supported on macOS hosts; use an arm64 image")
|
||||
qemu_platform_config = script_dir / "qemu" / "windows11" / f"{host_os}-{host_arch}.ini"
|
||||
|
||||
|
||||
def pick_firmware(override, *candidates):
|
||||
if override:
|
||||
return Path(override)
|
||||
@@ -214,15 +68,148 @@ def pick_firmware(override, *candidates):
|
||||
return Path(candidates[0])
|
||||
|
||||
|
||||
if host_arch == "arm64":
|
||||
uefi_code = pick_firmware(
|
||||
class Options:
|
||||
"""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 (no account, no SSH, no VirtIO or other hardware-specific drivers), so
|
||||
the devices in qemu/windows11 are ones Windows has inbox drivers for.
|
||||
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"),
|
||||
"/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(
|
||||
self.uefi_vars_template = pick_firmware(
|
||||
env("WIN11_VM_UEFI_VARS"),
|
||||
"/usr/share/AAVMF/AAVMF_VARS.fd",
|
||||
"/usr/share/edk2/aarch64/vars-template-pflash.raw",
|
||||
@@ -230,64 +217,147 @@ if host_arch == "arm64":
|
||||
"/usr/local/share/qemu/edk2-arm-vars.fd",
|
||||
)
|
||||
else:
|
||||
uefi_code = pick_firmware(
|
||||
self.uefi_code = pick_firmware(
|
||||
env("WIN11_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(
|
||||
self.uefi_vars_template = pick_firmware(
|
||||
env("WIN11_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",
|
||||
)
|
||||
if install_from_img:
|
||||
install_from_iso = True
|
||||
|
||||
if download_only:
|
||||
# The VirtIO tools ISO and the Windows installation ISO are the media the
|
||||
# launcher downloads itself; the base QCOW2 is supplied manually.
|
||||
image_downloader = None
|
||||
if env("WIN11_VM_TOOLS_ISO"):
|
||||
tools_iso = Path(env("WIN11_VM_TOOLS_ISO"))
|
||||
if not tools_iso.is_file():
|
||||
die(f"VirtIO Windows tools ISO was not found: {tools_iso}")
|
||||
|
||||
class Runtime:
|
||||
"""Paths of the files a VM keeps in its state directory."""
|
||||
|
||||
def __init__(self, state_dir):
|
||||
self.state_dir = state_dir
|
||||
self.disk_image = Path(env("WIN11_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
|
||||
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:
|
||||
image_downloader = find_image_downloader()
|
||||
tools_iso = Path(capture([str(image_downloader), "--print-path", "virtio-win"]))
|
||||
if not tools_iso.is_file():
|
||||
run([str(image_downloader), "virtio-win"])
|
||||
if not tools_iso.is_file():
|
||||
die(f"downloader did not create: {tools_iso}")
|
||||
print(f"VirtIO Windows tools ISO is ready: {tools_iso}")
|
||||
if env("WIN11_VM_INSTALL_ISO"):
|
||||
install_iso = Path(env("WIN11_VM_INSTALL_ISO"))
|
||||
if not install_iso.is_file():
|
||||
die(f"Windows 11 installation ISO was not found: {install_iso}")
|
||||
die(f"Windows 11 VM is already running (PID {old_pid}).")
|
||||
rt.pid_file.unlink(missing_ok=True)
|
||||
rt.monitor_socket.unlink(missing_ok=True)
|
||||
rt.guest_agent_socket.unlink(missing_ok=True)
|
||||
if not rt.uefi_vars.is_file():
|
||||
shutil.copyfile(plat.uefi_vars_template, rt.uefi_vars)
|
||||
|
||||
|
||||
def display_backend(vnc_listen):
|
||||
"""cocoa shows the guest at its own resolution. Append ,zoom-to-fit=on (via
|
||||
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:
|
||||
image_downloader = image_downloader or find_image_downloader()
|
||||
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"
|
||||
os.execvp(plat.qemu_binary, [plat.qemu_binary, *qemu_args, *extra_args])
|
||||
|
||||
|
||||
def build_seed_iso(image_downloader, force=False):
|
||||
"""Build the seed ISO, unless it is newer than all of its sources and not forced."""
|
||||
def build_seed_iso(seed_iso):
|
||||
"""Build the seed ISO, unless it is newer than all of its sources."""
|
||||
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]-*.ps1"))]
|
||||
|
||||
@@ -300,222 +370,49 @@ def build_seed_iso(image_downloader, force=False):
|
||||
|
||||
# The seed carries the OpenSSH zip for this architecture; build-seed.sh
|
||||
# downloads it when missing.
|
||||
seed_sources.append(Path(capture(
|
||||
[str(image_downloader), "--print-path", "openssh-win"])))
|
||||
openssh_zip = Path(capture(
|
||||
[str(find_image_downloader()), "--print-path", "openssh-win"]))
|
||||
seed_sources.append(openssh_zip)
|
||||
|
||||
def newer_than_seed(source):
|
||||
# Mirrors bash `-nt`: true if source exists and is newer than seed_iso.
|
||||
return source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime
|
||||
|
||||
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 not seed_iso.is_file() or not openssh_zip.is_file() or any(
|
||||
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 seed_only:
|
||||
build_seed_iso(find_image_downloader(), force=True)
|
||||
print(f"Seed ISO is ready: {seed_iso}")
|
||||
sys.exit(0)
|
||||
options = parse_args(sys.argv[1:])
|
||||
plat = Platform(options.arch_option)
|
||||
rt = Runtime(options.state_dir)
|
||||
disk_size = env("WIN11_VM_DISK_SIZE") or "64G"
|
||||
|
||||
installing = False
|
||||
installing_from_iso = False
|
||||
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:
|
||||
creating = not rt.disk_image.is_file()
|
||||
if creating:
|
||||
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:
|
||||
die(f"{qemu_binary} is not installed.")
|
||||
if installing:
|
||||
if shutil.which("qemu-img") is None:
|
||||
die("qemu-img is not installed.")
|
||||
if installing_from_iso:
|
||||
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:
|
||||
check_tools_and_firmware(plat, need_qemu_img=creating)
|
||||
prepare_runtime(plat, rt)
|
||||
if creating:
|
||||
seed_iso = Path(env("WIN11_VM_SEED_ISO") or rt.disk_image.parent / "seed.iso")
|
||||
build_seed_iso(seed_iso)
|
||||
if creating:
|
||||
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
||||
"-b", str(base_image), str(disk_image), disk_size])
|
||||
print(f"Created Windows 11 system disk: {disk_image}")
|
||||
"-b", str(base_image), str(rt.disk_image), disk_size])
|
||||
print(f"Created Windows 11 system disk: {rt.disk_image}")
|
||||
|
||||
# cocoa shows the guest at its own resolution. Append ,zoom-to-fit=on (via
|
||||
# WIN11_VM_DISPLAY) to stretch a low-resolution screen to the window.
|
||||
display_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)}")
|
||||
|
||||
# Most of the VM definition lives in the QEMU config template; fill in the
|
||||
# file paths and hand the rendered file to QEMU with -readconfig.
|
||||
config_values = {
|
||||
"uefi_code": uefi_code,
|
||||
"uefi_vars": uefi_vars,
|
||||
"disk_image": disk_image,
|
||||
"guest_agent_socket": guest_agent_socket,
|
||||
"monitor_socket": monitor_socket,
|
||||
}
|
||||
for key, value in config_values.items():
|
||||
# QEMU's config file parser has no escape syntax for quoted values.
|
||||
if any(char in str(value) for char in '"\n'):
|
||||
die(f"{key} must not contain double quotes or newlines: {value}")
|
||||
for config_file in (qemu_config_template, qemu_platform_config):
|
||||
if not config_file.is_file():
|
||||
die(f"QEMU config file was not found: {config_file}")
|
||||
try:
|
||||
qemu_config_text = string.Template(qemu_config_template.read_text()).substitute(
|
||||
{key: str(value) for key, value in config_values.items()})
|
||||
except (KeyError, ValueError) as error:
|
||||
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
||||
qemu_config.write_text(qemu_config_text)
|
||||
|
||||
# Each rule of the port forward file is one hostfwd entry of the network.
|
||||
forwards = vmforwards.usable_forwards(vmforwards.load_forwards(
|
||||
forwards_file, {label: port for label, port in (("RDP", rdp_port), ("SSH", ssh_port)) if port}))
|
||||
|
||||
# Neither Windows Setup nor the base image has the viogpudo driver yet, and
|
||||
# without it the virtio-gpu in the config file shows "display output is not
|
||||
# active". ramfb shows the screen until Configure-Windows.ps1 installs it. It
|
||||
# must be created before the virtio-gpu: the display window shows the first GPU.
|
||||
qemu_args = ["-device", "ramfb"] if installing and host_arch == "arm64" else []
|
||||
qemu_args += [
|
||||
"-readconfig", str(qemu_config),
|
||||
"-readconfig", str(qemu_platform_config),
|
||||
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
||||
"-cpu", "max",
|
||||
# A config file section cannot hold two hostfwd entries, so the network
|
||||
# that the NIC in the config file uses is defined here. -nic none stops
|
||||
# QEMU from adding its default NIC as well.
|
||||
"-netdev", vmforwards.netdev_argument(forwards),
|
||||
"-nic", "none",
|
||||
"-display", display_backend,
|
||||
# "-display", "none", "-spice", "port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off", "-device", "virtio-vga",
|
||||
]
|
||||
if host_os == "darwin" and host_arch == "arm64" and not install_from_img:
|
||||
# With the HVF in-kernel GICv3, QEMU's usb-storage races on the status
|
||||
# packet and Windows Setup stalls 10 s after every burst of reads
|
||||
# (utmapp/UTM#7970), so use the emulated interrupt controller; upstream
|
||||
# QEMU only gets the fix from the UTM fork. The NVMe install disk of
|
||||
# --install-from-img does not use usb-storage, and hangs with the emulated
|
||||
# controller.
|
||||
qemu_args, forwards = common_qemu_args(plat, rt, options)
|
||||
if plat.os == "darwin" and plat.arch == "arm64":
|
||||
# The HVF in-kernel GICv3 stalls Windows on its USB and NVMe devices
|
||||
# (utmapp/UTM#7970), so use the emulated interrupt controller.
|
||||
qemu_args += ["-machine", "kernel-irqchip=off"]
|
||||
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"{forward.label}: 0.0.0.0:{forward.host_port}" for forward in forwards),
|
||||
flush=True)
|
||||
|
||||
if installing_from_iso and not install_from_img:
|
||||
# The Windows ISO boots through bootmgr, which waits for a key ("Press any
|
||||
# key to boot from CD or DVD...") and gives up after a few seconds. Nobody
|
||||
# is there to press it, so a detached helper taps the space bar through the
|
||||
# QEMU monitor for the first 30 s. It exits by itself; later key presses
|
||||
# are harmless because Setup is unattended.
|
||||
key_script = (
|
||||
"import socket, sys, time\n"
|
||||
"path = sys.argv[1]\n"
|
||||
"end = time.time() + 30\n"
|
||||
"while time.time() < end:\n"
|
||||
" try:\n"
|
||||
" s = socket.socket(socket.AF_UNIX)\n"
|
||||
" s.connect(path)\n"
|
||||
" break\n"
|
||||
" except OSError:\n"
|
||||
" time.sleep(0.5)\n"
|
||||
"else:\n"
|
||||
" sys.exit(0)\n"
|
||||
"while time.time() < end:\n"
|
||||
" s.sendall(b'sendkey spc\\n')\n"
|
||||
" time.sleep(1)\n")
|
||||
subprocess.Popen([sys.executable, "-c", key_script, str(monitor_socket)],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
if daemonize:
|
||||
log_file.write_text("")
|
||||
qemu_args += ["-serial", f"file:{log_file}", "-daemonize", "-pidfile", str(pid_file)]
|
||||
run([qemu_binary, *qemu_args, *extra_args])
|
||||
print(f"VM started (PID {pid_file.read_text().strip()}); console log: {log_file}")
|
||||
else:
|
||||
os.execvp(qemu_binary, [qemu_binary, *qemu_args, *extra_args])
|
||||
if creating:
|
||||
# q35 has IDE (ide.0 is the system disk); "virt" does not, so use USB.
|
||||
qemu_args += ["-drive", f"file={seed_iso},if=none,id=seed_media,media=cdrom,readonly=on",
|
||||
"-device", "ide-cd,drive=seed_media,bus=ide.1" if plat.arch == "amd64"
|
||||
else "usb-storage,drive=seed_media"]
|
||||
start_qemu(plat, rt, qemu_args, options.extra_args, forwards, options.daemonize,
|
||||
"Starting Windows 11 from the QCOW2 base image." if creating
|
||||
else "Starting Windows 11 normally.")
|
||||
@@ -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(
|
||||
[switch]$InstallVirtIO,
|
||||
[switch]$EnableAutomaticLogon,
|
||||
[switch]$DisableAutoUpdate,
|
||||
[switch]$CopyInstallers,
|
||||
@@ -35,72 +34,6 @@ if (-not (Test-Administrator)) {
|
||||
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 {
|
||||
$winlogonPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||
|
||||
@@ -205,9 +138,6 @@ function Install-OpenSSHServer {
|
||||
|
||||
$exitCode = 0
|
||||
try {
|
||||
if ($InstallVirtIO) {
|
||||
Install-VirtIOGuestTools
|
||||
}
|
||||
if ($EnableAutomaticLogon) {
|
||||
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)"
|
||||
trap 'rm -rf "$work_dir"' EXIT
|
||||
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=(
|
||||
"$work_dir/Autounattend.xml"
|
||||
"$work_dir/Unattend.xml"
|
||||
"$script_dir/Configure-Windows.ps1"
|
||||
"$openssh_zip"
|
||||
)
|
||||
|
||||
+1
-1
Submodule vm-images updated: 1dc40655df...9c5abac9bf.
Reference in new issue
Block a user