Implements the one-shot goal from the greenfield audit: tear down fully and/or deploy cleanly on a pristine WSL instance with nothing but the interactive script. - './deploy.sh teardown': leave-it-down removal across ALL compose profiles (core/test/auth/evpn-test/scale-out -- the old reset missed the last two), networks, named volumes, rendered gobgpd.confs. Data wipe is opt-in (--wipe-data, guarded on native hosts) and preserves backups/ unless --purge-backups; --purge-images reclaims images. Prints the Windows-side cleanup (wsl-portproxy.ps1 -Remove, new switch). - fresh-host preflight: verifies the docker DAEMON (not just the client), offers to install docker-ce from the official repo (--install-prereqs for unattended), and handles the classic pristine-WSL trap: systemd off -> writes /etc/wsl.conf [boot] systemd=true and stops with exact restart instructions instead of dying cryptically. - sudo authenticates ONCE up front (no more password prompt buried mid-provision); --reset now covers all profiles and preserves backups/ - guided flow: banner, Step N/6 headers, context lines explaining HOST_IP-vs-router-facing before the address prompts, router-facing IP validated as a dotted quad (a failed Windows-LAN autodetect can no longer write a <WINDOWS_LAN_IP> placeholder into .env), stage completions print green checkmarks, loud abort note on the plan confirm, post-deploy Verify + teardown hints. - shared helpers moved to scripts/deploy-lib.sh; setup.sh sources it too (duplicate get_env/set_env/ask/confirm definitions deleted), keeping setup.sh as a standalone provisioning primitive. Verified live on this host: teardown left zero obmp containers and removed rendered configs; unattended redeploy brought the stack back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
237 lines
10 KiB
Bash
Executable File
237 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# OpenBMP stack bootstrap — idempotent. Safe to run against an existing
|
|
# deployment: every step is guarded and never overwrites live config.
|
|
#
|
|
# cp .env.example .env # first run only
|
|
# $EDITOR .env # at minimum fill in HOST_IP
|
|
# ./setup.sh # local-auth mode by default
|
|
# docker compose up -d # collector core
|
|
# docker compose --profile test up -d # + lab feeders (ExaBGP, gobgp, ...)
|
|
#
|
|
# For Authelia mode, set OBMP_AUTH_MODE=authelia plus OBMP_DOMAIN and
|
|
# OBMP_COOKIE_DOMAIN in .env before running setup.sh, then bring up with
|
|
# docker compose --profile test --profile auth up -d
|
|
#
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
AUTHELIA_IMAGE="authelia/authelia:4.38"
|
|
|
|
# --- .env -------------------------------------------------------------------
|
|
if [ ! -f .env ]; then
|
|
cp .env.example .env
|
|
echo "Created .env from .env.example."
|
|
echo "Edit it (set HOST_IP at minimum; OBMP_DOMAIN/OBMP_COOKIE_DOMAIN only"
|
|
echo "if OBMP_AUTH_MODE=authelia), then re-run ./setup.sh"
|
|
exit 1
|
|
fi
|
|
|
|
# Shared helpers (log ok warn die hr get_env set_env ...) — same styling as
|
|
# deploy.sh, defined once in scripts/deploy-lib.sh.
|
|
TAG=setup
|
|
. "$(dirname "$0")/scripts/deploy-lib.sh"
|
|
|
|
OBMP_DATA_ROOT="$(get_env OBMP_DATA_ROOT)"
|
|
OBMP_DATA_ROOT="${OBMP_DATA_ROOT:-/var/openbmp}"
|
|
OBMP_DOMAIN="$(get_env OBMP_DOMAIN)"
|
|
OBMP_COOKIE_DOMAIN="$(get_env OBMP_COOKIE_DOMAIN)"
|
|
HOST_IP="$(get_env HOST_IP)"
|
|
OBMP_AUTH_MODE="$(get_env OBMP_AUTH_MODE)"
|
|
if [ -z "$OBMP_AUTH_MODE" ]; then
|
|
# Back-compat: existing deployments predate OBMP_AUTH_MODE. If Authelia was
|
|
# ever initialized (session secret present) or a real OBMP_DOMAIN is set,
|
|
# this is an Authelia host — default to that mode rather than flipping it
|
|
# to local and breaking the next `docker compose up`. Fresh .env from
|
|
# .env.example explicitly sets OBMP_AUTH_MODE=local, so this branch only
|
|
# triggers for pre-existing installs.
|
|
if [ -n "$(get_env AUTHELIA_SESSION_SECRET)" ] || \
|
|
{ [ -n "$OBMP_DOMAIN" ] && [[ "$OBMP_DOMAIN" != changeme* ]]; }; then
|
|
OBMP_AUTH_MODE="authelia"
|
|
echo "OBMP_AUTH_MODE not set; inferred 'authelia' from existing .env"
|
|
else
|
|
OBMP_AUTH_MODE="local"
|
|
echo "OBMP_AUTH_MODE not set; defaulting to 'local'"
|
|
fi
|
|
set_env OBMP_AUTH_MODE "$OBMP_AUTH_MODE"
|
|
fi
|
|
|
|
# --- validate ---------------------------------------------------------------
|
|
# HOST_IP is always required. OBMP_DOMAIN / OBMP_COOKIE_DOMAIN are only
|
|
# required when OBMP_AUTH_MODE=authelia (Authelia needs a real cookie domain
|
|
# and the public URL it lives behind).
|
|
fail=0
|
|
required=(HOST_IP)
|
|
case "$OBMP_AUTH_MODE" in
|
|
local) ;;
|
|
authelia) required+=(OBMP_DOMAIN OBMP_COOKIE_DOMAIN) ;;
|
|
*)
|
|
echo "ERROR: OBMP_AUTH_MODE='$OBMP_AUTH_MODE' (must be 'local' or 'authelia')" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
for var in "${required[@]}"; do
|
|
val="$(get_env "$var")"
|
|
if [ -z "$val" ] || [[ "$val" == changeme* ]]; then
|
|
echo "ERROR: $var is unset or still 'changeme' in .env" >&2
|
|
fail=1
|
|
fi
|
|
done
|
|
[ "$fail" -eq 0 ] || { echo "Fix .env and re-run." >&2; exit 1; }
|
|
echo "Auth mode: $OBMP_AUTH_MODE"
|
|
|
|
# --- privilege helper -------------------------------------------------------
|
|
# $OBMP_DATA_ROOT is often root-owned (e.g. /var/openbmp). Use sudo only if the
|
|
# current user cannot write the parent directory.
|
|
parent="$(dirname "$OBMP_DATA_ROOT")"
|
|
SUDO=""
|
|
if [ ! -w "$parent" ] || { [ -d "$OBMP_DATA_ROOT" ] && [ ! -w "$OBMP_DATA_ROOT" ]; }; then
|
|
SUDO="sudo"
|
|
echo "Note: using sudo for filesystem setup under $OBMP_DATA_ROOT"
|
|
fi
|
|
|
|
# --- data-root directory tree -----------------------------------------------
|
|
echo "Creating data tree under $OBMP_DATA_ROOT ..."
|
|
for d in config grafana grafana/provisioning kafka-data zk-data zk-log \
|
|
postgres/data postgres/ts influxdb authelia; do
|
|
$SUDO mkdir -p "$OBMP_DATA_ROOT/$d"
|
|
done
|
|
# Container processes run as assorted UIDs; lab-permissive perms.
|
|
# EXCEPT postgres/: postgres enforces its own perms and refuses to start if
|
|
# its SSL key is group/world-accessible. A blanket 777 over an EXISTING data
|
|
# tree makes psql_server.key 0777 -> FATAL on every re-deploy (fresh installs
|
|
# are unaffected because the key is generated after this runs). Leave the
|
|
# postgres tree alone; the image entrypoint owns it.
|
|
for _p in "$OBMP_DATA_ROOT"/*; do
|
|
[ "$(basename "$_p")" = "postgres" ] && continue
|
|
$SUDO chmod -R 777 "$_p" 2>/dev/null || true
|
|
done
|
|
# Postgres bind dirs just need to exist and be reachable; the container
|
|
# entrypoint chowns/chmods PGDATA itself on init.
|
|
$SUDO chmod 777 "$OBMP_DATA_ROOT" "$OBMP_DATA_ROOT/postgres" 2>/dev/null || true
|
|
# Repair the re-deploy damage if a previous setup.sh already clobbered an
|
|
# initialized data dir: re-tighten the SSL key/cert so postgres will boot.
|
|
if [ -f "$OBMP_DATA_ROOT/postgres/data/psql_server.key" ]; then
|
|
$SUDO chmod 600 "$OBMP_DATA_ROOT/postgres/data/psql_server.key" \
|
|
"$OBMP_DATA_ROOT/postgres/data/psql_server.crt" 2>/dev/null || true
|
|
fi
|
|
|
|
# --- Grafana provisioning + dashboards --------------------------------------
|
|
# Provisioning YAML points Grafana at /var/lib/grafana/dashboards/... (bind-
|
|
# mounted from $OBMP_DATA_ROOT/grafana/dashboards). Sync BOTH -- otherwise
|
|
# Grafana loads the providers, finds no JSON files, and shows an empty UI.
|
|
# allowUiUpdates: true in the provisioning YAML means UI edits persist to
|
|
# the container's copy; the on-disk sync overwrites them, which is the
|
|
# desired behavior for a repo-authored dashboard set. If you edit a
|
|
# dashboard in Grafana, export it and commit the JSON.
|
|
echo "Syncing Grafana provisioning ..."
|
|
$SUDO cp -r obmp-grafana/provisioning/. "$OBMP_DATA_ROOT/grafana/provisioning/"
|
|
echo "Syncing Grafana dashboards ..."
|
|
$SUDO mkdir -p "$OBMP_DATA_ROOT/grafana/dashboards"
|
|
$SUDO cp -r obmp-grafana/dashboards/. "$OBMP_DATA_ROOT/grafana/dashboards/"
|
|
|
|
# --- Grafana root URL -------------------------------------------------------
|
|
# In local mode Grafana is hit directly on :3000; in authelia mode the public
|
|
# URL is https://OBMP_DOMAIN/grafana/. setup.sh writes the right value into
|
|
# .env so docker-compose just references ${GF_SERVER_ROOT_URL}.
|
|
case "$OBMP_AUTH_MODE" in
|
|
local) desired_root_url="http://${HOST_IP}:3000/grafana/" ;;
|
|
authelia) desired_root_url="https://${OBMP_DOMAIN}/grafana/" ;;
|
|
esac
|
|
current_root_url="$(get_env GF_SERVER_ROOT_URL)"
|
|
if [ "$current_root_url" != "$desired_root_url" ]; then
|
|
set_env GF_SERVER_ROOT_URL "$desired_root_url"
|
|
echo "Set GF_SERVER_ROOT_URL=$desired_root_url"
|
|
fi
|
|
|
|
# --- Authelia setup (authelia mode only) ------------------------------------
|
|
if [ "$OBMP_AUTH_MODE" = "authelia" ]; then
|
|
# Generate secrets only if blank/absent; never overwrite an existing value.
|
|
for key in AUTHELIA_SESSION_SECRET AUTHELIA_JWT_SECRET AUTHELIA_STORAGE_ENCRYPTION_KEY; do
|
|
cur="$(get_env "$key")"
|
|
if [ -z "$cur" ]; then
|
|
set_env "$key" "$(openssl rand -hex 32)"
|
|
echo "Generated $key"
|
|
fi
|
|
done
|
|
AUTHELIA_SESSION_SECRET="$(get_env AUTHELIA_SESSION_SECRET)"
|
|
AUTHELIA_JWT_SECRET="$(get_env AUTHELIA_JWT_SECRET)"
|
|
AUTHELIA_STORAGE_ENCRYPTION_KEY="$(get_env AUTHELIA_STORAGE_ENCRYPTION_KEY)"
|
|
|
|
# Config files: fresh-deploy only — never clobber a live config.
|
|
export AUTHELIA_SESSION_SECRET AUTHELIA_JWT_SECRET AUTHELIA_STORAGE_ENCRYPTION_KEY \
|
|
OBMP_DOMAIN OBMP_COOKIE_DOMAIN
|
|
SUBST='${AUTHELIA_SESSION_SECRET} ${AUTHELIA_JWT_SECRET} ${AUTHELIA_STORAGE_ENCRYPTION_KEY} ${OBMP_DOMAIN} ${OBMP_COOKIE_DOMAIN}'
|
|
|
|
if [ ! -f "$OBMP_DATA_ROOT/authelia/configuration.yml" ]; then
|
|
envsubst "$SUBST" < authelia/configuration.yml.template \
|
|
| $SUDO tee "$OBMP_DATA_ROOT/authelia/configuration.yml" > /dev/null
|
|
echo "Rendered authelia/configuration.yml"
|
|
else
|
|
echo "authelia/configuration.yml exists — left untouched"
|
|
fi
|
|
|
|
if [ ! -f "$OBMP_DATA_ROOT/authelia/users_database.yml" ]; then
|
|
$SUDO cp authelia/users_database.yml.template \
|
|
"$OBMP_DATA_ROOT/authelia/users_database.yml"
|
|
echo "Rendered authelia/users_database.yml (demo user: openbmp)"
|
|
else
|
|
echo "authelia/users_database.yml exists — left untouched"
|
|
fi
|
|
else
|
|
echo "Skipping Authelia setup (OBMP_AUTH_MODE=$OBMP_AUTH_MODE)"
|
|
fi
|
|
|
|
# --- gobgpd.conf rendering (host-side -- gobgp image is distroless) ---------
|
|
# Render gobgp{,-evpn}/gobgpd.conf from gobgpd.conf.tmpl, substituting
|
|
# __HOST_IP__ with $HOST_IP and __BMP_PORT__ with the collector's published
|
|
# port (ROUTER_FACING_PORT, default 1790 -- gobgp is host-networked, so it
|
|
# must target the published port, not the in-container 5000). The rendered
|
|
# .conf is gitignored.
|
|
BMP_PORT="$(get_env ROUTER_FACING_PORT)"; BMP_PORT="${BMP_PORT:-1790}"
|
|
for d in gobgp gobgp-evpn; do
|
|
if [ -f "$d/gobgpd.conf.tmpl" ]; then
|
|
sed -e "s/__HOST_IP__/$HOST_IP/g" -e "s/__BMP_PORT__/$BMP_PORT/g" \
|
|
"$d/gobgpd.conf.tmpl" > "$d/gobgpd.conf"
|
|
echo "Rendered $d/gobgpd.conf (HOST_IP=$HOST_IP, BMP port=$BMP_PORT)"
|
|
fi
|
|
done
|
|
|
|
# --- images -----------------------------------------------------------------
|
|
# Always pull / build the `test` profile (ExaBGP, traffic-gen, gobgp -- the
|
|
# lab feeder bits). Pull `auth` images only when that mode is selected so a
|
|
# local-mode bootstrap on a fresh host doesn't drag in Authelia + portal.
|
|
echo "Pulling and building images ..."
|
|
if [ "$OBMP_AUTH_MODE" = "authelia" ]; then
|
|
docker compose --profile test --profile auth pull --quiet
|
|
docker compose --profile test --profile auth build
|
|
else
|
|
docker compose --profile test pull --quiet
|
|
docker compose --profile test build
|
|
fi
|
|
|
|
# --- done -------------------------------------------------------------------
|
|
cat <<EOF
|
|
|
|
Setup complete.
|
|
|
|
EOF
|
|
if [ "$OBMP_AUTH_MODE" = "authelia" ]; then
|
|
cat <<EOF
|
|
docker compose up -d # BMP collector core
|
|
docker compose --profile test --profile auth up -d # full stack (lab + auth)
|
|
|
|
Grafana: https://${OBMP_DOMAIN}/grafana/ (behind Authelia)
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
docker compose up -d # BMP collector core
|
|
docker compose --profile test up -d # collector + lab feeders (ExaBGP, gobgp, traffic-gen)
|
|
|
|
Grafana: http://${HOST_IP}:3000/grafana/ (login: admin / openbmp)
|
|
|
|
To switch on Authelia later: set OBMP_AUTH_MODE=authelia + OBMP_DOMAIN +
|
|
OBMP_COOKIE_DOMAIN in .env, re-run ./setup.sh, then add --profile auth.
|
|
EOF
|
|
fi
|