#!/usr/bin/env bash set -euo pipefail # --- Dependencies --- # Check if docopts is installed (for Bash) if ! command -v docopts &> /dev/null; then echo "โŒ Error: 'docopts' is required for Bash." >&2 echo "See https://github.com/docopt/docopts to install it." >&2 exit 1 fi # --- Usage and Documentation --- usage="Create, bootstrap and deploy a NixOS LXC container on a remote Proxmox VE 9 server. Usage: $0 [options] $0 -h|--help Options: -h, --help Show this message. -t, --template TEMPLATE LXC template (e.g. local:vztmpl/nixos-unstable). -r, --rootfs-size SIZE Root filesystem size (e.g. 8G). -c, --cores CORES Number of CPU cores. -m, --memory MEMORY RAM in MiB. -s, --swap SWAP Swap in MiB. -p, --password PASSWORD Root password for the container. -b, --bridge BRIDGE Network bridge (e.g. vmbr0). -v, --vlan VLAN VLAN tag (e.g. tag=10). -d, --domain DOMAIN DNS domain. -u, --unprivileged UNPRIV Unprivileged container (0 or 1). -i, --ip IP Static IPv4 address (e.g. 192.168.1.100/24). --ip6 TOKEN IPv6 token for SLAAC (e.g. ::1:2:3:4). -C, --cmode CMODE Console mode (console or tty). Default: console. -T, --tags TAGS Tags for the container (optional). -k, --ssh-public-keys KEYS SSH public keys for the container. --pve-host HOST Proxmox host (e.g. pve). --pve-user USER Proxmox user (default: admin). --pve-port PORT SSH port for Proxmox (default: 22). --pve-ssh-key KEY SSH key file for authentication. --initial-config FILE Initial NixOS configuration file to push [default: ./initial-lxc-configuration.nix]. --repo-url URL Git repository URL for deploy.sh [default: (none, must be provided)]. --branch BRANCH Git branch for deploy.sh [default: main]. --environment ENV Environment name (production, dev, etc.) [default: (none, must be provided or set via config)]. --skip-deploy Skip the post-creation bootstrap (push + nixos-rebuild). --dry-run Simulate container creation without execution. Optional configuration files (loaded in order, later overrides earlier): /etc/nixos-infra/hosts/config \${XDG_CONFIG_HOME}/nixos-infra/hosts/config ./config " # --- Default Parameters (Environment Variables) --- # Proxmox Server PVE_HOST="${PVE_HOST:-}" PVE_USER="${PVE_USER:-admin}" PVE_PORT="${PVE_PORT:-22}" PVE_SSH_KEY="${PVE_SSH_KEY:-}" DRY_RUN="${DRY_RUN:-false}" # LXC Container TEMPLATE="${TEMPLATE:-local:vztmpl/nixos-unstable-amd64-default_20260428}" ROOTFS_SIZE="${ROOTFS_SIZE:-8G}" CORES="${CORES:-2}" MEMORY="${MEMORY:-2048}" SWAP="${SWAP:-1024}" PASSWORD="${PASSWORD:-changeme}" BRIDGE="${BRIDGE:-vmbr0}" VLAN="${VLAN:-}" DOMAIN="${DOMAIN:-}" UNPRIVILEGED="${UNPRIVILEGED:-0}" IP="${IP:-}" IP6="${IP6:-}" CMODE="${CMODE:-console}" TAGS="${TAGS:-}" SSH_PUBLIC_KEYS="${SSH_PUBLIC_KEYS:-}" # Bootstrap INITIAL_CONFIG="${INITIAL_CONFIG:-./initial-lxc-configuration.nix}" REPO_URL="${REPO_URL:-}" BRANCH="${BRANCH:-main}" SKIP_DEPLOY="${SKIP_DEPLOY:-false}" ENVIRONMENT="${ENVIRONMENT:-}" # --- Parse Arguments with docopts (Highest priority) --- # set +e is to prevent set -e from eating the error message from docopts. # This code is up here to prevent useless error messages to be printed # in case the "-h" or "--help" argument is used. set +e args=$(docopts -h "$usage" : "$@") eval "$args" set -e # --- Apply Configuration Files (by increasing priority) --- XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" CONFIG_FILES=(\ "/etc/nixos-infra/hosts/config" \ "$XDG_CONFIG_HOME/nixos-infra/hosts/config" \ "./config") for conffile in ${CONFIG_FILES[*]}; do if [ -f "$conffile" ]; then echo "๐Ÿ“„ Applying parameters from $conffile..." set -a source "$conffile" set +a else echo "โ„น๏ธ $conffile not found (optional)." fi done # Override with CLI arguments (have priority over config files) PVE_HOST="${pve_host:-$PVE_HOST}" PVE_USER="${pve_user:-$PVE_USER}" PVE_PORT="${pve_port:-$PVE_PORT}" PVE_SSH_KEY="${pve_ssh_key:-$PVE_SSH_KEY}" DRY_RUN="${dry_run:-$DRY_RUN}" TEMPLATE="${template:-$TEMPLATE}" ROOTFS_SIZE="${rootfs_size:-$ROOTFS_SIZE}" CORES="${cores:-$CORES}" MEMORY="${memory:-$MEMORY}" SWAP="${swap:-$SWAP}" PASSWORD="${password:-$PASSWORD}" BRIDGE="${bridge:-$BRIDGE}" VLAN="${vlan:-$VLAN}" DOMAIN="${domain:-$DOMAIN}" UNPRIVILEGED="${unprivileged:-$UNPRIVILEGED}" IP="${ip:-$IP}" IP6="${ip6:-$IP6}" CMODE="${cmode:-$CMODE}" TAGS="${tags:-$TAGS}" SSH_PUBLIC_KEYS="${ssh_public_keys:-$SSH_PUBLIC_KEYS}" INITIAL_CONFIG="${initial_config:-$INITIAL_CONFIG}" REPO_URL="${repo_url:-$REPO_URL}" BRANCH="${branch:-$BRANCH}" SKIP_DEPLOY="${skip_deploy:-$SKIP_DEPLOY}" ENVIRONMENT="${environment:-$ENVIRONMENT}" # --- SSH Key Default Logic --- if [ "$PVE_SSH_KEY" = "default" ]; then PVE_SSH_KEY="${HOME}/.ssh/id_${PVE_USER}" fi # --- Critical Parameters Validation --- mandatory_params=( "TEMPLATE" "ROOTFS_SIZE" "CORES" "MEMORY" "SWAP" "PASSWORD" "BRIDGE" "DOMAIN" "UNPRIVILEGED" "CMODE" "SSH_PUBLIC_KEYS" "PVE_HOST" "PVE_USER" "PVE_PORT" ) missing_params=() for param in ${mandatory_params[*]}; do if [ -z "${!param}" ]; then missing_params+=("$param"); fi done if [ ${#missing_params[@]} -gt 0 ]; then echo "โŒ Error: The following necessary parameters are missing: ${missing_params[*]}" >&2 echo "โŒ Error: Please provide them through a config file or the command line." >&2 exit 1 fi # Authentication Validation if [ ! -f "$PVE_SSH_KEY" ]; then echo "โŒ Error: SSH key file '$PVE_SSH_KEY' does not exist." >&2 exit 1 fi # Validate initial-config.nix exists (unless --skip-deploy) if [ "$SKIP_DEPLOY" != "true" ] && [ ! -f "$INITIAL_CONFIG" ]; then echo "โŒ Error: Initial NixOS configuration '$INITIAL_CONFIG' not found." >&2 echo " Provide a valid path with --initial-config or use --skip-deploy." >&2 exit 1 fi # Validate deploy.sh exists (unless --skip-deploy) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_SCRIPT="${SCRIPT_DIR}/deploy.sh" if [ "$SKIP_DEPLOY" != "true" ] && [ ! -f "$DEPLOY_SCRIPT" ]; then echo "โŒ Error: deploy.sh not found at '$DEPLOY_SCRIPT'." >&2 echo " The bootstrap phase requires deploy.sh to be present." >&2 exit 1 fi # --- SSH Connection to Proxmox Server --- run_proxmox() { local ssh_cmd="ssh -p $PVE_PORT" if [ -n "$PVE_SSH_KEY" ] && [ -f "$PVE_SSH_KEY" ]; then ssh_cmd="$ssh_cmd -i $PVE_SSH_KEY " else ssh_cmd="$ssh_cmd -o PreferredAuthentications=password " fi $ssh_cmd "$PVE_USER@$PVE_HOST" "$1" } # --- Network Options Construction --- NET_OPTS="name=eth0,bridge=$BRIDGE" if [ -n "$VLAN" ]; then NET_OPTS="$NET_OPTS,$VLAN" fi if [ -n "$IP" ]; then NET_OPTS="$NET_OPTS,ip=$IP" fi # IPv6: use SLAAC with a token if provided, otherwise DHCP if [ -n "$IP6" ]; then NET_OPTS="$NET_OPTS,ip6=auto,token6=${IP6}" else NET_OPTS="$NET_OPTS,ip6=dhcp" fi # --- Container Creation --- echo "๐Ÿš€ Creating LXC container $short_name on $PVE_HOST (domain: $DOMAIN)..." CREATE_CMD="pct create $ROOTFS_SIZE $TEMPLATE --cores $CORES \ --memory $MEMORY --swap $SWAP --hostname $short_name.$DOMAIN \ --password $PASSWORD --unprivileged $UNPRIVILEGED --net0 $NET_OPTS \ --onboot 0 --cmode $CMODE --ssh-public-keys $SSH_PUBLIC_KEYS" if [ -n "$TAGS" ]; then CREATE_CMD="$CREATE_CMD --tags $TAGS" fi # Display the command (with password masked) DISPLAY_CMD=$(echo "$CREATE_CMD" | sed "s/--password [^ ]*/--password \*\*\*\*\*/g") echo "๐Ÿ”ง Command to execute on $PVE_HOST: $DISPLAY_CMD" # Execute or simulate if [ "$DRY_RUN" = "true" ]; then echo "๐Ÿงช Dry run mode:" echo " - Container creation skipped" echo " - Bootstrap phase skipped" exit 0 fi LXC_ID=$(run_proxmox "$CREATE_CMD" | grep -oP '\d+') if [ -z "$LXC_ID" ]; then echo "โŒ Error: Failed to create the container." >&2 exit 1 fi echo "โœ… LXC container $short_name created successfully (ID: $LXC_ID)." # --- Bootstrap Phase (unless --skip-deploy) --- if [ "$SKIP_DEPLOY" = "true" ]; then echo "โญ๏ธ --skip-deploy set. Container created but not bootstrapped." echo " Start it manually with: pct start $LXC_ID" echo " Then apply a configuration manually." exit 0 fi echo "" echo "๐Ÿš€ Starting bootstrap phase for CT $LXC_ID..." # 1. Start the container echo "โ–ถ๏ธ Starting container $LXC_ID..." run_proxmox "pct start $LXC_ID" || { echo "โŒ Error: Failed to start container $LXC_ID." >&2 exit 1 } # 2. Wait for the container to be ready (SSH or pct exec available) echo "โณ Waiting for container to be ready..." for i in $(seq 1 30); do if run_proxmox "pct exec $LXC_ID -- true" 2>/dev/null; then echo "โœ… Container $LXC_ID is ready." break fi if [ "$i" -eq 30 ]; then echo "โŒ Error: Container $LXC_ID did not become ready in time." >&2 echo " You can retry bootstrap manually." >&2 exit 1 fi sleep 2 done # 3. Push initial-lxc-configuration.nix echo "๐Ÿ“„ Pushing initial NixOS configuration..." run_proxmox "pct push $LXC_ID '$INITIAL_CONFIG' /etc/nixos/configuration.nix" || { echo "โŒ Error: Failed to push initial configuration." >&2 exit 1 } # 4. Push deploy.sh echo "๐Ÿ“„ Pushing deploy script..." run_proxmox "pct push $LXC_ID '$DEPLOY_SCRIPT' /usr/local/bin/deploy-nixos" || { echo "โŒ Error: Failed to push deploy script." >&2 exit 1 } run_proxmox "pct exec $LXC_ID -- chmod +x /usr/local/bin/deploy-nixos" || { echo "โŒ Error: Failed to make deploy script executable." >&2 exit 1 } # 5. Apply initial configuration (nixos-rebuild switch) echo "โš™๏ธ Applying initial NixOS configuration (this may take a while)..." if ! run_proxmox "pct exec $LXC_ID -- nixos-rebuild switch" 2>&1; then echo "โš ๏ธ Warning: Initial nixos-rebuild may have issues." >&2 echo " Check the container manually: pct exec $LXC_ID -- nixos-rebuild switch" >&2 # Continue anyway โ€” deploy.sh might still work fi echo "โœ… Initial configuration applied." # 6. Run deploy.sh to clone the repo and apply the specific configuration echo "๐ŸŒ Running deploy.sh to clone repo and apply specific configuration..." # Pass REPO_URL, BRANCH and ENVIRONMENT as env vars to deploy.sh inside the container DEPLOY_CMD="REPO_URL='$REPO_URL' BRANCH='$BRANCH' ENVIRONMENT='$ENVIRONMENT' /usr/local/bin/deploy-nixos" if ! run_proxmox "pct exec $LXC_ID -- bash -c '$DEPLOY_CMD'" 2>&1; then echo "โš ๏ธ Warning: deploy.sh encountered issues." >&2 echo " You can retry manually: pct exec $LXC_ID -- /usr/local/bin/deploy-nixos" >&2 exit 1 fi echo "" echo "๐ŸŽ‰ Container $short_name (CT $LXC_ID) successfully created and deployed!" echo " Connect with: ssh root@${IP%%/*} (or use pct exec $LXC_ID)"