From b0ce72719d36c929da230ae82c74271c9e6f290d Mon Sep 17 00:00:00 2001 From: JieXu Date: Sat, 15 Nov 2025 20:04:08 +0800 Subject: [PATCH] Update reinstall-freebsd-linux.sh --- reinstall-freebsd-linux.sh | 654 +++++++++++++++++++++++++++++++++++++ 1 file changed, 654 insertions(+) diff --git a/reinstall-freebsd-linux.sh b/reinstall-freebsd-linux.sh index 8b13789..a33a12a 100644 --- a/reinstall-freebsd-linux.sh +++ b/reinstall-freebsd-linux.sh @@ -1 +1,655 @@ +#!/usr/bin/env bash +# reinstall-freebsd-linux.sh +# Reinstall system on Linux / FreeBSD using DD + cloud-init (NoCloud) with: +# - freebsd +# - rocky +# - almalinux +# - fedora +# - redhat +# +# All target systems use cloud-init to inject: +# - root password (--password) +# - SSH public key(s) (--ssh-key, multiple) +# - SSH port (--ssh-port) +# - optional FRPC config (--frpc-toml) stored as EFI:/nocloud/frpc.toml +# +# Requirements: +# - Run with bash: bash reinstall-freebsd-linux.sh ... +# - Needs dd, xz, qemu-img, mount, and curl or wget or fetch +set -eE + +SCRIPT_NAME="${0##*/}" + +error() { + echo "ERROR: $*" >&2 + exit 1 +} + +warn() { + echo "WARN: $*" >&2 +} + +info() { + echo "==> $*" +} + +usage() { + cat </dev/null 2>&1; then + curl -L --fail -o "$dst" "$url" + elif command -v wget >/dev/null 2>&1; then + wget -O "$dst" "$url" + elif command -v fetch >/dev/null 2>&1; then + fetch -o "$dst" "$url" + else + error "No curl/wget/fetch found, cannot download: $url" + fi +} + +detect_os_arch() { + OS=$(uname -s) + ARCH=$(uname -m) + + case "$OS" in + Linux|FreeBSD) ;; + *) error "Unsupported OS: $OS (only Linux and FreeBSD are supported)" ;; + esac + + case "$ARCH" in + x86_64|amd64) MACHINE_ARCH="x86_64" ;; + aarch64|arm64) MACHINE_ARCH="aarch64" ;; + *) + warn "Unknown arch: $ARCH, image URL selection may fail" + MACHINE_ARCH="$ARCH" + ;; + esac +} + +# Parse ssh-key: supports inline / URL / github / gitlab / file +parse_ssh_key() { + local val="$1" + local val_lower key_url tmpfile ssh_key + + ssh_key_error_and_exit() { + error "$1 +Available options: + --ssh-key \"ssh-rsa ...\" + --ssh-key \"ssh-ed25519 ...\" + --ssh-key \"ecdsa-sha2-nistp256/384/521 ...\" + --ssh-key github:your_username + --ssh-key gitlab:your_username + --ssh-key http://path/to/public_key + --ssh-key https://path/to/public_key + --ssh-key /path/to/public_key + --ssh-key C:\\path\\to\\public_key (not supported directly, copy to a local path first)" + } + + is_valid_ssh_key() { + grep -qE '^(ecdsa-sha2-nistp(256|384|521)|ssh-(ed25519|rsa)) ' <<<"$1" + } + + val_lower=$(to_lower <<<"$val") + + case "$val_lower" in + github:*|gitlab:*|http://*|https://*) + if [[ "$val_lower" == http* ]]; then + key_url="$val" + else + IFS=: read -r site user <<<"$val" + [ -n "$user" ] || ssh_key_error_and_exit "Need a username for $site" + key_url="https://$site.com/$user.keys" + fi + info "Downloading SSH key from: $key_url" + tmpfile=$(mktemp /tmp/reinstall-sshkey.XXXXXX) + if ! http_download "$key_url" "$tmpfile"; then + rm -f "$tmpfile" + ssh_key_error_and_exit "Failed to download SSH key from $key_url" + fi + ssh_key=$(grep -m1 -E '^(ecdsa-sha2-nistp(256|384|521)|ssh-(ed25519|rsa)) ' "$tmpfile" || true) + rm -f "$tmpfile" + [ -n "$ssh_key" ] || ssh_key_error_and_exit "No valid SSH key found in $key_url" + ;; + *) + # Reject Windows-style paths explicitly + if [[ "$val" =~ ^[A-Za-z]:\\ ]]; then + ssh_key_error_and_exit "Windows path is not supported, please copy the key file to local filesystem and use /path/to/public_key" + fi + # Inline key or local file + if is_valid_ssh_key "$val"; then + ssh_key="$val" + else + if [ ! -f "$val" ]; then + ssh_key_error_and_exit "SSH key/file/url \"$val\" is invalid (file not found)" + fi + ssh_key=$(grep -m1 -E '^(ecdsa-sha2-nistp(256|384|521)|ssh-(ed25519|rsa)) ' "$val" || true) + [ -n "$ssh_key" ] || ssh_key_error_and_exit "No valid SSH key found in file: $val" + fi + ;; + esac + + echo "$ssh_key" +} + +# Choose default image URL based on target OS, version, and host arch +get_default_image_url() { + local os="$1" ver="$2" + + case "$os" in + freebsd) + case "$ver" in + 14|14.*) + case "$MACHINE_ARCH" in + x86_64) + echo "https://download.freebsd.org/releases/VM-IMAGES/14.3-RELEASE/amd64/Latest/FreeBSD-14.3-RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2.xz" + ;; + aarch64) + echo "https://download.freebsd.org/releases/VM-IMAGES/14.3-RELEASE/aarch64/Latest/FreeBSD-14.3-RELEASE-arm64-aarch64-BASIC-CLOUDINIT-ufs.qcow2.xz" + ;; + *) + error "Current arch $MACHINE_ARCH is not supported for automatic FreeBSD image selection, please specify --img manually" + ;; + esac + ;; + *) + error "Unsupported FreeBSD version: $ver (only 14.x is baked in; use --img for others)" + ;; + esac + ;; + rocky) + case "$ver" in + 10) + case "$MACHINE_ARCH" in + x86_64) + echo "https://download.rockylinux.org/pub/rocky/10/images/x86_64/Rocky-10-EC2-LVM.latest.x86_64.qcow2" + ;; + *) + error "Rocky 10 default image is only provided for x86_64; use --img for other arches" + ;; + esac + ;; + *) + error "Unsupported Rocky version: $ver (future: add rocky 9, etc.)" + ;; + esac + ;; + almalinux) + case "$ver" in + 10) + case "$MACHINE_ARCH" in + x86_64) + echo "https://repo.almalinux.org/almalinux/10/cloud/x86_64/images/AlmaLinux-10-GenericCloud-latest.x86_64.qcow2" + ;; + aarch64) + echo "https://repo.almalinux.org/almalinux/10/cloud/aarch64/images/AlmaLinux-10-GenericCloud-latest.aarch64.qcow2" + ;; + *) + error "Current arch $MACHINE_ARCH is not supported for automatic AlmaLinux image selection, please specify --img manually" + ;; + esac + ;; + *) + error "Unsupported AlmaLinux version: $ver" + ;; + esac + ;; + fedora) + case "$ver" in + 43) + case "$MACHINE_ARCH" in + x86_64) + echo "https://download.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2" + ;; + aarch64) + echo "https://download.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-43-1.6.aarch64.qcow2" + ;; + *) + error "Current arch $MACHINE_ARCH is not supported for automatic Fedora image selection, please specify --img manually" + ;; + esac + ;; + *) + error "Unsupported Fedora version: $ver" + ;; + esac + ;; + redhat) + # Red Hat image must be supplied by user via --img + echo "" + ;; + *) + error "Unknown target OS: $os" + ;; + esac +} + +find_efi_partition() { + # Guess EFI partition name from disk path (p1 vs 1) + local disk="$1" part + case "$disk" in + */nvme*|*/*nvd*) + part="${disk}p1" + ;; + *) + # /dev/sda /dev/vda /dev/ada0 etc. + if [[ "$(uname -s)" == "FreeBSD" ]]; then + part="${disk}p1" + else + part="${disk}1" + fi + ;; + esac + echo "$part" +} + +write_nocloud_seed() { + local os="$1" meta_path="$2" user_path="$3" + + mkdir -p "$(dirname "$meta_path")" + + # meta-data + cat >"$meta_path" </dev/null || \ + sed -i 's/^Port .*/Port ${SSH_PORT}/' /etc/ssh/sshd_config 2>/dev/null || \ + sed -i '' 's/^#Port .*/Port ${SSH_PORT}/' /etc/ssh/sshd_config 2>/dev/null || \ + sed -i '' 's/^Port .*/Port ${SSH_PORT}/' /etc/ssh/sshd_config 2>/dev/null || true + fi + service sshd restart 2>/dev/null || systemctl restart sshd 2>/dev/null || true +EOF + fi + + if [ -n "$FRPC_PRESENT" ]; then + cat <<'EOF' + - | + # If EFI nocloud contains frpc.toml, copy to /etc/frp and try to start frpc + if [ -f /boot/efi/nocloud/frpc.toml ]; then + mkdir -p /etc/frp + cp /boot/efi/nocloud/frpc.toml /etc/frp/frpc.toml + (frpc -c /etc/frp/frpc.toml || /usr/local/bin/frpc -c /etc/frp/frpc.toml || true) & + fi +EOF + fi + } >"$user_path" +} + +# ----------------- main ----------------- + +[ $# -lt 1 ] && usage + +TARGET_OS=$(echo "$1" | to_lower) +shift || true + +TARGET_VER="" +IMG_URL="" +DISK="" +PASSWORD="" +SSH_KEYS_ALL="" +SSH_PORT="" +WEB_PORT="" +FRPC_TOML="" +FRPC_PRESENT="" +HOLD="0" + +# If the next positional arg is numeric, treat it as version for specific OSes. +if [ $# -gt 0 ] && [[ "$1" =~ ^[0-9]+$ ]]; then + case "$TARGET_OS" in + freebsd|rocky|almalinux|fedora) + TARGET_VER="$1" + shift + ;; + redhat) + error "Do not specify a version for redhat. Use: $SCRIPT_NAME redhat --img URL --disk /dev/XXX ..." + ;; + *) + ;; + esac +fi + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + ;; + --disk) + shift + [ -n "$1" ] || error "Need value for --disk" + DISK="$1" + ;; + --disk=*) + DISK="${1#*=}" + ;; + --img) + shift + [ -n "$1" ] || error "Need value for --img" + IMG_URL="$1" + ;; + --img=*) + IMG_URL="${1#*=}" + ;; + --password|--passwd) + shift + [ -n "$1" ] || error "Need value for --password" + PASSWORD="$1" + ;; + --ssh-key|--public-key) + shift + [ -n "$1" ] || error "Need value for --ssh-key" + key_line=$(parse_ssh_key "$1") + if [ -n "$SSH_KEYS_ALL" ]; then + SSH_KEYS_ALL+=$'\n' + fi + SSH_KEYS_ALL+="$key_line" + ;; + --ssh-port) + shift + [ -n "$1" ] || error "Need value for --ssh-port" + is_port_valid "$1" || error "Invalid --ssh-port: $1" + SSH_PORT="$1" + ;; + --web-port) + shift + [ -n "$1" ] || error "Need value for --web-port" + is_port_valid "$1" || error "Invalid --web-port: $1" + WEB_PORT="$1" + ;; + --frpc-toml) + shift + [ -n "$1" ] || error "Need value for --frpc-toml" + FRPC_TOML="$1" + ;; + --hold) + shift + [ -n "$1" ] || error "Need value for --hold" + [[ "$1" == "1" || "$1" == "2" ]] || error "Invalid --hold: $1 (must be 1 or 2)" + HOLD="$1" + ;; + *) + error "Unknown argument: $1" + ;; + esac + shift || true +done + +[ -n "$DISK" ] || error "You must specify --disk, e.g. --disk /dev/sda or --disk /dev/ada0" + +# Normalize disk path +if [[ "$DISK" != /dev/* ]]; then + DISK="/dev/$DISK" +fi + +if [ ! -b "$DISK" ] && [ ! -c "$DISK" ]; then + error "Target disk $DISK does not exist or is not a block/char device" +fi + +detect_os_arch + +# Default versions +if [ -z "$TARGET_VER" ]; then + case "$TARGET_OS" in + freebsd) TARGET_VER="14" ;; + rocky) TARGET_VER="10" ;; + almalinux) TARGET_VER="10" ;; + fedora) TARGET_VER="43" ;; + redhat) TARGET_VER="" ;; # user must provide image + *) ;; + esac +fi + +# Get default image URL (redhat requires user-supplied --img) +if [ -z "$IMG_URL" ]; then + IMG_URL=$(get_default_image_url "$TARGET_OS" "$TARGET_VER") + if [ -z "$IMG_URL" ] && [ "$TARGET_OS" = "redhat" ]; then + error "For redhat you must specify image URL with --img" + fi +fi + +info "Host: OS=$OS ARCH=$ARCH ($MACHINE_ARCH)" +info "Target: $TARGET_OS ${TARGET_VER:-"(no version)"}" +info "Disk: $DISK" +info "Image URL: $IMG_URL" + +if [ "$HOLD" = "1" ]; then + info "--hold 1 is set: only parameter check and summary, no download or disk write." + exit 0 +fi + +TMPDIR=$(mktemp -d /tmp/reinstall-cloudinit.XXXXXX) +trap 'rm -rf "$TMPDIR"' EXIT + +IMG_QCOW="$TMPDIR/image.qcow2" +IMG_RAW="$TMPDIR/image.raw" + +# Download image +info "Downloading image..." +http_download "$IMG_URL" "$IMG_QCOW" + +# Check if it's xz compressed +if file "$IMG_QCOW" | grep -qi 'xz compressed'; then + info "Detected xz compressed image, decompressing..." + if ! command -v xz >/dev/null 2>&1; then + error "xz not found, please install xz-utils / xz" + fi + mv "$IMG_QCOW" "$IMG_QCOW.xz" + xz -dc "$IMG_QCOW.xz" >"$IMG_QCOW" +fi + +# Convert to raw using qemu-img +info "Converting qcow2 to raw with qemu-img..." +if ! command -v qemu-img >/dev/null 2>&1; then + error "qemu-img not found; install qemu-img (Linux) or qemu-tools (FreeBSD) first" +fi + +qemu-img convert -O raw "$IMG_QCOW" "$IMG_RAW" + +# Final confirmation +echo +echo "WARNING: dd will be run on $DISK. ALL DATA ON THIS DISK WILL BE LOST!" +read -r -p "Type 'yes' to continue: " ans +if [ "$ans" != "yes" ]; then + error "Operation cancelled by user." +fi + +# dd to disk +info "Writing image to disk with dd, this may take a while..." +dd if="$IMG_RAW" of="$DISK" bs=4M conv=fsync status=progress +sync +info "dd finished." + +# Try to refresh partition table (best-effort) +if command -v partprobe >/dev/null 2>&1; then + partprobe "$DISK" || true +elif command -v blockdev >/dev/null 2>&1; then + blockdev --rereadpt "$DISK" || true +fi + +sleep 2 + +# Find and mount EFI partition +EFI_PART=$(find_efi_partition "$DISK") +info "Trying EFI partition: $EFI_PART" + +MNT_EFI="$TMPDIR/efi" +mkdir -p "$MNT_EFI" + +if [[ "$OS" == "FreeBSD" ]]; then + if ! mount -t msdosfs "$EFI_PART" "$MNT_EFI" 2>/dev/null; then + warn "Failed to mount EFI partition $EFI_PART, skipping cloud-init NoCloud injection." + EFI_PART="" + fi +else + if ! mount "$EFI_PART" "$MNT_EFI" 2>/dev/null; then + # Some systems require explicit vfat + if ! mount -t vfat "$EFI_PART" "$MNT_EFI" 2>/dev/null && ! mount -t msdos "$EFI_PART" "$MNT_EFI" 2>/dev/null; then + warn "Failed to mount EFI partition $EFI_PART, skipping cloud-init NoCloud injection." + EFI_PART="" + fi + fi +fi + +if [ -n "$EFI_PART" ]; then + NOCLOUD_DIR="$MNT_EFI/nocloud" + mkdir -p "$NOCLOUD_DIR" + + # Handle FRPC config if present + if [ -n "$FRPC_TOML" ]; then + FRPC_PRESENT=1 + if [[ "$FRPC_TOML" =~ ^https?:// ]]; then + info "Downloading FRPC config: $FRPC_TOML" + if ! http_download "$FRPC_TOML" "$NOCLOUD_DIR/frpc.toml"; then + warn "Failed to download FRPC config, ignoring" + FRPC_PRESENT="" + fi + elif [ -f "$FRPC_TOML" ]; then + info "Copying FRPC config from: $FRPC_TOML" + cp "$FRPC_TOML" "$NOCLOUD_DIR/frpc.toml" + else + warn "Invalid FRPC config path: $FRPC_TOML, ignoring" + FRPC_PRESENT="" + fi + fi + + info "Writing NoCloud seed to EFI:/nocloud/ ..." + write_nocloud_seed "$TARGET_OS" "$NOCLOUD_DIR/meta-data" "$NOCLOUD_DIR/user-data" + + sync + umount "$MNT_EFI" || true +else + warn "EFI could not be mounted; target system can still boot, but cloud-init configuration may not be applied." +fi + +info "Image write and cloud-init NoCloud injection completed." + +if [ "$HOLD" = "2" ]; then + info "--hold 2 is set: will NOT reboot automatically. You can inspect or chroot into the new system manually." + exit 0 +fi + +echo +echo "You can now reboot into the new system, for example:" +if [ "$OS" = "FreeBSD" ]; then + echo " shutdown -r now" +else + echo " reboot" +fi + +exit 0