Initial commit
This commit is contained in:
commit
58a0a2d1ff
24 files changed
+1156
No files matched your search
+19
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "vm-images"]
|
||||||
|
path = vm-images
|
||||||
|
url = https://www.wangguofang.net/git/vm-images.git
|
||||||
@@ -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
|
||||||
|
```
|
||||||
Executable
+169
@@ -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
|
||||||
Executable
+167
@@ -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
|
||||||
Executable
+192
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
seed.iso
|
||||||
|
*.iso
|
||||||
@@ -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.
|
||||||
Executable
+35
@@ -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"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
instance-id: almalinux10-host-001
|
||||||
|
local-hostname: almalinux10-host
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
ethernets:
|
||||||
|
all-en:
|
||||||
|
match:
|
||||||
|
name: "en*"
|
||||||
|
dhcp4: true
|
||||||
|
all-eth:
|
||||||
|
match:
|
||||||
|
name: "eth*"
|
||||||
|
dhcp4: true
|
||||||
@@ -0,0 +1,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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
seed.iso
|
||||||
|
*.iso
|
||||||
@@ -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.
|
||||||
Executable
+35
@@ -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}"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
instance-id: debian13-host-001
|
||||||
|
local-hostname: debian13-host
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
ethernets:
|
||||||
|
all-en:
|
||||||
|
match:
|
||||||
|
name: "en*"
|
||||||
|
dhcp4: true
|
||||||
|
all-eth:
|
||||||
|
match:
|
||||||
|
name: "eth*"
|
||||||
|
dhcp4: true
|
||||||
@@ -0,0 +1,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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
seed.iso
|
||||||
@@ -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.' }; & $script.FullName"</CommandLine>
|
||||||
|
<RequiresUserInput>false</RequiresUserInput>
|
||||||
|
</SynchronousCommand>
|
||||||
|
</FirstLogonCommands>
|
||||||
|
</component>
|
||||||
|
</settings>
|
||||||
|
</unattend>
|
||||||
@@ -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
|
||||||
Executable
+22
@@ -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.
Reference in new issue
Block a user