772 lines
24 KiB
Bash
Executable File
772 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# incusos-seed - Generate IncusOS seed archives for installation customization
|
|
#
|
|
# Creates tar archives or FAT images containing seed configuration files
|
|
# for IncusOS installations. Supports all seed file types including install,
|
|
# applications, Incus preseed, Operations Center, network, and update config.
|
|
#
|
|
# Usage: incusos-seed [OPTIONS]
|
|
# Run 'incusos-seed --help' for full usage information.
|
|
|
|
set -euo pipefail
|
|
|
|
readonly VERSION="0.1.0"
|
|
readonly SCRIPT_NAME="incusos-seed"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Color and formatting
|
|
# ---------------------------------------------------------------------------
|
|
|
|
setup_colors() {
|
|
if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
|
|
fi
|
|
}
|
|
|
|
info() { echo -e "${BLUE}[info]${RESET} $*"; }
|
|
success() { echo -e "${GREEN}[ok]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; }
|
|
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
|
|
step() { echo -e "${CYAN}[step]${RESET} ${BOLD}$*${RESET}"; }
|
|
detail() { echo -e "${DIM} $*${RESET}"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Help
|
|
# ---------------------------------------------------------------------------
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Generate IncusOS seed archives
|
|
|
|
${BOLD}USAGE${RESET}
|
|
${SCRIPT_NAME} [OPTIONS]
|
|
|
|
${BOLD}OPTIONS${RESET}
|
|
${BOLD}-A, --app${RESET} APP Application to configure: ${CYAN}incus${RESET}, ${CYAN}operations-center${RESET}
|
|
(default: incus)
|
|
${BOLD}-d, --defaults${RESET} Apply default configuration (ZFS pool, bridge, port 8443)
|
|
${BOLD}-c, --cert${RESET} FILE Path to client certificate PEM file
|
|
(default: auto-detect from local Incus installation)
|
|
${BOLD}--no-cert${RESET} Do not inject any client certificate
|
|
${BOLD}-o, --output${RESET} FILE Output file path (default: incusos-seed.tar)
|
|
|
|
${BOLD}Install options:${RESET}
|
|
${BOLD}--disk-target${RESET} ID Target disk ID pattern for /dev/disk/by-id/ (e.g. "virtio-*")
|
|
${BOLD}--force-install${RESET} Overwrite existing installation without prompting
|
|
${BOLD}--force-reboot${RESET} Automatically reboot after installation
|
|
${BOLD}--missing-tpm${RESET} Allow installation without TPM 2.0
|
|
${BOLD}--missing-secureboot${RESET} Allow installation without Secure Boot
|
|
|
|
${BOLD}Network options:${RESET}
|
|
${BOLD}--hostname${RESET} NAME Set system hostname
|
|
${BOLD}--domain${RESET} NAME Set system domain
|
|
${BOLD}--dns${RESET} SERVERS Comma-separated DNS servers (e.g. "8.8.8.8,1.1.1.1")
|
|
|
|
${BOLD}Update options:${RESET}
|
|
${BOLD}--channel${RESET} CHANNEL Update channel: ${CYAN}stable${RESET}, ${CYAN}testing${RESET} (default: stable)
|
|
${BOLD}--auto-reboot${RESET} Enable automatic reboot for updates
|
|
|
|
${BOLD}Output options:${RESET}
|
|
${BOLD}-f, --format${RESET} FORMAT Output format: ${CYAN}tar${RESET}, ${CYAN}iso${RESET}, ${CYAN}fat${RESET} (default: tar)
|
|
tar: for use with flasher-tool --seed
|
|
iso: ISO 9660 image with SEED_DATA label for CD-ROM
|
|
fat: FAT image with SEED_DATA label for external media
|
|
${BOLD}--fat-size${RESET} SIZE FAT image size in MiB (default: 10)
|
|
|
|
${BOLD}General:${RESET}
|
|
${BOLD}--dry-run${RESET} Show generated seed files without creating output
|
|
${BOLD}-q, --quiet${RESET} Suppress informational output
|
|
${BOLD}-h, --help${RESET} Show this help message
|
|
${BOLD}-V, --version${RESET} Show version
|
|
|
|
${BOLD}EXAMPLES${RESET}
|
|
# Standalone Incus with defaults and auto-detected certificate
|
|
${SCRIPT_NAME} --defaults
|
|
|
|
# Operations Center with explicit certificate
|
|
${SCRIPT_NAME} --app operations-center --defaults --cert ~/.config/incus/client.crt
|
|
|
|
# Cluster node (no defaults) with forced install on virtio disk
|
|
${SCRIPT_NAME} --disk-target 'virtio-*' --force-install --force-reboot
|
|
|
|
# Create an ISO 9660 image for CD-ROM seed delivery (e.g. Proxmox VMs)
|
|
${SCRIPT_NAME} --format iso --defaults --output seed.iso
|
|
|
|
# Create a FAT image for SEED_DATA partition
|
|
${SCRIPT_NAME} --format fat --defaults --output seed.img
|
|
|
|
# Preview what would be generated
|
|
${SCRIPT_NAME} --defaults --dry-run
|
|
|
|
${BOLD}SEED FILES REFERENCE${RESET}
|
|
The following files are generated inside the archive:
|
|
|
|
${CYAN}install.yaml${RESET} Installation target, security, and reboot settings
|
|
${CYAN}applications.yaml${RESET} Application selection (incus, operations-center)
|
|
${CYAN}incus.yaml${RESET} Incus preseed (defaults, certificates)
|
|
${CYAN}operations-center.yaml${RESET} Operations Center config (when --app=operations-center)
|
|
${CYAN}network.yaml${RESET} Network configuration (hostname, DNS)
|
|
${CYAN}update.yaml${RESET} Update channel and reboot policy
|
|
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Argument parsing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Defaults
|
|
APP="incus"
|
|
APPLY_DEFAULTS=false
|
|
CERT_FILE=""
|
|
NO_CERT=false
|
|
OUTPUT=""
|
|
DISK_TARGET=""
|
|
FORCE_INSTALL=false
|
|
FORCE_REBOOT=false
|
|
MISSING_TPM=false
|
|
MISSING_SECUREBOOT=false
|
|
HOSTNAME_OPT=""
|
|
DOMAIN_OPT=""
|
|
DNS_SERVERS=""
|
|
CHANNEL="stable"
|
|
AUTO_REBOOT=false
|
|
FORMAT="tar"
|
|
FAT_SIZE=10
|
|
DRY_RUN=false
|
|
QUIET=false
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-A|--app)
|
|
APP="${2:?'--app requires a value'}"
|
|
shift 2
|
|
;;
|
|
-d|--defaults)
|
|
APPLY_DEFAULTS=true
|
|
shift
|
|
;;
|
|
-c|--cert)
|
|
CERT_FILE="${2:?'--cert requires a file path'}"
|
|
shift 2
|
|
;;
|
|
--no-cert)
|
|
NO_CERT=true
|
|
shift
|
|
;;
|
|
-o|--output)
|
|
OUTPUT="${2:?'--output requires a file path'}"
|
|
shift 2
|
|
;;
|
|
--disk-target)
|
|
DISK_TARGET="${2:?'--disk-target requires a value'}"
|
|
shift 2
|
|
;;
|
|
--force-install)
|
|
FORCE_INSTALL=true
|
|
shift
|
|
;;
|
|
--force-reboot)
|
|
FORCE_REBOOT=true
|
|
shift
|
|
;;
|
|
--missing-tpm)
|
|
MISSING_TPM=true
|
|
shift
|
|
;;
|
|
--missing-secureboot)
|
|
MISSING_SECUREBOOT=true
|
|
shift
|
|
;;
|
|
--hostname)
|
|
HOSTNAME_OPT="${2:?'--hostname requires a value'}"
|
|
shift 2
|
|
;;
|
|
--domain)
|
|
DOMAIN_OPT="${2:?'--domain requires a value'}"
|
|
shift 2
|
|
;;
|
|
--dns)
|
|
DNS_SERVERS="${2:?'--dns requires a value'}"
|
|
shift 2
|
|
;;
|
|
--channel)
|
|
CHANNEL="${2:?'--channel requires a value'}"
|
|
shift 2
|
|
;;
|
|
--auto-reboot)
|
|
AUTO_REBOOT=true
|
|
shift
|
|
;;
|
|
-f|--format)
|
|
FORMAT="${2:?'--format requires a value'}"
|
|
shift 2
|
|
;;
|
|
--fat-size)
|
|
FAT_SIZE="${2:?'--fat-size requires a value'}"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
-q|--quiet)
|
|
QUIET=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
;;
|
|
-V|--version)
|
|
echo "${SCRIPT_NAME} v${VERSION}"
|
|
exit 0
|
|
;;
|
|
-*)
|
|
error "Unknown option: $1"
|
|
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
|
|
exit 1
|
|
;;
|
|
*)
|
|
error "Unexpected argument: $1"
|
|
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
validate_args() {
|
|
case "$APP" in
|
|
incus|operations-center) ;;
|
|
*)
|
|
error "Invalid application: ${APP}"
|
|
error "Supported: incus, operations-center"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "$FORMAT" in
|
|
tar|iso|fat) ;;
|
|
*)
|
|
error "Invalid output format: ${FORMAT}"
|
|
error "Supported: tar, iso, fat"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "$CHANNEL" in
|
|
stable|testing) ;;
|
|
*)
|
|
error "Invalid channel: ${CHANNEL}"
|
|
error "Supported: stable, testing"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [[ -n "$CERT_FILE" ]] && [[ "$NO_CERT" == true ]]; then
|
|
error "Cannot use both --cert and --no-cert"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "$CERT_FILE" ]] && [[ ! -f "$CERT_FILE" ]]; then
|
|
error "Certificate file not found: ${CERT_FILE}"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$FORMAT" == "iso" ]]; then
|
|
if ! command -v genisoimage &>/dev/null; then
|
|
error "ISO 9660 image creation requires genisoimage but it was not found"
|
|
error "Install with: sudo apt install genisoimage"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$FORMAT" == "fat" ]]; then
|
|
# mkfs.fat lives in /usr/sbin or /sbin which may not be in PATH for non-root users
|
|
local has_mkfs_fat=false
|
|
if command -v mkfs.fat &>/dev/null || [[ -x /usr/sbin/mkfs.fat ]] || [[ -x /sbin/mkfs.fat ]]; then
|
|
has_mkfs_fat=true
|
|
fi
|
|
if ! command -v mcopy &>/dev/null && [[ "$has_mkfs_fat" != true ]]; then
|
|
error "FAT image creation requires mtools (mcopy) or dosfstools (mkfs.fat)"
|
|
error "Install with: sudo apt install mtools dosfstools"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Set default output filename
|
|
if [[ -z "$OUTPUT" ]]; then
|
|
case "$FORMAT" in
|
|
tar) OUTPUT="incusos-seed.tar" ;;
|
|
iso) OUTPUT="incusos-seed.iso" ;;
|
|
fat) OUTPUT="incusos-seed.img" ;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Certificate detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CERT_CONTENT=""
|
|
|
|
detect_client_cert() {
|
|
if [[ "$NO_CERT" == true ]]; then
|
|
[[ "$QUIET" != true ]] && info "Skipping certificate injection (--no-cert)"
|
|
return
|
|
fi
|
|
|
|
if [[ -n "$CERT_FILE" ]]; then
|
|
step "Loading client certificate from ${CERT_FILE}"
|
|
CERT_CONTENT=$(<"$CERT_FILE")
|
|
if [[ "$CERT_CONTENT" != *"BEGIN CERTIFICATE"* ]]; then
|
|
error "File does not appear to be a PEM certificate: ${CERT_FILE}"
|
|
exit 1
|
|
fi
|
|
success "Certificate loaded"
|
|
return
|
|
fi
|
|
|
|
# Auto-detection: prefer reading cert files directly (works with all
|
|
# Incus versions including 6.0 LTS which lacks 'get-client-certificate').
|
|
step "Auto-detecting client certificate"
|
|
|
|
# 1) Check common on-disk certificate locations first
|
|
local cert_paths=(
|
|
"${HOME}/.config/incus/client.crt"
|
|
"${HOME}/snap/incus/common/config/client.crt"
|
|
"${HOME}/.config/lxc/client.crt"
|
|
)
|
|
|
|
for path in "${cert_paths[@]}"; do
|
|
if [[ -f "$path" ]]; then
|
|
CERT_CONTENT=$(<"$path")
|
|
if [[ "$CERT_CONTENT" == *"BEGIN CERTIFICATE"* ]]; then
|
|
success "Certificate loaded from ${path}"
|
|
return
|
|
fi
|
|
detail "File exists but does not contain a valid PEM certificate: ${path}"
|
|
fi
|
|
done
|
|
|
|
# 2) Try the incus CLI (only works with Incus >= 6.3)
|
|
if command -v incus &>/dev/null; then
|
|
local cert
|
|
if cert=$(incus remote get-client-certificate 2>/dev/null); then
|
|
CERT_CONTENT="$cert"
|
|
success "Certificate obtained via Incus CLI"
|
|
return
|
|
fi
|
|
local incus_version
|
|
incus_version=$(incus version 2>/dev/null | grep -oP 'Client version: \K[0-9.]+' || echo "unknown")
|
|
detail "Incus CLI v${incus_version} found but no certificate available"
|
|
detail "Generate one with: incus remote list (triggers auto-generation)"
|
|
fi
|
|
|
|
# 3) Nothing found -- report based on context
|
|
if [[ "$APP" == "operations-center" ]]; then
|
|
error "No client certificate found"
|
|
error "Operations Center requires at least one trusted certificate for access"
|
|
echo "" >&2
|
|
error "Options:"
|
|
error " --cert FILE Provide a certificate file directly"
|
|
error " Generate one: incus remote list (creates ~/.config/incus/client.crt)"
|
|
error " --no-cert Skip certificate injection (you will be locked out!)"
|
|
exit 1
|
|
fi
|
|
|
|
warn "No client certificate found"
|
|
if command -v incus &>/dev/null; then
|
|
warn "Run 'incus remote list' to generate a client certificate, then re-run this script"
|
|
else
|
|
warn "The Incus client is not installed on this machine"
|
|
warn "Install it to enable auto-detection (see: https://github.com/lxc/incus)"
|
|
fi
|
|
warn "Use --cert FILE to provide a certificate, or --no-cert to suppress this warning"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Seed file generation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
WORKDIR=""
|
|
|
|
setup_workdir() {
|
|
WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME}-XXXXXX")
|
|
trap 'rm -rf "$WORKDIR"' EXIT
|
|
}
|
|
|
|
generate_install_yaml() {
|
|
local file="${WORKDIR}/install.yaml"
|
|
|
|
step "Generating install.yaml"
|
|
|
|
{
|
|
echo "# IncusOS installation configuration"
|
|
echo "# Generated by ${SCRIPT_NAME} v${VERSION} on $(date -Iseconds)"
|
|
|
|
if [[ "$FORCE_INSTALL" == true ]]; then
|
|
echo "force_install: true"
|
|
fi
|
|
|
|
if [[ "$FORCE_REBOOT" == true ]]; then
|
|
echo "force_reboot: true"
|
|
fi
|
|
|
|
if [[ -n "$DISK_TARGET" ]]; then
|
|
echo "target:"
|
|
echo " id: \"${DISK_TARGET}\""
|
|
fi
|
|
|
|
if [[ "$MISSING_TPM" == true ]] || [[ "$MISSING_SECUREBOOT" == true ]]; then
|
|
echo "security:"
|
|
if [[ "$MISSING_TPM" == true ]]; then
|
|
echo " missing_tpm: true"
|
|
fi
|
|
if [[ "$MISSING_SECUREBOOT" == true ]]; then
|
|
echo " missing_secure_boot: true"
|
|
fi
|
|
fi
|
|
} > "$file"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
detail "--- install.yaml ---"
|
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
|
else
|
|
success "install.yaml"
|
|
fi
|
|
}
|
|
|
|
generate_applications_yaml() {
|
|
local file="${WORKDIR}/applications.yaml"
|
|
|
|
step "Generating applications.yaml"
|
|
|
|
{
|
|
echo "# IncusOS application selection"
|
|
echo "applications:"
|
|
echo " - name: ${APP}"
|
|
} > "$file"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
detail "--- applications.yaml ---"
|
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
|
else
|
|
success "applications.yaml (${APP})"
|
|
fi
|
|
}
|
|
|
|
generate_incus_yaml() {
|
|
local file="${WORKDIR}/incus.yaml"
|
|
|
|
step "Generating incus.yaml"
|
|
|
|
{
|
|
echo "# Incus preseed configuration"
|
|
echo "apply_defaults: ${APPLY_DEFAULTS}"
|
|
|
|
if [[ -n "$CERT_CONTENT" ]]; then
|
|
echo "preseed:"
|
|
echo " certificates:"
|
|
echo " - name: \"auto-injected-client\""
|
|
echo " type: \"client\""
|
|
echo " certificate: |-"
|
|
# Indent each line of the certificate
|
|
while IFS= read -r line; do
|
|
echo " ${line}"
|
|
done <<< "$CERT_CONTENT"
|
|
fi
|
|
} > "$file"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
detail "--- incus.yaml ---"
|
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
|
else
|
|
local desc="defaults=$(bool_str $APPLY_DEFAULTS)"
|
|
[[ -n "$CERT_CONTENT" ]] && desc+=", certificate=injected"
|
|
success "incus.yaml (${desc})"
|
|
fi
|
|
}
|
|
|
|
generate_ops_center_yaml() {
|
|
local file="${WORKDIR}/operations-center.yaml"
|
|
|
|
step "Generating operations-center.yaml"
|
|
|
|
{
|
|
echo "# Operations Center configuration"
|
|
echo "apply_defaults: ${APPLY_DEFAULTS}"
|
|
|
|
if [[ -n "$CERT_CONTENT" ]]; then
|
|
echo "trusted_client_certificates:"
|
|
echo " - |"
|
|
while IFS= read -r line; do
|
|
echo " ${line}"
|
|
done <<< "$CERT_CONTENT"
|
|
fi
|
|
} > "$file"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
detail "--- operations-center.yaml ---"
|
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
|
else
|
|
local desc="defaults=$(bool_str $APPLY_DEFAULTS)"
|
|
[[ -n "$CERT_CONTENT" ]] && desc+=", certificate=injected"
|
|
success "operations-center.yaml (${desc})"
|
|
fi
|
|
}
|
|
|
|
generate_network_yaml() {
|
|
# Only generate if network options were specified
|
|
if [[ -z "$HOSTNAME_OPT" ]] && [[ -z "$DOMAIN_OPT" ]] && [[ -z "$DNS_SERVERS" ]]; then
|
|
return
|
|
fi
|
|
|
|
local file="${WORKDIR}/network.yaml"
|
|
|
|
step "Generating network.yaml"
|
|
|
|
{
|
|
echo "# Network configuration"
|
|
|
|
if [[ -n "$HOSTNAME_OPT" ]] || [[ -n "$DOMAIN_OPT" ]] || [[ -n "$DNS_SERVERS" ]]; then
|
|
echo "dns:"
|
|
[[ -n "$HOSTNAME_OPT" ]] && echo " hostname: \"${HOSTNAME_OPT}\""
|
|
[[ -n "$DOMAIN_OPT" ]] && echo " domain: \"${DOMAIN_OPT}\""
|
|
|
|
if [[ -n "$DNS_SERVERS" ]]; then
|
|
echo " nameservers:"
|
|
IFS=',' read -ra servers <<< "$DNS_SERVERS"
|
|
for srv in "${servers[@]}"; do
|
|
echo " - \"$(echo "$srv" | xargs)\""
|
|
done
|
|
fi
|
|
fi
|
|
} > "$file"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
detail "--- network.yaml ---"
|
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
|
else
|
|
local desc=""
|
|
[[ -n "$HOSTNAME_OPT" ]] && desc+="hostname=${HOSTNAME_OPT}"
|
|
[[ -n "$DNS_SERVERS" ]] && desc+=", dns=${DNS_SERVERS}"
|
|
success "network.yaml (${desc})"
|
|
fi
|
|
}
|
|
|
|
generate_update_yaml() {
|
|
# Only generate if non-default values were specified
|
|
if [[ "$CHANNEL" == "stable" ]] && [[ "$AUTO_REBOOT" == false ]]; then
|
|
return
|
|
fi
|
|
|
|
local file="${WORKDIR}/update.yaml"
|
|
|
|
step "Generating update.yaml"
|
|
|
|
{
|
|
echo "# Update configuration"
|
|
echo "channel: \"${CHANNEL}\""
|
|
echo "auto_reboot: ${AUTO_REBOOT}"
|
|
echo "check_frequency: \"6h\""
|
|
} > "$file"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
detail "--- update.yaml ---"
|
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
|
else
|
|
success "update.yaml (channel=${CHANNEL}, auto_reboot=$(bool_str $AUTO_REBOOT))"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Output creation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
create_tar_output() {
|
|
step "Creating tar archive: ${OUTPUT}"
|
|
|
|
tar -cf "$OUTPUT" -C "$WORKDIR" .
|
|
|
|
local size
|
|
size=$(du -h "$OUTPUT" | cut -f1)
|
|
success "Seed archive created: ${OUTPUT} (${size})"
|
|
}
|
|
|
|
create_iso_output() {
|
|
step "Creating ISO 9660 image: ${OUTPUT}"
|
|
|
|
genisoimage -o "$OUTPUT" -V SEED_DATA -J -r "$WORKDIR"/ 2>/dev/null
|
|
|
|
local size
|
|
size=$(du -h "$OUTPUT" | cut -f1)
|
|
success "ISO 9660 seed image created: ${OUTPUT} (${size})"
|
|
}
|
|
|
|
create_fat_output() {
|
|
step "Creating FAT image: ${OUTPUT} (${FAT_SIZE} MiB)"
|
|
|
|
# Create empty image
|
|
dd if=/dev/zero of="$OUTPUT" bs=1M count="$FAT_SIZE" status=none
|
|
|
|
# mkfs.fat lives in /usr/sbin or /sbin which may not be in PATH for non-root users
|
|
local mkfs_fat=""
|
|
if command -v mkfs.fat &>/dev/null; then
|
|
mkfs_fat="mkfs.fat"
|
|
elif [[ -x /usr/sbin/mkfs.fat ]]; then
|
|
mkfs_fat="/usr/sbin/mkfs.fat"
|
|
elif [[ -x /sbin/mkfs.fat ]]; then
|
|
mkfs_fat="/sbin/mkfs.fat"
|
|
fi
|
|
|
|
if [[ -n "$mkfs_fat" ]]; then
|
|
"$mkfs_fat" -n SEED_DATA "$OUTPUT" >/dev/null
|
|
|
|
if command -v mcopy &>/dev/null; then
|
|
# Use mtools to copy files without mounting
|
|
for f in "${WORKDIR}"/*.yaml; do
|
|
[[ -f "$f" ]] && mcopy -i "$OUTPUT" "$f" "::/$(basename "$f")"
|
|
done
|
|
else
|
|
warn "mtools not installed -- FAT image created but files not copied"
|
|
warn "Install mtools and re-run, or manually mount and copy seed files"
|
|
warn "Files are in: ${WORKDIR}/"
|
|
# Keep workdir around
|
|
trap - EXIT
|
|
return
|
|
fi
|
|
elif command -v mcopy &>/dev/null; then
|
|
# mtools can format too
|
|
mformat -i "$OUTPUT" -v SEED_DATA ::
|
|
for f in "${WORKDIR}"/*.yaml; do
|
|
[[ -f "$f" ]] && mcopy -i "$OUTPUT" "$f" "::/$(basename "$f")"
|
|
done
|
|
fi
|
|
|
|
local size
|
|
size=$(du -h "$OUTPUT" | cut -f1)
|
|
success "FAT seed image created: ${OUTPUT} (${size})"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Utilities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
bool_str() {
|
|
if [[ "$1" == true ]]; then echo "yes"; else echo "no"; fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
print_summary() {
|
|
echo ""
|
|
echo -e "${BOLD}Seed archive summary${RESET}"
|
|
echo -e " Application: ${CYAN}${APP}${RESET}"
|
|
echo -e " Defaults: $(if [[ "$APPLY_DEFAULTS" == true ]]; then echo "${GREEN}yes${RESET}"; else echo "${YELLOW}no${RESET}"; fi)"
|
|
echo -e " Certificate: $(if [[ -n "$CERT_CONTENT" ]]; then echo "${GREEN}injected${RESET}"; else echo "${DIM}none${RESET}"; fi)"
|
|
echo -e " Format: ${FORMAT}"
|
|
echo -e " Output: ${BOLD}${OUTPUT}${RESET}"
|
|
|
|
if [[ -n "$DISK_TARGET" ]]; then
|
|
echo -e " Disk target: ${DISK_TARGET}"
|
|
fi
|
|
if [[ "$MISSING_TPM" == true ]]; then
|
|
echo -e " Security: ${YELLOW}TPM not required${RESET}"
|
|
fi
|
|
if [[ "$MISSING_SECUREBOOT" == true ]]; then
|
|
echo -e " Security: ${YELLOW}Secure Boot not required${RESET}"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
if [[ "$FORMAT" == "tar" ]]; then
|
|
echo -e "${DIM} Use with flasher-tool:${RESET}"
|
|
echo -e " flasher-tool --seed ${OUTPUT}"
|
|
elif [[ "$FORMAT" == "iso" ]]; then
|
|
echo -e "${DIM} Attach as CD-ROM to the VM.${RESET}"
|
|
echo -e "${DIM} IncusOS will detect the SEED_DATA volume label at boot.${RESET}"
|
|
elif [[ "$FORMAT" == "fat" ]]; then
|
|
echo -e "${DIM} Attach as secondary disk/ISO to the VM.${RESET}"
|
|
echo -e "${DIM} IncusOS will detect the SEED_DATA label at boot.${RESET}"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
main() {
|
|
setup_colors
|
|
parse_args "$@"
|
|
validate_args
|
|
|
|
if [[ "$QUIET" != true ]] && [[ "$DRY_RUN" != true ]]; then
|
|
echo -e "${BOLD}${SCRIPT_NAME}${RESET} v${VERSION}"
|
|
echo ""
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo -e "${BOLD}Dry run -- showing generated seed files${RESET}"
|
|
echo ""
|
|
fi
|
|
|
|
setup_workdir
|
|
detect_client_cert
|
|
|
|
echo ""
|
|
|
|
# Generate all seed files
|
|
generate_install_yaml
|
|
generate_applications_yaml
|
|
|
|
case "$APP" in
|
|
incus)
|
|
generate_incus_yaml
|
|
;;
|
|
operations-center)
|
|
generate_ops_center_yaml
|
|
;;
|
|
esac
|
|
|
|
generate_network_yaml
|
|
generate_update_yaml
|
|
|
|
echo ""
|
|
|
|
# Create output
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
info "Dry run complete -- no files were created"
|
|
else
|
|
case "$FORMAT" in
|
|
tar) create_tar_output ;;
|
|
iso) create_iso_output ;;
|
|
fat) create_fat_output ;;
|
|
esac
|
|
|
|
if [[ "$QUIET" != true ]]; then
|
|
print_summary
|
|
fi
|
|
fi
|
|
}
|
|
|
|
main "$@"
|