Initial commit

This commit is contained in:
Guofang committed 2026-08-26 12:36:30 +08:00
commit 58a0a2d1ff
24 files changed
+1156

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# QEMU runtime state and writable overlays
.vm
# VM disk and generated installation/seed images
*.qcow2
*.vhd
*.vhdx
*.img
*.iso
*.part
# Runtime output
*.log
*.pid
# Editor and operating-system metadata
*.swp
*~
.DS_Store
+3
View File
@@ -0,0 +1,3 @@
[submodule "vm-images"]
path = vm-images
url = https://www.wangguofang.net/git/vm-images.git
+107
View File
@@ -0,0 +1,107 @@
# cloudhost
Start the required operating system with its distribution-specific launcher:
```bash
./run-debian13.sh
./run-almalinux10.sh
./run-windows11.sh
```
All virtual machines use OVMF/UEFI firmware. Each launcher creates a private,
persistent `OVMF_VARS_4M.fd` variable store in the VM state directory.
Closing the QEMU window stops a foreground VM. To keep the launcher in the
background while still showing the graphical window:
```bash
./run-debian13.sh --daemon
```
The script uses KVM when `/dev/kvm` is accessible, forwards host port `2222`
to guest port `22`, and creates `.vm/debian13/disk.qcow2` as a 20G writable overlay with the
original qcow2 image as its backing file. The base image therefore stays
unchanged. Resource settings can be overridden with environment variables;
run `./run-debian13.sh --help` for the full list.
If `.vm/debian13/seed.iso` is missing, the launcher automatically builds it
from `seediso/debian13/user-data`, `seediso/debian13/meta-data`, and
`seediso/debian13/network-config` before starting the VM.
Image download URLs and implementation are maintained in the `vm-images`
submodule. Clone this repository with its submodules, or initialize it later:
```bash
git submodule update --init --recursive
```
The launchers prefer `vm-images/download-images.sh` when the submodule is
initialized. Otherwise, they fall back to `../vm-images/download-images.sh`
for compatibility with the previous sibling-repository layout. Set
`VM_IMAGE_HELPER` to use another helper path explicitly.
If the Debian base image is missing, the launcher calls the selected image
helper with `debian13`.
## AlmaLinux 10
Start an AlmaLinux 10 Generic Cloud VM with:
```bash
./run-almalinux10.sh
```
When the shared AlmaLinux image is missing, the launcher calls the selected
image helper with `almalinux10`. It creates the writable
overlay `.vm/almalinux10/disk.qcow2` and uses the NoCloud configuration in
`seediso/almalinux10/`. SSH is forwarded to host port `2223` by default:
```bash
ssh -p 2223 guofang@localhost
```
AlmaLinux settings use the `ALMA_VM_` prefix. For example:
```bash
ALMA_VM_MEMORY=4G ALMA_VM_CPUS=4 ALMA_VM_SSH_PORT=2224 ./run-almalinux10.sh
```
Run `./run-almalinux10.sh --help` for all supported settings. The seed ISO is
stored alongside the VM disk at `.vm/almalinux10/seed.iso`. If it already
exists after editing its source files, rebuild it explicitly with
`./seediso/almalinux10/build-seed.sh .vm/almalinux10/seed.iso`.
## Windows 11 23H2
On first boot, the Windows 11 launcher creates a writable system-disk overlay
from the shared Windows 11 QCOW2 base image:
```bash
./run-windows11.sh
```
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
`localhost:3389`. During the first login it also installs and enables the
Windows OpenSSH Server on `localhost:2222` and silently installs the VirtIO
Windows guest tools. It also installs the host's configured SSH public key for
passwordless administrator login. The installer includes the VirtIO drivers, SPICE vdagent,
and QEMU Guest Agent. Connect with `ssh guofang@localhost -p 2222`. 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 generated answer-file ISO is stored beside the disk as
`.vm/windows11/seed.iso`.
To install from the Windows ISO instead, use `./run-windows11.sh
--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, resources, RDP port, or SSH port with the `WIN11_VM_` environment
variables shown by `./run-windows11.sh --help`. For example, when the VirtIO ISO
is elsewhere:
```bash
WIN11_VM_TOOLS_ISO=/custom/path/virtio-win.iso ./run-windows11.sh --install-from-iso
```
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if [[ -n "${VM_IMAGE_HELPER:-}" ]]; then
image_downloader="$VM_IMAGE_HELPER"
elif [[ -x "${script_dir}/vm-images/download-images.sh" ]]; then
image_downloader="${script_dir}/vm-images/download-images.sh"
else
image_downloader="${script_dir}/../vm-images/download-images.sh"
fi
uses_shared_base_image=false
if [[ -n "${ALMA_VM_BASE_IMAGE:-}" ]]; then
base_image="$ALMA_VM_BASE_IMAGE"
else
[[ -x "$image_downloader" ]] || { printf 'Error: shared image helper is unavailable: %s\n' "$image_downloader" >&2; exit 1; }
base_image="$("$image_downloader" --print-path almalinux10)"
uses_shared_base_image=true
fi
state_dir="${ALMA_VM_STATE_DIR:-${script_dir}/.vm/almalinux10}"
disk_image="${ALMA_VM_DISK_IMAGE:-${state_dir}/disk.qcow2}"
seed_image="${ALMA_VM_SEED_IMAGE:-$(dirname -- "$disk_image")/seed.iso}"
disk_size="${ALMA_VM_DISK_SIZE:-20G}"
memory="${ALMA_VM_MEMORY:-8G}"
cpus="${ALMA_VM_CPUS:-8}"
ssh_port="${ALMA_VM_SSH_PORT:-2223}"
uefi_code="${ALMA_VM_UEFI_CODE:-/usr/share/OVMF/OVMF_CODE_4M.fd}"
uefi_vars_template="${ALMA_VM_UEFI_VARS:-/usr/share/OVMF/OVMF_VARS_4M.fd}"
uefi_vars="${state_dir}/OVMF_VARS_4M.fd"
pid_file="${state_dir}/qemu.pid"
log_file="${state_dir}/console.log"
daemonize=false
extra_args=()
usage() {
cat <<EOF
Usage: $(basename "$0") [--daemon] [-- QEMU_ARGUMENT ...]
Start an AlmaLinux 10 Generic Cloud virtual machine. The launcher obtains a
missing base image through ../vm-images/download-images.sh and creates a
writable overlay under .vm/almalinux10/, leaving the base image unchanged.
Options:
--daemon Run in the background while keeping the graphical window
-h, --help Show this help
Environment variables:
ALMA_VM_MEMORY Guest memory (default: 8G)
ALMA_VM_CPUS Virtual CPU count (default: 8)
ALMA_VM_SSH_PORT Host port forwarded to guest SSH (default: 2223)
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_STATE_DIR Runtime state directory (default: .vm/almalinux10)
ALMA_VM_DISK_IMAGE Writable overlay path
ALMA_VM_DISK_SIZE New overlay virtual capacity (default: 20G)
ALMA_VM_UEFI_CODE OVMF read-only firmware path
ALMA_VM_UEFI_VARS OVMF variable-store template path
EOF
}
while (($#)); do
case "$1" in
--daemon)
daemonize=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
extra_args+=("$@")
break
;;
*)
printf 'Error: unknown option: %s\n' "$1" >&2
usage >&2
exit 2
;;
esac
done
for command_name in qemu-system-x86_64 qemu-img; do
if ! command -v "$command_name" >/dev/null 2>&1; then
printf 'Error: %s is not installed.\n' "$command_name" >&2
exit 1
fi
done
if [[ ! -f "$base_image" ]]; then
if [[ "$uses_shared_base_image" == true ]]; then
"$image_downloader" almalinux10
else
printf 'Error: AlmaLinux base image was not found: %s\n' "$base_image" >&2
exit 1
fi
fi
[[ -f "$base_image" ]] || { printf 'Error: downloader did not create: %s\n' "$base_image" >&2; exit 1; }
for firmware_file in "$uefi_code" "$uefi_vars_template"; do
[[ -f "$firmware_file" ]] || { printf 'Error: required UEFI firmware was not found: %s\n' "$firmware_file" >&2; exit 1; }
done
if [[ ! -f "$seed_image" ]]; then
seed_builder="${script_dir}/seediso/almalinux10/build-seed.sh"
if [[ ! -x "$seed_builder" ]]; then
printf 'Error: seed image is missing and builder is unavailable: %s\n' \
"$seed_builder" >&2
exit 1
fi
printf 'Seed image not found; building %s...\n' "$seed_image"
"$seed_builder" "$seed_image"
fi
mkdir -p "$state_dir"
[[ -f "$uefi_vars" ]] || cp -- "$uefi_vars_template" "$uefi_vars"
if [[ -f "$pid_file" ]]; then
old_pid="$(<"$pid_file")"
if [[ "$old_pid" =~ ^[0-9]+$ ]] && kill -0 "$old_pid" 2>/dev/null; then
printf 'Error: the AlmaLinux virtual machine is already running (PID %s).\n' \
"$old_pid" >&2
exit 1
fi
rm -f "$pid_file"
fi
if [[ ! -f "$disk_image" ]]; then
qemu-img create -q -f qcow2 -F qcow2 -b "$base_image" "$disk_image" "$disk_size"
printf 'Created writable disk overlay: %s\n' "$disk_image"
fi
qemu_args=(
-name almalinux10
-machine q35
-display gtk
-m "$memory"
-smp "$cpus"
-drive "if=pflash,format=raw,readonly=on,file=${uefi_code}"
-drive "if=pflash,format=raw,file=${uefi_vars}"
-drive "file=${disk_image},if=virtio,format=qcow2"
-drive "file=${seed_image},media=cdrom,readonly=on"
-nic "user,model=virtio-net-pci,hostfwd=tcp::${ssh_port}-:22"
)
if [[ -r /dev/kvm && -w /dev/kvm ]]; then
qemu_args+=(-enable-kvm -cpu host)
accelerator=KVM
else
qemu_args+=(-accel tcg -cpu max)
accelerator=TCG
fi
printf 'Starting AlmaLinux 10 with %s acceleration; SSH is forwarded to localhost:%s.\n' \
"$accelerator" "$ssh_port"
if [[ "$daemonize" == true ]]; then
: >"$log_file"
qemu_args+=(
-serial "file:${log_file}"
-monitor none
-daemonize
-pidfile "$pid_file"
)
qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
printf 'VM started (PID %s); console log: %s\n' "$(<"$pid_file")" "$log_file"
else
exec qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
fi
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if [[ -n "${VM_IMAGE_HELPER:-}" ]]; then
image_downloader="$VM_IMAGE_HELPER"
elif [[ -x "${script_dir}/vm-images/download-images.sh" ]]; then
image_downloader="${script_dir}/vm-images/download-images.sh"
else
image_downloader="${script_dir}/../vm-images/download-images.sh"
fi
uses_shared_base_image=false
if [[ -n "${VM_BASE_IMAGE:-}" ]]; then
base_image="$VM_BASE_IMAGE"
else
[[ -x "$image_downloader" ]] || { printf 'Error: shared image helper is unavailable: %s\n' "$image_downloader" >&2; exit 1; }
base_image="$("$image_downloader" --print-path debian13)"
uses_shared_base_image=true
fi
state_dir="${VM_STATE_DIR:-${script_dir}/.vm/debian13}"
disk_image="${VM_DISK_IMAGE:-${state_dir}/disk.qcow2}"
seed_image="${VM_SEED_IMAGE:-$(dirname -- "$disk_image")/seed.iso}"
disk_size="${VM_DISK_SIZE:-20G}"
memory="${VM_MEMORY:-8G}"
cpus="${VM_CPUS:-8}"
ssh_port="${VM_SSH_PORT:-2222}"
uefi_code="${VM_UEFI_CODE:-/usr/share/OVMF/OVMF_CODE_4M.fd}"
uefi_vars_template="${VM_UEFI_VARS:-/usr/share/OVMF/OVMF_VARS_4M.fd}"
uefi_vars="${state_dir}/OVMF_VARS_4M.fd"
pid_file="${state_dir}/qemu.pid"
log_file="${state_dir}/console.log"
daemonize=false
extra_args=()
usage() {
cat <<EOF
Usage: $(basename "$0") [--daemon] [-- QEMU_ARGUMENT ...]
Start the Debian 13 virtual machine. A writable overlay is created under
.vm/debian13/, so the downloaded base image is never modified.
Options:
--daemon Run in the background while keeping the graphical window
-h, --help Show this help
Environment variables:
VM_MEMORY Guest memory (default: 8G)
VM_CPUS Virtual CPU count (default: 8)
VM_SSH_PORT Host port forwarded to guest SSH (default: 2222)
VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper)
VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk)
VM_STATE_DIR Runtime state directory (default: .vm/debian13)
VM_DISK_IMAGE Writable overlay path
VM_DISK_SIZE New overlay virtual capacity (default: 20G)
VM_UEFI_CODE OVMF read-only firmware path
VM_UEFI_VARS OVMF variable-store template path
EOF
}
while (($#)); do
case "$1" in
--daemon)
daemonize=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
extra_args+=("$@")
break
;;
*)
printf 'Error: unknown option: %s\n' "$1" >&2
usage >&2
exit 2
;;
esac
done
for command_name in qemu-system-x86_64 qemu-img; do
if ! command -v "$command_name" >/dev/null 2>&1; then
printf 'Error: %s is not installed.\n' "$command_name" >&2
exit 1
fi
done
if [[ ! -f "$base_image" ]]; then
if [[ "$uses_shared_base_image" == true ]]; then
"$image_downloader" debian13
else
printf 'Error: Debian base image was not found: %s\n' "$base_image" >&2
exit 1
fi
fi
[[ -f "$base_image" ]] || { printf 'Error: downloader did not create: %s\n' "$base_image" >&2; exit 1; }
for firmware_file in "$uefi_code" "$uefi_vars_template"; do
[[ -f "$firmware_file" ]] || { printf 'Error: required UEFI firmware was not found: %s\n' "$firmware_file" >&2; exit 1; }
done
if [[ ! -f "$seed_image" ]]; then
seed_builder="${script_dir}/seediso/debian13/build-seed.sh"
if [[ ! -x "$seed_builder" ]]; then
printf 'Error: seed image is missing and builder is unavailable: %s\n' \
"$seed_builder" >&2
exit 1
fi
printf 'Seed image not found; building %s...\n' "$seed_image"
"$seed_builder" "$seed_image"
fi
mkdir -p "$state_dir"
[[ -f "$uefi_vars" ]] || cp -- "$uefi_vars_template" "$uefi_vars"
if [[ -f "$pid_file" ]]; then
old_pid="$(<"$pid_file")"
if [[ "$old_pid" =~ ^[0-9]+$ ]] && kill -0 "$old_pid" 2>/dev/null; then
printf 'Error: the virtual machine is already running (PID %s).\n' "$old_pid" >&2
exit 1
fi
rm -f "$pid_file"
fi
if [[ ! -f "$disk_image" ]]; then
qemu-img create -q -f qcow2 -F qcow2 -b "$base_image" "$disk_image" "$disk_size"
printf 'Created writable disk overlay: %s\n' "$disk_image"
fi
qemu_args=(
-name debian13
-machine q35
-display gtk
-m "$memory"
-smp "$cpus"
-drive "if=pflash,format=raw,readonly=on,file=${uefi_code}"
-drive "if=pflash,format=raw,file=${uefi_vars}"
-drive "file=${disk_image},if=virtio,format=qcow2"
-drive "file=${seed_image},media=cdrom,readonly=on"
-nic "user,model=virtio-net-pci,hostfwd=tcp::${ssh_port}-:22"
)
if [[ -r /dev/kvm && -w /dev/kvm ]]; then
qemu_args+=(-enable-kvm -cpu host)
accelerator=KVM
else
qemu_args+=(-accel tcg -cpu max)
accelerator=TCG
fi
printf 'Starting VM with %s acceleration; SSH is forwarded to localhost:%s.\n' \
"$accelerator" "$ssh_port"
if [[ "$daemonize" == true ]]; then
: >"$log_file"
qemu_args+=(
-serial "file:${log_file}"
-monitor none
-daemonize
-pidfile "$pid_file"
)
qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
printf 'VM started (PID %s); console log: %s\n' "$(<"$pid_file")" "$log_file"
else
exec qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
fi
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
state_dir="${WIN11_VM_STATE_DIR:-${script_dir}/.vm/windows11}"
disk_image="${WIN11_VM_DISK_IMAGE:-${state_dir}/disk.qcow2}"
seed_iso="${WIN11_VM_SEED_ISO:-$(dirname -- "$disk_image")/seed.iso}"
disk_size="${WIN11_VM_DISK_SIZE:-64G}"
memory="${WIN11_VM_MEMORY:-8G}"
cpus="${WIN11_VM_CPUS:-8}"
rdp_port="${WIN11_VM_RDP_PORT:-3389}"
ssh_port="${WIN11_VM_SSH_PORT:-2222}"
uefi_code="${WIN11_VM_UEFI_CODE:-/usr/share/OVMF/OVMF_CODE_4M.fd}"
uefi_vars_template="${WIN11_VM_UEFI_VARS:-/usr/share/OVMF/OVMF_VARS_4M.fd}"
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"
daemonize=false
install_from_iso=false
extra_args=()
usage() {
cat <<EOF
Usage: $(basename "$0") [--daemon] [--install-from-iso] [-- QEMU_ARGUMENT ...]
If ${disk_image} is missing, create it from the Windows 11 QCOW2 base image.
Use --install-from-iso to perform an automatic ISO installation instead.
If it exists, start Windows normally without attaching installation media.
Options:
--daemon Run QEMU in the background
--install-from-iso Install from ISO when the system disk is missing
-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
WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper)
WIN11_VM_MEMORY Guest memory (default: 8G)
WIN11_VM_CPUS Virtual CPU count (default: 8)
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
WIN11_VM_UEFI_VARS OVMF variable-store template path
EOF
}
while (($#)); do
case "$1" in
--daemon) daemonize=true; shift ;;
--install-from-iso) install_from_iso=true; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; extra_args+=("$@"); break ;;
*) printf 'Error: unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
esac
done
installing=false
if [[ ! -f "$disk_image" ]]; then
installing=true
if [[ -n "${VM_IMAGE_HELPER:-}" ]]; then
image_downloader="$VM_IMAGE_HELPER"
elif [[ -x "${script_dir}/vm-images/download-images.sh" ]]; then
image_downloader="${script_dir}/vm-images/download-images.sh"
else
image_downloader="${script_dir}/../vm-images/download-images.sh"
fi
[[ -x "$image_downloader" ]] || { printf 'Error: shared image helper is unavailable: %s\n' "$image_downloader" >&2; exit 1; }
if [[ "$install_from_iso" == true ]]; then
install_iso="${WIN11_VM_INSTALL_ISO:-$("$image_downloader" --print-path windows11 --iso)}"
uses_shared_tools_iso=false
if [[ -n "${WIN11_VM_TOOLS_ISO:-}" ]]; then
tools_iso="$WIN11_VM_TOOLS_ISO"
else
tools_iso="$("$image_downloader" --print-path virtio-win)"
uses_shared_tools_iso=true
fi
else
base_image="${WIN11_VM_BASE_IMAGE:-$("$image_downloader" --print-path windows11)}"
fi
fi
for command_name in qemu-system-x86_64; do
command -v "$command_name" >/dev/null 2>&1 || { printf 'Error: %s is not installed.\n' "$command_name" >&2; exit 1; }
done
if [[ "$installing" == true ]]; then
command -v qemu-img >/dev/null 2>&1 || { printf 'Error: qemu-img is not installed.\n' >&2; exit 1; }
if [[ "$install_from_iso" == true ]]; then
[[ -f "$install_iso" ]] || { printf 'Error: Windows 11 ISO was not found: %s\n' "$install_iso" >&2; exit 1; }
else
[[ -f "$base_image" ]] || { printf 'Error: Windows 11 base QCOW2 image was not found: %s\n' "$base_image" >&2; exit 1; }
fi
fi
for firmware_file in "$uefi_code" "$uefi_vars_template"; do
[[ -f "$firmware_file" ]] || { printf 'Error: required UEFI firmware was not found: %s\n' "$firmware_file" >&2; exit 1; }
done
mkdir -p "$state_dir"
if [[ "$installing" == true && "$install_from_iso" == true ]]; then
if [[ ! -f "$tools_iso" ]]; then
if [[ "$uses_shared_tools_iso" == true ]]; then
"$image_downloader" virtio-win
else
printf 'Error: VirtIO Windows tools ISO was not found: %s\n' "$tools_iso" >&2
exit 1
fi
fi
[[ -f "$tools_iso" ]] || { printf 'Error: downloader did not create: %s\n' "$tools_iso" >&2; exit 1; }
if [[ ! -f "$seed_iso" ||
"${script_dir}/seediso/windows11/Autounattend.xml" -nt "$seed_iso" ||
"${script_dir}/seediso/windows11/Configure-Windows.ps1" -nt "$seed_iso" ]]; then
"${script_dir}/seediso/windows11/build-seed.sh" "$seed_iso"
fi
fi
if [[ -f "$pid_file" ]]; then
old_pid="$(<"$pid_file")"
if [[ "$old_pid" =~ ^[0-9]+$ ]] && kill -0 "$old_pid" 2>/dev/null; then
printf 'Error: Windows 11 VM is already running (PID %s).\n' "$old_pid" >&2
exit 1
fi
rm -f "$pid_file"
fi
rm -f "$monitor_socket" "$guest_agent_socket"
[[ -f "$uefi_vars" ]] || cp -- "$uefi_vars_template" "$uefi_vars"
if [[ "$installing" == true ]]; then
if [[ "$install_from_iso" == true ]]; then
qemu-img create -q -f qcow2 "$disk_image" "$disk_size"
else
qemu-img create -q -f qcow2 -F qcow2 -b "$base_image" "$disk_image" "$disk_size"
fi
printf 'Created Windows 11 system disk: %s\n' "$disk_image"
fi
qemu_args=(
-name windows11 -machine q35 -m "$memory" -smp "$cpus"
-display gtk
# -display none -spice port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off -device virtio-vga
-drive "if=pflash,format=raw,readonly=on,file=${uefi_code}"
-drive "if=pflash,format=raw,file=${uefi_vars}"
-drive "file=${disk_image},if=none,id=system_disk,format=qcow2"
-device "ide-hd,drive=system_disk,bus=ide.0,bootindex=1"
-nic "user,model=e1000e,hostfwd=tcp::${rdp_port}-:3389,hostfwd=tcp::${ssh_port}-:22"
-device "virtio-serial-pci,id=virtio_serial"
-chardev "qemu-vdagent,id=vdagent,name=vdagent,clipboard=on,mouse=off"
-device "virtserialport,chardev=vdagent,name=com.redhat.spice.0"
# Register the USB tablet after vdagent so QEMU selects it as the active pointer.
-device "qemu-xhci,id=usb_controller" -device "usb-tablet,bus=usb_controller.0"
-chardev "socket,path=${guest_agent_socket},server=on,wait=off,id=qga"
-device "virtserialport,chardev=qga,name=org.qemu.guest_agent.0"
-monitor "unix:${monitor_socket},server=on,wait=off"
)
if [[ "$installing" == true && "$install_from_iso" == true ]]; then
qemu_args+=(
-drive "file=${install_iso},if=none,id=install_media,media=cdrom,readonly=on"
-device "ide-cd,drive=install_media,bus=ide.1,bootindex=2"
-drive "file=${seed_iso},if=none,id=answer_media,media=cdrom,readonly=on"
-device "ide-cd,drive=answer_media,bus=ide.2,bootindex=3"
-drive "file=${tools_iso},if=none,id=tools_media,media=cdrom,readonly=on"
-device "ide-cd,drive=tools_media,bus=ide.3,bootindex=4"
)
fi
if [[ -r /dev/kvm && -w /dev/kvm ]]; then
qemu_args+=(-enable-kvm -cpu host)
accelerator=KVM
else
qemu_args+=(-accel tcg -cpu max)
accelerator=TCG
fi
if [[ "$installing" == true && "$install_from_iso" == true ]]; then
printf 'Starting Windows 11 automatic installation with %s acceleration.\n' "$accelerator"
elif [[ "$installing" == true ]]; then
printf 'Starting Windows 11 from the QCOW2 base image with %s acceleration.\n' "$accelerator"
else
printf 'Starting Windows 11 normally with %s acceleration.\n' "$accelerator"
fi
printf 'Account: guofang / password; RDP: localhost:%s; SSH: localhost:%s\n' "$rdp_port" "$ssh_port"
if [[ "$daemonize" == true ]]; then
: >"$log_file"
qemu_args+=(-serial "file:${log_file}" -daemonize -pidfile "$pid_file")
qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
printf 'VM started (PID %s); console log: %s\n' "$(<"$pid_file")" "$log_file"
else
exec qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
fi
+14
View File
@@ -0,0 +1,14 @@
# Cloud-init seed images
Each operating system has an independent NoCloud configuration and generated
`seed.iso`:
```text
seediso/
├── debian13/
└── almalinux10/
```
Keeping per-OS metadata separate allows each VM to use a distinct hostname and
cloud-init `instance-id`. Generated ISO files are ignored by Git and are built
automatically by the corresponding launcher when missing.
+2
View File
@@ -0,0 +1,2 @@
seed.iso
*.iso
+13
View File
@@ -0,0 +1,13 @@
# AlmaLinux 10 seed.iso
This directory contains the cloud-init NoCloud configuration used by
`run-almalinux10.sh`.
After changing `user-data`, `meta-data`, or `network-config`, remove the old
`seed.iso` and rebuild it:
```bash
./seediso/almalinux10/build-seed.sh
```
Use a unique `instance-id` when cloning this configuration for another VM.
+35
View File
@@ -0,0 +1,35 @@
#!/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)
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="${script_dir}/network-config")
fi
cloud-localds "${args[@]}" "$output" \
"${script_dir}/user-data" "${script_dir}/meta-data"
elif command -v xorriso >/dev/null 2>&1; then
xorriso -as mkisofs -quiet -volid cidata -joliet -rock \
-output "$output" "${files[@]/#/${script_dir}/}"
elif command -v genisoimage >/dev/null 2>&1; then
genisoimage -quiet -output "$output" -volid cidata -joliet -rock \
"${files[@]/#/${script_dir}/}"
elif command -v mkisofs >/dev/null 2>&1; then
mkisofs -quiet -output "$output" -volid cidata -joliet -rock \
"${files[@]/#/${script_dir}/}"
else
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
exit 1
fi
printf 'Created %s\n' "$output"
+2
View File
@@ -0,0 +1,2 @@
instance-id: almalinux10-host-001
local-hostname: almalinux10-host
+10
View File
@@ -0,0 +1,10 @@
version: 2
ethernets:
all-en:
match:
name: "en*"
dhcp4: true
all-eth:
match:
name: "eth*"
dhcp4: true
+16
View File
@@ -0,0 +1,16 @@
#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
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
chpasswd:
expire: false
+2
View File
@@ -0,0 +1,2 @@
seed.iso
*.iso
+21
View File
@@ -0,0 +1,21 @@
# Debian 13 seed.iso
This directory contains the cloud-init NoCloud configuration used by
`run-debian13.sh`.
1. Edit `user-data`, `meta-data`, and (if needed) `network-config`.
2. Build the image:
```bash
./seediso/debian13/build-seed.sh
```
To choose another output path:
```bash
./seediso/debian13/build-seed.sh /path/to/seed.iso
```
The script uses `cloud-localds` when available, then falls back to `xorriso`,
`genisoimage`, or `mkisofs`. Attach the resulting ISO as a CD-ROM when booting
a cloud-init-enabled virtual machine.
+35
View File
@@ -0,0 +1,35 @@
#!/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)
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="${script_dir}/network-config")
fi
cloud-localds "${args[@]}" "${output}" \
"${script_dir}/user-data" "${script_dir}/meta-data"
elif command -v xorriso >/dev/null 2>&1; then
xorriso -as mkisofs -quiet -volid cidata -joliet -rock \
-output "${output}" "${files[@]/#/${script_dir}/}"
elif command -v genisoimage >/dev/null 2>&1; then
genisoimage -quiet -output "${output}" -volid cidata -joliet -rock \
"${files[@]/#/${script_dir}/}"
elif command -v mkisofs >/dev/null 2>&1; then
mkisofs -quiet -output "${output}" -volid cidata -joliet -rock \
"${files[@]/#/${script_dir}/}"
else
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
exit 1
fi
printf 'Created %s\n' "${output}"
+2
View File
@@ -0,0 +1,2 @@
instance-id: debian13-host-001
local-hostname: debian13-host
+10
View File
@@ -0,0 +1,10 @@
version: 2
ethernets:
all-en:
match:
name: "en*"
dhcp4: true
all-eth:
match:
name: "eth*"
dhcp4: true
+45
View File
@@ -0,0 +1,45 @@
#cloud-config
preserve_hostname: false
manage_etc_hosts: true
ssh_pwauth: true
users:
- name: guofang
gecos: Guofang
groups: [sudo]
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD: ALL"
lock_passwd: false
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwsF2ZDSHDaXM9BdDq3Vxkp6T+DJEakKaVZQJUEHexS34mbrxYAxmGBUPCrddfE01dS31kGS4XHVBa+wGUYodY+nPwnrfD1Q4phKVSzXr65K1Y/sCcjSiANBPWpv2Xd7IMOfLlrdLtmf5QcKWroIqO2LaIDGECwzUUHrfprgeaF9N0FGcmrgbmm23JUk+f6guRQ4br88L4rWIWEBvL9bTdwErNkdfsYE2F/U8aIFpMcVy2eJIOgy8ZjZvkGdm4r730YkTDL1VmAjiHH3/m2R1EOvwLzFhyWKoa4ULk/IQW+8iNnG42QEcaWRM+hMwlVdnNhPJP6WzS+fAEbibD3zM0q2X/4ex+cXRNOCd2/0KhnD2+3p1BM5QeFTzDappkk8W2meQkDE3hvTjtIQCzygFO6LNWtEEa7BTvvlMqF3CZWh88J9XiP750SjESe3Ge5o9A0T2hcJa+KZ1LxVEmNt4nD6E/OD4EymoaLIJZ1fH5ksSFjHzJ+mJouSVjc/H/VSE= guofang@GUOFANG-OFFICE
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
chpasswd:
expire: false
apt:
primary:
- arches: [default]
uri: "https://mirrors.ustc.edu.cn/debian"
security:
- arches: [default]
uri: "https://mirrors.ustc.edu.cn/debian-security"
# http_proxy: "http://10.15.112.151:3128/"
# https_proxy: "http://10.15.112.151:3128/"
# http_proxy: "socks5h://10.0.2.2:1080/"
# https_proxy: "socks5h://10.0.2.2:1080/"
# package_update: true
# packages:
# - task-gnome-desktop
# runcmd:
# - systemctl set-default graphical.target
# - systemctl enable gdm3.service
# power_state:
# delay: now
# mode: reboot
# message: "Cloud-init configuration completed; rebooting."
# condition: true
+1
View File
@@ -0,0 +1 @@
seed.iso
+171
View File
@@ -0,0 +1,171 @@
<?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 "$script = Get-ChildItem -Path (Get-PSDrive -PSProvider FileSystem).Root -Filter 'Configure-Windows.ps1' -File -ErrorAction SilentlyContinue | Select-Object -First 1; if (-not $script) { throw 'Configure-Windows.ps1 was not found on the attached media.' }; &amp; $script.FullName"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>
+97
View File
@@ -0,0 +1,97 @@
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Install-VirtIOGuestTools {
$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 Install-OpenSSHServer {
$capability = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' |
Select-Object -First 1
if (-not $capability) {
throw 'The OpenSSH Server Windows capability was not found.'
}
if ($capability.State -ne 'Installed') {
Add-WindowsCapability -Online -Name $capability.Name | Out-Null
}
Set-Service -Name 'sshd' -StartupType Automatic
Start-Service -Name 'sshd'
$rule = Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue
if ($rule) {
Set-NetFirewallRule `
-Name 'OpenSSH-Server-In-TCP' `
-Enabled True `
-Profile Private, Public
}
else {
New-NetFirewallRule `
-Name 'OpenSSH-Server-In-TCP' `
-DisplayName 'OpenSSH SSH Server (sshd)' `
-Enabled True `
-Direction Inbound `
-Protocol TCP `
-Action Allow `
-LocalPort 22 `
-Profile Private, Public | Out-Null
}
}
function Install-AdministratorAuthorizedKey {
$authorizedKeys = Join-Path $env:ProgramData 'ssh\administrators_authorized_keys'
$publicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwsF2ZDSHDaXM9BdDq3Vxkp6T+DJEakKaVZQJUEHexS34mbrxYAxmGBUPCrddfE01dS31kGS4XHVBa+wGUYodY+nPwnrfD1Q4phKVSzXr65K1Y/sCcjSiANBPWpv2Xd7IMOfLlrdLtmf5QcKWroIqO2LaIDGECwzUUHrfprgeaF9N0FGcmrgbmm23JUk+f6guRQ4br88L4rWIWEBvL9bTdwErNkdfsYE2F/U8aIFpMcVy2eJIOgy8ZjZvkGdm4r730YkTDL1VmAjiHH3/m2R1EOvwLzFhyWKoa4ULk/IQW+8iNnG42QEcaWRM+hMwlVdnNhPJP6WzS+fAEbibD3zM0q2X/4ex+cXRNOCd2/0KhnD2+3p1BM5QeFTzDappkk8W2meQkDE3hvTjtIQCzygFO6LNWtEEa7BTvvlMqF3CZWh88J9XiP750SjESe3Ge5o9A0T2hcJa+KZ1LxVEmNt4nD6E/OD4EymoaLIJZ1fH5ksSFjHzJ+mJouSVjc/H/VSE= guofang@GUOFANG-OFFICE'
[IO.File]::WriteAllText(
$authorizedKeys,
$publicKey + [Environment]::NewLine,
[Text.Encoding]::ASCII
)
& icacls.exe `
$authorizedKeys `
'/inheritance:r' `
'/grant' `
'*S-1-5-32-544:F' `
'*S-1-5-18:F'
if ($LASTEXITCODE -ne 0) {
throw "icacls.exe exited with code $LASTEXITCODE."
}
}
function Enable-AutomaticLogon {
$winlogonPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
Set-ItemProperty -Path $winlogonPath -Name 'AutoAdminLogon' -Value '1' -Type String
Set-ItemProperty -Path $winlogonPath -Name 'DefaultUserName' -Value 'guofang' -Type String
Set-ItemProperty -Path $winlogonPath -Name 'DefaultPassword' -Value 'wang' -Type String
Set-ItemProperty -Path $winlogonPath -Name 'DefaultDomainName' -Value $env:COMPUTERNAME -Type String
Remove-ItemProperty -Path $winlogonPath -Name 'AutoLogonCount' -ErrorAction SilentlyContinue
}
Install-VirtIOGuestTools
Install-OpenSSHServer
Install-AdministratorAuthorizedKey
Enable-AutomaticLogon
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
output="${1:-${script_dir}/seed.iso}"
seed_files=(
"$script_dir/Autounattend.xml"
"$script_dir/Configure-Windows.ps1"
)
mkdir -p "$(dirname -- "$output")"
if command -v xorriso >/dev/null 2>&1; then
xorriso -as mkisofs -quiet -o "$output" -V CIDATA -J -r "${seed_files[@]}"
elif command -v genisoimage >/dev/null 2>&1; then
genisoimage -quiet -o "$output" -V CIDATA -J -r "${seed_files[@]}"
elif command -v mkisofs >/dev/null 2>&1; then
mkisofs -quiet -o "$output" -V CIDATA -J -r "${seed_files[@]}"
else
printf 'Error: install xorriso, genisoimage, or mkisofs.\n' >&2
exit 1
fi
printf 'Built Windows 11 answer-file ISO: %s\n' "$output"
Submodule
+1
Submodule vm-images added at 04c0109baa.