Add Ubuntu 26.04 virtual machine
Add run-ubuntu2604.py with its QEMU config files and NoCloud seed. The launcher follows the Debian 13 one, with UBUNTU_VM_ settings and SSH forwarded to host port 2225. The seed injects the current user's SSH public key, uses the USTC mirror (ubuntu-ports on arm64) and installs the GNOME desktop on first boot. Update vm-images for the ubuntu2604 image target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kwxp2XWvKnuEBifnCbhyX5
This commit is contained in:
1 parent
649399aaa1
commit
1691062554
13 files changed
+564
-4
No files matched your search
@@ -6,6 +6,7 @@ Start the required operating system with its distribution-specific launcher:
|
||||
./run-debian13.py
|
||||
./run-almalinux10.py
|
||||
./run-fedora44.py
|
||||
./run-ubuntu2604.py
|
||||
./run-windows11.py
|
||||
```
|
||||
|
||||
@@ -46,8 +47,8 @@ corresponding `*_STATE_DIR` environment variable:
|
||||
|
||||
With `--download-only`, a launcher only downloads the media it would fetch
|
||||
automatically and then exits without creating a VM disk or starting QEMU: the
|
||||
base image for Debian, AlmaLinux and Fedora, and the VirtIO tools ISO for
|
||||
Windows 11 (whose installation ISO and base QCOW2 are supplied manually):
|
||||
base image for Debian, AlmaLinux, Fedora and Ubuntu, and the VirtIO tools ISO
|
||||
for Windows 11 (whose installation ISO and base QCOW2 are supplied manually):
|
||||
|
||||
```bash
|
||||
./run-fedora44.py --download-only
|
||||
@@ -126,6 +127,35 @@ architecture-specific devices in `qemu/fedora44-amd64.ini` and
|
||||
`qemu/fedora44-arm64.ini`. Other Fedora settings use the `FEDORA_VM_` prefix.
|
||||
Run `./run-fedora44.py --help` for the complete list of overrides.
|
||||
|
||||
## Ubuntu 26.04
|
||||
|
||||
Start an Ubuntu 26.04 LTS (Resolute Raccoon) cloud image VM with:
|
||||
|
||||
```bash
|
||||
./run-ubuntu2604.py
|
||||
```
|
||||
|
||||
The launcher downloads the `ubuntu2604` image through the shared helper when
|
||||
it is missing, creates `.vm/ubuntu2604/disk.qcow2`, and builds its NoCloud seed
|
||||
from `seediso/ubuntu2604/`. Like the Debian seed, it injects the current user's
|
||||
SSH public key and points APT at the USTC mirror (`ubuntu-ports` on arm64).
|
||||
SSH is forwarded to host port `2225` by default:
|
||||
|
||||
```bash
|
||||
ssh -p 2225 guofang@localhost
|
||||
```
|
||||
|
||||
On the first boot, cloud-init installs `ubuntu-desktop-minimal` (GNOME) and
|
||||
`ibus-table-wubi`, makes the graphical target the default and reboots the
|
||||
guest. Keep the VM running until this initial installation and reboot have
|
||||
completed.
|
||||
|
||||
The VM hardware (8G memory, 8 CPUs, drives, SSH port `2225`) is defined in
|
||||
`qemu/ubuntu2604.ini`, with the machine type and architecture-specific devices
|
||||
in `qemu/ubuntu2604-amd64.ini` and `qemu/ubuntu2604-arm64.ini`. Other Ubuntu
|
||||
settings use the `UBUNTU_VM_` prefix. Run `./run-ubuntu2604.py --help` for the
|
||||
complete list of overrides.
|
||||
|
||||
## Windows 11 23H2
|
||||
|
||||
On first boot, the Windows 11 launcher creates a writable system-disk overlay
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# amd64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini.
|
||||
|
||||
[machine]
|
||||
type = "q35"
|
||||
@@ -0,0 +1,17 @@
|
||||
# arm64 part of the Ubuntu 26.04 QEMU configuration; read after ubuntu2604.ini.
|
||||
|
||||
[machine]
|
||||
type = "virt"
|
||||
|
||||
# Unlike q35, the "virt" machine has no default GPU or USB input devices.
|
||||
[device "gpu0"]
|
||||
driver = "virtio-gpu-pci"
|
||||
|
||||
[device "usb0"]
|
||||
driver = "qemu-xhci"
|
||||
|
||||
[device "kbd0"]
|
||||
driver = "usb-kbd"
|
||||
|
||||
[device "tablet0"]
|
||||
driver = "usb-tablet"
|
||||
@@ -0,0 +1,69 @@
|
||||
# QEMU configuration for the Ubuntu 26.04 virtual machine, shared by all
|
||||
# architectures. The machine type and architecture-specific devices are in
|
||||
# ubuntu2604-<arch>.ini, which QEMU reads after this file.
|
||||
#
|
||||
# run-ubuntu2604.py fills in the file path placeholders below, writes the result
|
||||
# to <state dir>/qemu.ini and passes it to QEMU with -readconfig. Options that
|
||||
# have no -readconfig equivalent (-cpu, -display, daemon mode) are still added
|
||||
# by the launcher.
|
||||
|
||||
[name]
|
||||
guest = "ubuntu2604"
|
||||
|
||||
# Accelerators are tried in order: KVM on Linux, HVF on macOS, else emulation.
|
||||
[accel]
|
||||
accel = "kvm"
|
||||
|
||||
[accel]
|
||||
accel = "hvf"
|
||||
|
||||
[accel]
|
||||
accel = "tcg"
|
||||
|
||||
[memory]
|
||||
size = "8G"
|
||||
|
||||
[smp-opts]
|
||||
cpus = "8"
|
||||
|
||||
# UEFI firmware: read-only code and the per-VM variable store.
|
||||
[drive "pflash0"]
|
||||
if = "pflash"
|
||||
format = "raw"
|
||||
readonly = "on"
|
||||
file = "${uefi_code}"
|
||||
|
||||
[drive "pflash1"]
|
||||
if = "pflash"
|
||||
format = "raw"
|
||||
file = "${uefi_vars}"
|
||||
|
||||
# Writable qcow2 overlay on top of the shared base image.
|
||||
[drive "disk0"]
|
||||
if = "none"
|
||||
format = "qcow2"
|
||||
file = "${disk_image}"
|
||||
|
||||
[device "disk0-dev"]
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "disk0"
|
||||
|
||||
# cloud-init seed ISO.
|
||||
[drive "seed0"]
|
||||
if = "none"
|
||||
format = "raw"
|
||||
readonly = "on"
|
||||
file = "${seed_image}"
|
||||
|
||||
[device "seed0-dev"]
|
||||
driver = "virtio-blk-pci"
|
||||
drive = "seed0"
|
||||
|
||||
# User-mode networking with guest SSH forwarded to the host.
|
||||
[netdev "net0"]
|
||||
type = "user"
|
||||
hostfwd = "tcp::2225-:22"
|
||||
|
||||
[device "nic0"]
|
||||
driver = "virtio-net-pci"
|
||||
netdev = "net0"
|
||||
Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Start the Ubuntu 26.04 virtual machine."""
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
env = os.environ.get
|
||||
|
||||
|
||||
def die(message, code=1):
|
||||
print(f"Error: {message}", file=sys.stderr)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def run(command):
|
||||
"""Run a command, exiting with its status on failure (like `set -e`)."""
|
||||
result = subprocess.run(command)
|
||||
if result.returncode != 0:
|
||||
sys.exit(result.returncode)
|
||||
|
||||
|
||||
def is_executable(path):
|
||||
return path.is_file() and os.access(path, os.X_OK)
|
||||
|
||||
|
||||
if env("VM_IMAGE_HELPER"):
|
||||
image_downloader = Path(env("VM_IMAGE_HELPER"))
|
||||
elif is_executable(script_dir / "vm-images" / "download-images.sh"):
|
||||
image_downloader = script_dir / "vm-images" / "download-images.sh"
|
||||
else:
|
||||
image_downloader = script_dir / ".." / "vm-images" / "download-images.sh"
|
||||
|
||||
uses_shared_base_image = False
|
||||
if env("UBUNTU_VM_BASE_IMAGE"):
|
||||
base_image = Path(env("UBUNTU_VM_BASE_IMAGE"))
|
||||
else:
|
||||
if not is_executable(image_downloader):
|
||||
die(f"shared image helper is unavailable: {image_downloader}")
|
||||
result = subprocess.run(
|
||||
[str(image_downloader), "--print-path", "ubuntu2604"],
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
sys.exit(result.returncode)
|
||||
base_image = Path(result.stdout.rstrip("\n"))
|
||||
uses_shared_base_image = True
|
||||
|
||||
state_dir = Path(env("UBUNTU_VM_STATE_DIR") or script_dir / ".vm" / "ubuntu2604")
|
||||
disk_size = env("UBUNTU_VM_DISK_SIZE") or "20G"
|
||||
|
||||
|
||||
def normalize_arch(arch):
|
||||
if arch in ("arm64", "aarch64"):
|
||||
return "arm64"
|
||||
if arch in ("x86_64", "amd64"):
|
||||
return "amd64"
|
||||
die(f"unsupported architecture: {arch}")
|
||||
|
||||
|
||||
native_arch = normalize_arch(platform.machine())
|
||||
host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch)
|
||||
qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
|
||||
qemu_config_template = Path(
|
||||
env("UBUNTU_VM_QEMU_CONFIG") or script_dir / "qemu" / "ubuntu2604.ini")
|
||||
qemu_arch_config = script_dir / "qemu" / f"ubuntu2604-{host_arch}.ini"
|
||||
|
||||
|
||||
def pick_firmware(override, *candidates):
|
||||
if override:
|
||||
return Path(override)
|
||||
for candidate in candidates:
|
||||
if Path(candidate).is_file():
|
||||
return Path(candidate)
|
||||
return Path(candidates[0])
|
||||
|
||||
|
||||
if host_arch == "arm64":
|
||||
uefi_code = pick_firmware(
|
||||
env("UBUNTU_VM_UEFI_CODE"),
|
||||
"/usr/share/AAVMF/AAVMF_CODE.fd",
|
||||
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
|
||||
"/opt/homebrew/share/qemu/edk2-aarch64-code.fd",
|
||||
"/usr/local/share/qemu/edk2-aarch64-code.fd",
|
||||
)
|
||||
uefi_vars_template = pick_firmware(
|
||||
env("UBUNTU_VM_UEFI_VARS"),
|
||||
"/usr/share/AAVMF/AAVMF_VARS.fd",
|
||||
"/usr/share/edk2/aarch64/vars-template-pflash.raw",
|
||||
"/opt/homebrew/share/qemu/edk2-arm-vars.fd",
|
||||
"/usr/local/share/qemu/edk2-arm-vars.fd",
|
||||
)
|
||||
else:
|
||||
uefi_code = pick_firmware(
|
||||
env("UBUNTU_VM_UEFI_CODE"),
|
||||
"/usr/share/OVMF/OVMF_CODE_4M.fd",
|
||||
"/usr/share/edk2/ovmf/OVMF_CODE.fd",
|
||||
"/opt/homebrew/share/qemu/edk2-x86_64-code.fd",
|
||||
"/usr/local/share/qemu/edk2-x86_64-code.fd",
|
||||
)
|
||||
uefi_vars_template = pick_firmware(
|
||||
env("UBUNTU_VM_UEFI_VARS"),
|
||||
"/usr/share/OVMF/OVMF_VARS_4M.fd",
|
||||
"/usr/share/edk2/ovmf/OVMF_VARS.fd",
|
||||
"/opt/homebrew/share/qemu/edk2-i386-vars.fd",
|
||||
"/usr/local/share/qemu/edk2-i386-vars.fd",
|
||||
)
|
||||
daemonize = False
|
||||
download_only = False
|
||||
extra_args = []
|
||||
|
||||
|
||||
def usage(stream=sys.stdout):
|
||||
stream.write(f"""\
|
||||
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--state-dir DIR] [-- QEMU_ARGUMENT ...]
|
||||
|
||||
Start an Ubuntu 26.04 LTS cloud image virtual machine. A writable overlay is
|
||||
created in the state directory (default: .vm/ubuntu2604/), so the downloaded
|
||||
base image is never modified. The VM hardware (memory, CPUs, SSH port forward,
|
||||
...) is defined in qemu/ubuntu2604.ini, which is rendered to qemu.ini in the
|
||||
state directory and loaded with QEMU's -readconfig, followed by
|
||||
qemu/ubuntu2604-<arch>.ini. Later QEMU arguments override them, e.g.
|
||||
`-- -m 4G -smp 4`.
|
||||
|
||||
Options:
|
||||
--daemon Run in the background while keeping the graphical window
|
||||
--download-only Download the base image if it is missing, then exit
|
||||
--state-dir DIR Directory for the VM disk, seed ISO and runtime files
|
||||
(default: .vm/ubuntu2604; overrides UBUNTU_VM_STATE_DIR)
|
||||
-h, --help Show this help
|
||||
|
||||
Environment variables:
|
||||
UBUNTU_VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper)
|
||||
UBUNTU_VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk)
|
||||
UBUNTU_VM_STATE_DIR Runtime state directory (default: .vm/ubuntu2604)
|
||||
UBUNTU_VM_DISK_IMAGE Writable overlay path
|
||||
UBUNTU_VM_DISK_SIZE New overlay virtual capacity (default: 20G)
|
||||
VM_IMAGE_ARCH Guest/host architecture: arm64 or amd64 (default: auto-detected from the host)
|
||||
UBUNTU_VM_UEFI_CODE UEFI read-only firmware path (auto-detected for the selected architecture)
|
||||
UBUNTU_VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
|
||||
UBUNTU_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
|
||||
UBUNTU_VM_QEMU_CONFIG QEMU config template rendered into the state directory and
|
||||
passed to -readconfig (default: qemu/ubuntu2604.ini)
|
||||
""")
|
||||
|
||||
|
||||
args = sys.argv[1:]
|
||||
while args:
|
||||
arg = args.pop(0)
|
||||
if arg == "--daemon":
|
||||
daemonize = True
|
||||
elif arg == "--download-only":
|
||||
download_only = True
|
||||
elif arg == "--state-dir":
|
||||
if not args:
|
||||
die(f"{arg} requires a directory argument.", 2)
|
||||
state_dir = Path(args.pop(0))
|
||||
elif arg.startswith("--state-dir="):
|
||||
state_dir = Path(arg.split("=", 1)[1])
|
||||
elif arg in ("-h", "--help"):
|
||||
usage()
|
||||
sys.exit(0)
|
||||
elif arg == "--":
|
||||
extra_args.extend(args)
|
||||
break
|
||||
else:
|
||||
print(f"Error: unknown option: {arg}", file=sys.stderr)
|
||||
usage(sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
disk_image = Path(env("UBUNTU_VM_DISK_IMAGE") or state_dir / "disk.qcow2")
|
||||
seed_image = Path(env("UBUNTU_VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
|
||||
uefi_vars = state_dir / "OVMF_VARS_4M.fd"
|
||||
pid_file = state_dir / "qemu.pid"
|
||||
log_file = state_dir / "console.log"
|
||||
qemu_config = state_dir / "qemu.ini"
|
||||
|
||||
if not download_only:
|
||||
for command_name in (qemu_binary, "qemu-img"):
|
||||
if shutil.which(command_name) is None:
|
||||
die(f"{command_name} is not installed.")
|
||||
|
||||
if not base_image.is_file():
|
||||
if uses_shared_base_image:
|
||||
run([str(image_downloader), "ubuntu2604"])
|
||||
else:
|
||||
die(f"Ubuntu base image was not found: {base_image}")
|
||||
if not base_image.is_file():
|
||||
die(f"downloader did not create: {base_image}")
|
||||
if download_only:
|
||||
print(f"Base image is ready: {base_image}")
|
||||
sys.exit(0)
|
||||
for firmware_file in (uefi_code, uefi_vars_template):
|
||||
if not firmware_file.is_file():
|
||||
die(f"required UEFI firmware was not found: {firmware_file}")
|
||||
|
||||
if not seed_image.is_file():
|
||||
seed_builder = script_dir / "seediso" / "ubuntu2604" / "build-seed.sh"
|
||||
if not is_executable(seed_builder):
|
||||
die(f"seed image is missing and builder is unavailable: {seed_builder}")
|
||||
print(f"Seed image not found; building {seed_image}...", flush=True)
|
||||
run([str(seed_builder), str(seed_image)])
|
||||
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# QEMU requires the code and vars pflash images to be exactly the same size.
|
||||
# Homebrew's edk2 firmware ships code/vars pairs at mismatched sizes (true for
|
||||
# both the aarch64 and x86_64 pairs), so pad per-VM copies of whichever one is
|
||||
# smaller up to match the other.
|
||||
uefi_code_flash = state_dir / "OVMF_CODE_4M.fd"
|
||||
if not uefi_code_flash.is_file() or not uefi_vars.is_file():
|
||||
flash_size = max(uefi_code.stat().st_size, uefi_vars_template.stat().st_size)
|
||||
for source, target in ((uefi_code, uefi_code_flash), (uefi_vars_template, uefi_vars)):
|
||||
if not target.is_file():
|
||||
shutil.copyfile(source, target)
|
||||
os.truncate(target, flash_size)
|
||||
|
||||
if pid_file.is_file():
|
||||
old_pid = pid_file.read_text().strip()
|
||||
if old_pid.isdigit():
|
||||
try:
|
||||
os.kill(int(old_pid), 0)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
die(f"the Ubuntu virtual machine is already running (PID {old_pid}).")
|
||||
pid_file.unlink(missing_ok=True)
|
||||
|
||||
if not disk_image.is_file():
|
||||
run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2",
|
||||
"-b", str(base_image), str(disk_image), disk_size])
|
||||
print(f"Created writable disk overlay: {disk_image}")
|
||||
|
||||
is_macos = platform.system() == "Darwin"
|
||||
display_backend = env("UBUNTU_VM_DISPLAY") or ("cocoa" if is_macos else "gtk")
|
||||
|
||||
# Most of the VM definition lives in the QEMU config template; fill in the
|
||||
# file paths and hand the rendered file to QEMU with -readconfig.
|
||||
config_values = {
|
||||
"uefi_code": uefi_code_flash,
|
||||
"uefi_vars": uefi_vars,
|
||||
"disk_image": disk_image,
|
||||
"seed_image": seed_image,
|
||||
}
|
||||
for key, value in config_values.items():
|
||||
# QEMU's config file parser has no escape syntax for quoted values.
|
||||
if any(char in str(value) for char in '"\n'):
|
||||
die(f"{key} must not contain double quotes or newlines: {value}")
|
||||
for config_file in (qemu_config_template, qemu_arch_config):
|
||||
if not config_file.is_file():
|
||||
die(f"QEMU config file was not found: {config_file}")
|
||||
try:
|
||||
qemu_config_text = string.Template(qemu_config_template.read_text()).substitute(
|
||||
{key: str(value) for key, value in config_values.items()})
|
||||
except (KeyError, ValueError) as error:
|
||||
die(f"invalid placeholder in {qemu_config_template}: {error}")
|
||||
qemu_config.write_text(qemu_config_text)
|
||||
|
||||
qemu_args = [
|
||||
"-readconfig", str(qemu_config),
|
||||
"-readconfig", str(qemu_arch_config),
|
||||
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
||||
"-cpu", "max",
|
||||
# The config file's [netdev] does not stop QEMU from adding its default
|
||||
# NIC, a second user network without the SSH port forward.
|
||||
"-nic", "none",
|
||||
"-display", display_backend,
|
||||
]
|
||||
|
||||
print(f"Starting Ubuntu 26.04 ({host_arch}) from {qemu_config_template} "
|
||||
f"and {qemu_arch_config}.", flush=True)
|
||||
# The SSH port forward lives in the config file, e.g. hostfwd = "tcp::2225-:22".
|
||||
ssh_forward = re.search(r'hostfwd\s*=\s*"tcp:[^:"]*:(\d+)-[^:"]*:22"', qemu_config_text)
|
||||
if ssh_forward:
|
||||
print(f"SSH is forwarded to localhost:{ssh_forward.group(1)}: "
|
||||
f"ssh -p {ssh_forward.group(1)} guofang@localhost", flush=True)
|
||||
|
||||
if daemonize:
|
||||
log_file.write_text("")
|
||||
qemu_args += [
|
||||
"-serial", f"file:{log_file}",
|
||||
"-monitor", "none",
|
||||
"-daemonize",
|
||||
"-pidfile", str(pid_file),
|
||||
]
|
||||
run([qemu_binary, *qemu_args, *extra_args])
|
||||
print(f"VM started (PID {pid_file.read_text().strip()}); console log: {log_file}")
|
||||
else:
|
||||
os.execvp(qemu_binary, [qemu_binary, *qemu_args, *extra_args])
|
||||
+2
-1
@@ -7,7 +7,8 @@ Each operating system has an independent NoCloud configuration and generated
|
||||
seediso/
|
||||
├── debian13/
|
||||
├── almalinux10/
|
||||
└── fedora44/
|
||||
├── fedora44/
|
||||
└── ubuntu2604/
|
||||
```
|
||||
|
||||
Keeping per-OS metadata separate allows each VM to use a distinct hostname and
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
seed.iso
|
||||
*.iso
|
||||
@@ -0,0 +1,21 @@
|
||||
# Ubuntu 26.04 seed.iso
|
||||
|
||||
This directory contains the cloud-init NoCloud configuration used by
|
||||
`run-ubuntu2604.py`.
|
||||
|
||||
1. Edit `user-data`, `meta-data`, and (if needed) `network-config`.
|
||||
2. Build the image:
|
||||
|
||||
```bash
|
||||
./seediso/ubuntu2604/build-seed.sh
|
||||
```
|
||||
|
||||
To choose another output path:
|
||||
|
||||
```bash
|
||||
./seediso/ubuntu2604/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
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
output="${1:-${script_dir}/seed.iso}"
|
||||
files=(user-data meta-data)
|
||||
|
||||
# Public key injected into user-data: $SSH_PUBKEY_FILE, else the first
|
||||
# ~/.ssh/id_*.pub, else the first key offered by ssh-agent.
|
||||
pubkey=""
|
||||
if [[ -n "${SSH_PUBKEY_FILE:-}" ]]; then
|
||||
pubkey="$(head -n1 -- "${SSH_PUBKEY_FILE}")"
|
||||
else
|
||||
for f in "${HOME}"/.ssh/id_ed25519.pub "${HOME}"/.ssh/id_*.pub; do
|
||||
if [[ -f "$f" ]]; then pubkey="$(head -n1 -- "$f")"; break; fi
|
||||
done
|
||||
if [[ -z "$pubkey" ]] && command -v ssh-add >/dev/null 2>&1; then
|
||||
pubkey="$(ssh-add -L 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
fi
|
||||
if [[ "$pubkey" != ssh-* && "$pubkey" != ecdsa-* && "$pubkey" != sk-* ]]; then
|
||||
printf 'Error: no SSH public key found; set SSH_PUBKEY_FILE.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${work}"' EXIT
|
||||
template="$(<"${script_dir}/user-data")"
|
||||
printf '%s\n' "${template//@SSH_PUBKEY@/${pubkey}}" > "${work}/user-data"
|
||||
cp -- "${script_dir}/meta-data" "${work}/meta-data"
|
||||
if [[ -f "${script_dir}/network-config" ]]; then
|
||||
cp -- "${script_dir}/network-config" "${work}/network-config"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname -- "$output")"
|
||||
|
||||
if [[ -f "${script_dir}/network-config" ]]; then
|
||||
files+=(network-config)
|
||||
fi
|
||||
|
||||
if command -v cloud-localds >/dev/null 2>&1; then
|
||||
args=()
|
||||
if [[ -f "${script_dir}/network-config" ]]; then
|
||||
args+=(--network-config="${work}/network-config")
|
||||
fi
|
||||
cloud-localds "${args[@]}" "${output}" \
|
||||
"${work}/user-data" "${work}/meta-data"
|
||||
elif command -v xorriso >/dev/null 2>&1; then
|
||||
xorriso -as mkisofs -quiet -volid cidata -joliet -rock \
|
||||
-output "${output}" "${files[@]/#/${work}/}"
|
||||
elif command -v genisoimage >/dev/null 2>&1; then
|
||||
genisoimage -quiet -output "${output}" -volid cidata -joliet -rock \
|
||||
"${files[@]/#/${work}/}"
|
||||
elif command -v mkisofs >/dev/null 2>&1; then
|
||||
mkisofs -quiet -output "${output}" -volid cidata -joliet -rock \
|
||||
"${files[@]/#/${work}/}"
|
||||
else
|
||||
printf 'Error: install cloud-image-utils, xorriso, genisoimage, or mkisofs.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Created %s\n' "${output}"
|
||||
@@ -0,0 +1,2 @@
|
||||
instance-id: ubuntu2604-host-001
|
||||
local-hostname: ubuntu2604-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,46 @@
|
||||
#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_PUBKEY@
|
||||
hashed_passwd: $6$0NtCERV3eO12t9oJ$nlFE2sSc66nFfuD5X0xMnOla1cuzOnHfeGNRY5X7dCDHg.WzWVmuRe7M17Puj44.eCWLnJAWt4WNjd9vP9X0D0
|
||||
|
||||
chpasswd:
|
||||
expire: false
|
||||
|
||||
# amd64 packages are in the ubuntu archive, arm64 packages in ubuntu-ports.
|
||||
apt:
|
||||
primary:
|
||||
- arches: [amd64, i386]
|
||||
uri: "https://mirrors.ustc.edu.cn/ubuntu"
|
||||
- arches: [default]
|
||||
uri: "https://mirrors.ustc.edu.cn/ubuntu-ports"
|
||||
security:
|
||||
- arches: [amd64, i386]
|
||||
uri: "https://mirrors.ustc.edu.cn/ubuntu"
|
||||
- arches: [default]
|
||||
uri: "https://mirrors.ustc.edu.cn/ubuntu-ports"
|
||||
|
||||
package_update: true
|
||||
|
||||
packages:
|
||||
- ubuntu-desktop-minimal
|
||||
- ibus-table-wubi
|
||||
|
||||
runcmd:
|
||||
- systemctl set-default graphical.target
|
||||
|
||||
power_state:
|
||||
delay: now
|
||||
mode: reboot
|
||||
message: "GNOME installation completed; rebooting into graphical mode."
|
||||
condition: true
|
||||
+1
-1
Submodule vm-images updated: 5ba114f5ad...6286495d27.
Reference in new issue
Block a user