325 lines
11 KiB
Bash
Executable File
325 lines
11 KiB
Bash
Executable File
#!/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 <short_name> [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)" |