chore: Cleaned up ansible and added control script
This commit is contained in:
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bash
|
||||
# control.sh - interactive control panel for this repo.
|
||||
#
|
||||
# Steps you through the pre-setup and day-to-day operations for both the
|
||||
# OpenTofu infra (infra/) and the Ansible config (ansible/), in the order the
|
||||
# firewall rules require (vpn first, then tunnel in, then dev/prod). It only
|
||||
# ever runs `tofu`/`ansible-playbook` for you - it's a menu wrapper, nothing
|
||||
# more, so anything it does you can also do by hand from infra/ or ansible/.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
INFRA_DIR="$REPO_ROOT/infra"
|
||||
ANSIBLE_DIR="$REPO_ROOT/ansible"
|
||||
|
||||
# --- pretty output --------------------------------------------------------
|
||||
if [ -t 1 ]; then
|
||||
BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
|
||||
RED=$(tput setaf 1); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); CYAN=$(tput setaf 6)
|
||||
else
|
||||
BOLD=""; DIM=""; RESET=""; RED=""; GREEN=""; YELLOW=""; CYAN=""
|
||||
fi
|
||||
|
||||
ok() { printf "%s ok %s %s\n" "$GREEN" "$RESET" "$1"; }
|
||||
warn() { printf "%s warn%s %s\n" "$YELLOW" "$RESET" "$1"; }
|
||||
bad() { printf "%s miss%s %s\n" "$RED" "$RESET" "$1"; }
|
||||
title() { printf "\n%s%s%s\n" "$BOLD$CYAN" "$1" "$RESET"; }
|
||||
|
||||
# Pick the OpenTofu binary: prefer `tofu`, fall back to `terraform`.
|
||||
TOFU=""
|
||||
detect_tofu() {
|
||||
if command -v tofu >/dev/null 2>&1; then TOFU="tofu"
|
||||
elif command -v terraform >/dev/null 2>&1; then TOFU="terraform"
|
||||
fi
|
||||
}
|
||||
detect_tofu
|
||||
|
||||
# Run a command inside a directory, echoing it first. Returns the command's exit code.
|
||||
run_in() {
|
||||
local dir="$1"; shift
|
||||
printf "%s\$ (cd %s && %s)%s\n" "$DIM" "${dir#$REPO_ROOT/}" "$*" "$RESET"
|
||||
( cd "$dir" && "$@" )
|
||||
}
|
||||
|
||||
pause() { read -rp $'\nPress Enter to return to the menu... ' _; }
|
||||
|
||||
confirm() {
|
||||
local reply
|
||||
read -rp "$1 [y/N] " reply
|
||||
[[ "$reply" == "y" || "$reply" == "Y" ]]
|
||||
}
|
||||
|
||||
# --- prerequisite checks --------------------------------------------------
|
||||
check_prereqs() {
|
||||
title "Tools"
|
||||
if [ -n "$TOFU" ]; then ok "OpenTofu ($TOFU: $("$TOFU" version 2>/dev/null | head -n1))"
|
||||
else bad "Neither 'tofu' nor 'terraform' found on PATH"; fi
|
||||
if command -v ansible-playbook >/dev/null 2>&1; then ok "Ansible ($(ansible --version 2>/dev/null | head -n1))"
|
||||
else bad "'ansible-playbook' not found on PATH"; fi
|
||||
|
||||
title "Environment variables"
|
||||
[ -n "${HCLOUD_TOKEN:-}" ] && ok "HCLOUD_TOKEN set" || bad "HCLOUD_TOKEN not set (needed by both tofu and the ansible inventory)"
|
||||
[ -n "${BACKUP_S3_BUCKET:-}" ] && ok "BACKUP_S3_BUCKET set" || warn "BACKUP_S3_BUCKET not set (needed for deploy/spinup - S3 backup .env)"
|
||||
[ -n "${BACKUP_S3_ACCESS_KEY_ID:-}" ] && ok "BACKUP_S3_ACCESS_KEY_ID set" || warn "BACKUP_S3_ACCESS_KEY_ID not set"
|
||||
[ -n "${BACKUP_S3_SECRET_ACCESS_KEY:-}" ] && ok "BACKUP_S3_SECRET_ACCESS_KEY set" || warn "BACKUP_S3_SECRET_ACCESS_KEY not set"
|
||||
[ -n "${BACKUP_S3_ENDPOINT:-}" ] && ok "BACKUP_S3_ENDPOINT set" || warn "BACKUP_S3_ENDPOINT not set (omit only for real AWS S3)"
|
||||
|
||||
local key="${ANSIBLE_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519}"
|
||||
[ -f "${key/#\~/$HOME}" ] && ok "SSH key present ($key)" || warn "SSH key not found at $key (set ANSIBLE_SSH_PRIVATE_KEY_FILE)"
|
||||
|
||||
title "Files"
|
||||
[ -f "$INFRA_DIR/terraform.tfvars" ] && ok "infra/terraform.tfvars exists" \
|
||||
|| bad "infra/terraform.tfvars missing (copy terraform.tfvars.example and fill in ssh_key_names)"
|
||||
[ -d "$INFRA_DIR/.terraform" ] && ok "infra/.terraform exists (tofu init has run)" \
|
||||
|| warn "infra/.terraform missing - run OpenTofu > init"
|
||||
if ansible-galaxy collection list hetzner.hcloud >/dev/null 2>&1; then ok "hetzner.hcloud collection installed"
|
||||
else warn "hetzner.hcloud collection not installed - run Ansible > Install collections"; fi
|
||||
}
|
||||
|
||||
# --- OpenTofu actions -----------------------------------------------------
|
||||
require_tofu() {
|
||||
if [ -z "$TOFU" ]; then bad "No tofu/terraform binary found."; return 1; fi
|
||||
if [ ! -f "$INFRA_DIR/terraform.tfvars" ]; then
|
||||
bad "infra/terraform.tfvars is missing - copy terraform.tfvars.example first."; return 1
|
||||
fi
|
||||
}
|
||||
tofu_init() { require_tofu && run_in "$INFRA_DIR" "$TOFU" init; }
|
||||
tofu_plan() { require_tofu && run_in "$INFRA_DIR" "$TOFU" plan; }
|
||||
tofu_apply() { require_tofu && run_in "$INFRA_DIR" "$TOFU" apply; }
|
||||
tofu_output() { require_tofu && run_in "$INFRA_DIR" "$TOFU" output; }
|
||||
tofu_destroy() {
|
||||
require_tofu || return 1
|
||||
confirm "${RED}Destroy ALL infra managed by OpenTofu?${RESET}" && run_in "$INFRA_DIR" "$TOFU" destroy
|
||||
}
|
||||
|
||||
# --- Ansible actions ------------------------------------------------------
|
||||
ansible_collections() { run_in "$ANSIBLE_DIR" ansible-galaxy collection install -r requirements.yml; }
|
||||
ansible_inventory() { run_in "$ANSIBLE_DIR" ansible-inventory --graph; }
|
||||
|
||||
# $1 = playbook, $2 = -l limit expression
|
||||
play() { run_in "$ANSIBLE_DIR" ansible-playbook "playbooks/$1" -l "$2"; }
|
||||
|
||||
ansible_bootstrap_vpn() { play bootstrap.yml role_vpn; }
|
||||
ansible_deploy_vpn() { play deploy.yml role_vpn && play spinup.yml role_vpn; }
|
||||
ansible_bootstrap_dp() { play bootstrap.yml role_dev,role_prod; }
|
||||
ansible_deploy_dp() { play deploy.yml role_dev,role_prod && play spinup.yml role_dev,role_prod; }
|
||||
|
||||
ansible_spindown() {
|
||||
local target
|
||||
read -rp "Spindown which hosts? (e.g. role_vpn, role_dev,role_prod, all): " target
|
||||
[ -n "$target" ] && play spindown.yml "$target"
|
||||
}
|
||||
|
||||
# --- guided walkthrough ---------------------------------------------------
|
||||
guided() {
|
||||
title "Guided full setup"
|
||||
cat <<EOF
|
||||
This walks the whole bring-up in the order the firewalls require. dev/prod only
|
||||
accept SSH from the vpn server's public IP, so vpn must exist and you must be
|
||||
connected to it (OpenVPN) before dev/prod are reachable.
|
||||
|
||||
Steps, in order:
|
||||
1. OpenTofu: init -> plan -> apply (creates servers, network, firewalls, DNS)
|
||||
2. Ansible: bootstrap + deploy + spinup for role_vpn
|
||||
3. YOU: connect to the VPN with an OpenVPN client
|
||||
4. Ansible: bootstrap + deploy + spinup for role_dev,role_prod
|
||||
EOF
|
||||
confirm "Start the guided walkthrough?" || return 0
|
||||
|
||||
echo; title "1/4 OpenTofu"
|
||||
if confirm "Run $TOFU init/plan/apply now?"; then
|
||||
tofu_init && tofu_plan && tofu_apply || { bad "OpenTofu step failed - stopping."; return 1; }
|
||||
fi
|
||||
|
||||
echo; title "2/4 Ansible - vpn"
|
||||
if confirm "Bootstrap + deploy + spinup role_vpn now?"; then
|
||||
ansible_bootstrap_vpn && ansible_deploy_vpn || { bad "vpn step failed - stopping."; return 1; }
|
||||
fi
|
||||
|
||||
echo; title "3/4 Connect the VPN"
|
||||
warn "Connect to the vpn server with your OpenVPN client now, then continue."
|
||||
confirm "Are you connected to the VPN?" || { warn "Stopping - reconnect and re-run this step."; return 0; }
|
||||
|
||||
echo; title "4/4 Ansible - dev + prod"
|
||||
if confirm "Bootstrap + deploy + spinup role_dev,role_prod now?"; then
|
||||
ansible_bootstrap_dp && ansible_deploy_dp || { bad "dev/prod step failed."; return 1; }
|
||||
fi
|
||||
ok "Guided setup complete."
|
||||
}
|
||||
|
||||
# --- menu -----------------------------------------------------------------
|
||||
menu() {
|
||||
clear
|
||||
printf "%s==== server control panel ====%s\n" "$BOLD$CYAN" "$RESET"
|
||||
printf "%srepo: %s tofu: %s%s\n" "$DIM" "$REPO_ROOT" "${TOFU:-none}" "$RESET"
|
||||
|
||||
title "Setup"
|
||||
echo " 1) Check prerequisites"
|
||||
echo " 2) Guided full setup (vpn -> connect -> dev/prod)"
|
||||
|
||||
title "OpenTofu (infra/)"
|
||||
echo " 3) init 4) plan 5) apply"
|
||||
echo " 6) output 7) destroy"
|
||||
|
||||
title "Ansible (ansible/)"
|
||||
echo " 8) Install collections 9) Show inventory"
|
||||
echo " 10) Bootstrap vpn 11) Deploy + spinup vpn"
|
||||
echo " 12) Bootstrap dev+prod 13) Deploy + spinup dev+prod"
|
||||
echo " 14) Spindown (choose target)"
|
||||
|
||||
echo
|
||||
echo " 0) Quit"
|
||||
echo
|
||||
read -rp "Select an option: " choice
|
||||
}
|
||||
|
||||
while true; do
|
||||
menu
|
||||
case "$choice" in
|
||||
1) check_prereqs; pause ;;
|
||||
2) guided; pause ;;
|
||||
3) tofu_init; pause ;;
|
||||
4) tofu_plan; pause ;;
|
||||
5) tofu_apply; pause ;;
|
||||
6) tofu_output; pause ;;
|
||||
7) tofu_destroy; pause ;;
|
||||
8) ansible_collections; pause ;;
|
||||
9) ansible_inventory; pause ;;
|
||||
10) ansible_bootstrap_vpn; pause ;;
|
||||
11) ansible_deploy_vpn; pause ;;
|
||||
12) ansible_bootstrap_dp; pause ;;
|
||||
13) ansible_deploy_dp; pause ;;
|
||||
14) ansible_spindown; pause ;;
|
||||
0) echo "Bye."; exit 0 ;;
|
||||
"") ;;
|
||||
*) warn "Unknown option: $choice"; pause ;;
|
||||
esac
|
||||
done
|
||||
Reference in New Issue
Block a user