#!/usr/bin/env bash set -euo pipefail # ── Config ─────────────────────────────────────────────────────────────────── REPO_DIR="" # set after prompting for REPO_URL DEPLOY_KEY="$HOME/.ssh/github_deploy_key" DEPLOY_KEY_COMMENT="home-server-deploy" SIGNING_KEY="$HOME/.ssh/github_signing_key" SIGNING_KEY_COMMENT="home-server-signing" # ── Output helpers ──────────────────────────────────────────────────────────── RESET=$'\033[0m' BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' BLUE=$'\033[34m' CYAN=$'\033[36m' trap 'printf "\n ${RED}❌ Script failed at line $LINENO. Check the output above.${RESET}\n"' ERR step() { printf "\n${BOLD}${CYAN}── %s${RESET}\n" "$*"; } ok() { printf " ${GREEN}✅ %s${RESET}\n" "$*"; } warn() { printf " ${YELLOW}⚠️ %s${RESET}\n" "$*"; } info() { printf " ${DIM}%s${RESET}\n" "$*"; } link() { printf " ${BOLD}${BLUE}🔗 %s${RESET}\n" "$*"; } boxed() { local text="$1" local border="" i for (( i=0; i < ${#text} + 4; i++ )); do border+="─"; done printf " ${YELLOW}┌%s┐${RESET}\n" "$border" printf " ${YELLOW}│${RESET} %s ${YELLOW}│${RESET}\n" "$text" printf " ${YELLOW}└%s┘${RESET}\n" "$border" } # ── Prompt helpers ──────────────────────────────────────────────────────────── prompt_yes_no() { local question="$1" while true; do printf "${BOLD}%s${RESET} ${DIM}[yes/n]${RESET}: " "$question" read -r answer case "$answer" in yes) return 0 ;; n|N|no) return 1 ;; *) warn "Incorrect input — please enter 'yes' or 'n'." ;; esac done } prompt_input() { local question="$1" local value="" while [[ -z "$value" ]]; do printf "${BOLD}%s${RESET} " "$question" >&2 read -r value [[ -z "$value" ]] && warn "Cannot be empty." >&2 done printf "%s" "$value" } # Waits for the user to confirm they've completed an out-of-band action. # Returns 1 if they choose to skip. prompt_done() { while true; do printf " ${DIM}Type ${RESET}${BOLD}yes${RESET}${DIM} when done, or ${RESET}${BOLD}n${RESET}${DIM} to skip${RESET}: " read -r answer case "$answer" in yes) return 0 ;; n|N|no) return 1 ;; *) warn "Incorrect input — please enter 'yes' or 'n'." ;; esac done } # ── SSH key setup ───────────────────────────────────────────────────────────── setup_ssh_key() { local key_path="$1" local key_comment="$2" local setup_url="$3" local instructions="$4" local generate=true if [[ -f "$key_path" ]]; then if prompt_yes_no "🔑 $key_path already exists. Replace it?"; then rm -f "$key_path" "${key_path}.pub" else ok "Keeping existing key." generate=false fi fi if $generate; then ssh-keygen -t ed25519 -C "$key_comment" -f "$key_path" -N "" ok "Key generated." local pubkey pubkey=$(cat "${key_path}.pub") printf "\n" boxed "$pubkey" printf "\n" info "$instructions" link "$setup_url" printf "\n" prompt_done || warn "Skipped — remember to add this key before proceeding." fi } # ── Main ───────────────────────────────────────────────────────────────────── printf "${BOLD}${CYAN}" printf "╔══════════════════════════════════════╗\n" printf "║ Home Server Bootstrap Setup ║\n" printf "╚══════════════════════════════════════╝\n" printf "${RESET}\n" info " 1. 🐙 GitHub repository details" info " 2. 🔐 Request sudo privileges" info " 3. 📦 Update packages and install Ansible" info " 4. 🔑 Set up SSH keys (deploy + signing)" info " 5. 📥 Clone the repository" info " 6. ⚙️ Run the Ansible playbook" info " 7. 💾 Restore backup (optional)" info " 8. 🔄 Reboot" printf "\n" prompt_yes_no "Ready to start?" || { warn "Aborted."; exit 0; } # ── Step 1: Repository ─────────────────────────────────────────────────────── step "🐙 Step 1 — GitHub repository" GITHUB_USER=$(prompt_input "GitHub username:") REPO_NAME=$(prompt_input "Repository name:") REPO_URL="git@github.com:${GITHUB_USER}/${REPO_NAME}.git" REPO_PATH="${GITHUB_USER}/${REPO_NAME}" REPO_DIR="$HOME/src/$REPO_NAME" ok "Repository set to ${REPO_URL}." # ── Step 2: Sudo ────────────────────────────────────────────────────────────── step "🔐 Step 2 — Sudo" sudo -v && ok "🔐 Sudo access granted." # ── Step 3: Packages ───────────────────────────────────────────────────────── step "📦 Step 3 — Packages" sudo sh -c 'apt -y update && apt -y full-upgrade && apt -y install unzip ansible' ok "Packages up to date." # ── Step 4: SSH keys ───────────────────────────────────────────────────────── step "🔑 Step 4 — SSH Keys" setup_ssh_key \ "$DEPLOY_KEY" \ "$DEPLOY_KEY_COMMENT" \ "https://github.com/$REPO_PATH/settings/keys" \ "Add the key as a Deploy Key on GitHub. Leave 'Allow write access' unchecked. Remove any old deploy key if reinstalling." setup_ssh_key \ "$SIGNING_KEY" \ "$SIGNING_KEY_COMMENT" \ "https://github.com/settings/ssh/new" \ "Add the key to your GitHub account. Key type: Signing Key." # ── Step 5: Clone ──────────────────────────────────────────────────────────── step "📥 Step 5 — Clone repository" if [[ -d "$REPO_DIR" ]]; then warn "$REPO_DIR already exists, skipping clone." else mkdir -p "$(dirname "$REPO_DIR")" GIT_SSH_COMMAND="ssh -i '$DEPLOY_KEY' -o IdentitiesOnly=yes" \ git clone "$REPO_URL" "$REPO_DIR" ok "Cloned." fi # ── Step 6: Ansible ────────────────────────────────────────────────────────── step "⚙️ Step 6 — Ansible playbook" ansible-galaxy collection install -r "$REPO_DIR/ansible/requirements.yml" ANSIBLE_CONFIG="$REPO_DIR/ansible/ansible.cfg" \ ansible-playbook "$REPO_DIR/ansible/docker_home.yaml" ok "Playbook complete." # ── Step 7: Restore backup ─────────────────────────────────────────────────── step "💾 Step 7 — Restore backup" if prompt_yes_no "Restore latest backup?"; then sudo "$REPO_DIR/scripts/backup.sh" --restore-latest --yes ok "Backup restored." else info "Skipping backup restore." fi # ── Step 8: Reboot ─────────────────────────────────────────────────────────── step "🔄 Step 8 — Reboot" ok "Setup complete!" if prompt_yes_no "Reboot now?"; then sudo reboot else warn "Remember to reboot when ready." fi