commit 58a0a2d1ff73a2dbc77409e7564e3a12869beef1 Author: Guofang Date: Tue Aug 18 17:02:41 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95074fa --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..46fd1bf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vm-images"] + path = vm-images + url = https://www.wangguofang.net/git/vm-images.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..829b892 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/run-almalinux10.sh b/run-almalinux10.sh new file mode 100755 index 0000000..e770845 --- /dev/null +++ b/run-almalinux10.sh @@ -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 <&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 diff --git a/run-debian13.sh b/run-debian13.sh new file mode 100755 index 0000000..0439d38 --- /dev/null +++ b/run-debian13.sh @@ -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 <&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 diff --git a/run-windows11.sh b/run-windows11.sh new file mode 100755 index 0000000..c16c61d --- /dev/null +++ b/run-windows11.sh @@ -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 <&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 diff --git a/seediso/README.md b/seediso/README.md new file mode 100644 index 0000000..afad59c --- /dev/null +++ b/seediso/README.md @@ -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. diff --git a/seediso/almalinux10/.gitignore b/seediso/almalinux10/.gitignore new file mode 100644 index 0000000..0115591 --- /dev/null +++ b/seediso/almalinux10/.gitignore @@ -0,0 +1,2 @@ +seed.iso +*.iso diff --git a/seediso/almalinux10/README.md b/seediso/almalinux10/README.md new file mode 100644 index 0000000..8c2316e --- /dev/null +++ b/seediso/almalinux10/README.md @@ -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. diff --git a/seediso/almalinux10/build-seed.sh b/seediso/almalinux10/build-seed.sh new file mode 100755 index 0000000..c7f8722 --- /dev/null +++ b/seediso/almalinux10/build-seed.sh @@ -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" diff --git a/seediso/almalinux10/meta-data b/seediso/almalinux10/meta-data new file mode 100644 index 0000000..eb22c01 --- /dev/null +++ b/seediso/almalinux10/meta-data @@ -0,0 +1,2 @@ +instance-id: almalinux10-host-001 +local-hostname: almalinux10-host diff --git a/seediso/almalinux10/network-config b/seediso/almalinux10/network-config new file mode 100644 index 0000000..fa9a6d7 --- /dev/null +++ b/seediso/almalinux10/network-config @@ -0,0 +1,10 @@ +version: 2 +ethernets: + all-en: + match: + name: "en*" + dhcp4: true + all-eth: + match: + name: "eth*" + dhcp4: true diff --git a/seediso/almalinux10/user-data b/seediso/almalinux10/user-data new file mode 100644 index 0000000..9a3f4f9 --- /dev/null +++ b/seediso/almalinux10/user-data @@ -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 diff --git a/seediso/debian13/.gitignore b/seediso/debian13/.gitignore new file mode 100644 index 0000000..0115591 --- /dev/null +++ b/seediso/debian13/.gitignore @@ -0,0 +1,2 @@ +seed.iso +*.iso diff --git a/seediso/debian13/README.md b/seediso/debian13/README.md new file mode 100644 index 0000000..9f12dab --- /dev/null +++ b/seediso/debian13/README.md @@ -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. diff --git a/seediso/debian13/build-seed.sh b/seediso/debian13/build-seed.sh new file mode 100755 index 0000000..056757e --- /dev/null +++ b/seediso/debian13/build-seed.sh @@ -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}" diff --git a/seediso/debian13/meta-data b/seediso/debian13/meta-data new file mode 100644 index 0000000..745d027 --- /dev/null +++ b/seediso/debian13/meta-data @@ -0,0 +1,2 @@ +instance-id: debian13-host-001 +local-hostname: debian13-host diff --git a/seediso/debian13/network-config b/seediso/debian13/network-config new file mode 100644 index 0000000..fa9a6d7 --- /dev/null +++ b/seediso/debian13/network-config @@ -0,0 +1,10 @@ +version: 2 +ethernets: + all-en: + match: + name: "en*" + dhcp4: true + all-eth: + match: + name: "eth*" + dhcp4: true diff --git a/seediso/debian13/user-data b/seediso/debian13/user-data new file mode 100644 index 0000000..5118852 --- /dev/null +++ b/seediso/debian13/user-data @@ -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 diff --git a/seediso/windows11/.gitignore b/seediso/windows11/.gitignore new file mode 100644 index 0000000..cf8ca7e --- /dev/null +++ b/seediso/windows11/.gitignore @@ -0,0 +1 @@ +seed.iso diff --git a/seediso/windows11/Autounattend.xml b/seediso/windows11/Autounattend.xml new file mode 100644 index 0000000..f262c21 --- /dev/null +++ b/seediso/windows11/Autounattend.xml @@ -0,0 +1,171 @@ + + + + + + en-US + + en-US + en-US + en-US + en-US + + + + false + Never + + + + 0 + true + + + 1 + EFI + 100 + + + 2 + MSR + 16 + + + 3 + Primary + true + + + + + 1 + 1 + FAT32 + + + + 2 + 3 + NTFS + + C + + + + OnError + + + + + + /IMAGE/INDEX + 6 + + + + 0 + 3 + + OnError + + + + true + + W269N-WFGWX-YVC9B-4J6C9-T83GX + Never + + + + + 1 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f + + + 2 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f + + + 3 + reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f + + + + + + + WIN11 + China Standard Time + + + false + + + + + true + Remote Desktop + all + + + + + + + en-US + en-US + en-US + en-US + + + + true + true + true + 3 + + + + + guofang + Administrators + Guofang + + wang + 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> diff --git a/seediso/windows11/Configure-Windows.ps1 b/seediso/windows11/Configure-Windows.ps1 new file mode 100644 index 0000000..9ac2203 --- /dev/null +++ b/seediso/windows11/Configure-Windows.ps1 @@ -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 diff --git a/seediso/windows11/build-seed.sh b/seediso/windows11/build-seed.sh new file mode 100755 index 0000000..ad5e929 --- /dev/null +++ b/seediso/windows11/build-seed.sh @@ -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" diff --git a/vm-images b/vm-images new file mode 160000 index 0000000..04c0109 --- /dev/null +++ b/vm-images @@ -0,0 +1 @@ +Subproject commit 04c0109baae421d59e79e6b5489aa209192f8686