Compare commits

..

3 Commits

Author SHA1 Message Date
Sam
5b5593b6cc deploy.sh: teardown subcommand, fresh-host preflight, guided interactive flow
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>
2026-07-20 17:12:49 -07:00
Sam
da975d7375 docs: audit fixes -- retire stale claims, add navigation, correct init_db story
From a two-agent audit (docs accuracy + greenfield deploy path):

- init_db docs were describing upstream behavior: this repo's psql-app
  auto-creates the schema on first run and drops config/do_not_init_db to
  skip later (psql-app/scripts/run:74). README/DOCS/backup-restore now
  describe the marker semantics; the restore flow creates the marker
  BEFORE first start instead of 'not creating init_db'
- DOCS.md contained heavy retired-lab drift: banner declares it a legacy
  walkthrough with illustrative values; fixed its self-contradictions --
  external port 1790, folder OBMP-Reference, datasource 'PostgreSQL',
  OpenConfig gNMI paths (matching telegraf.conf), EXABGP_PEERS,
  TRAFFIC_GEN_PORT
- deploy.sh --help now shows the current scope names (old ones remain
  accepted aliases)
- navigation: new docs/README.md index with operator and network-engineer
  tracks (links the previously orphaned backup-restore, security-hardening,
  ROADMAP, DB_SCHEMA); README gains a Start-here router
- intra-docs prose paths no longer carry the docs/ prefix (they resolve
  from within docs/); RR-CLIENTS -> RR-CLIENT matches the blueprint;
  ROADMAP A6 marked done
- scripts/deploy-lib.sh added: shared log/ask/confirm/get_env/set_env
  helpers for the deploy tooling consolidation (wiring lands next)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:05:47 -07:00
Sam
65fd88a0a2 blueprints: fix cross-references, flag next-hop-self caveat, sync XRd timers
Review findings from verifying the router-blueprints drop against the
deployed stack:

- router-blueprints/README.md linked docs/router-config-guide.md, which
  does not exist -- the guide is docs/router-integration.md
- router-integration.md still advised --router-port 5000 for lab routers;
  1790 is the standard now, the note points at re-applying via
  cml/proxmox_bmp_config.py instead
- verify sections referenced Grafana paths that don't exist in the
  provisioned UI: 'Base-1001 -> Peers Table' -> Inventory / Peer Detail,
  and folder 'OBMP-Learning' -> OBMP-Reference
- role-route-reflector.cfg: loud caveat block on next-hop-self -- it only
  rewrites eBGP-learned/local routes (what this lab wants); reflected
  iBGP routes need 'ibgp policy out enforce-modifications', so on a pure
  RR the line is a silent no-op
- cml/xrd-node-definition.yaml BMP timers synced to the blueprint values
  (initial-delay 60, refresh 60/30, drop flapping-delay) per the
  keep-hand-config-and-automation-in-sync rule

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:49:40 -07:00
17 changed files with 449 additions and 114 deletions

View File

@ -1,6 +1,14 @@
# OpenBMP docker files # OpenBMP docker files
Docker files for OpenBMP. Docker files for OpenBMP.
> **Start here:**
> - **Deploying the stack?** Jump to [Greenfield deploy](#greenfield-deploy-recommended-deploysh)
> below, then see the [docs index](docs/README.md) for deployment types,
> portability findings, sizing, and backup.
> - **Configuring routers to feed it?** Go straight to
> [docs/router-integration.md](docs/router-integration.md) and the
> copy-paste fragments in [router-blueprints/](router-blueprints/).
## (Prerequisite) Platform Docker Install ## (Prerequisite) Platform Docker Install
> Ignore this step if you already have a current docker install > Ignore this step if you already have a current docker install
@ -104,8 +112,10 @@ sudo chmod -R 7777 $OBMP_DATA_ROOT
> refuse to start. Skip `postgres/` (setup.sh does this for you — see > refuse to start. Skip `postgres/` (setup.sh does this for you — see
> [docs/PORTABILITY-FINDINGS.md](docs/PORTABILITY-FINDINGS.md) finding 10). > [docs/PORTABILITY-FINDINGS.md](docs/PORTABILITY-FINDINGS.md) finding 10).
> In order to init the DB tables, you must create the file ```${OBMP_DATA_ROOT}/config/init_db```. This should > DB tables are created **automatically** by psql-app on its first run (it
> only be done once or whenever you want to completely wipe out the DB and start over. > drops a `config/do_not_init_db` marker afterward so restarts skip the
> migration). No `init_db` trigger file is needed — that was upstream
> behavior this repo's psql-app replaces.
Change ```OBMP_DATA_ROOT=<path>``` to where you created the directories above. The default is ```/var/openbmp``` Change ```OBMP_DATA_ROOT=<path>``` to where you created the directories above. The default is ```/var/openbmp```

View File

@ -130,12 +130,11 @@ configuration:
! !
bmp server 1 bmp server 1
host 10.40.40.202 port 1790 host 10.40.40.202 port 1790
description OpenBMP description OpenBMP-Collector
update-source Gi0/0/0/0 update-source Gi0/0/0/0
flapping-delay 60 initial-delay 60
initial-delay 5
stats-reporting-period 300 stats-reporting-period 300
initial-refresh delay 30 spread 2 initial-refresh delay 60 spread 30
! !
ssh server v2 ssh server v2
end end

270
deploy.sh
View File

@ -17,14 +17,22 @@
# ./deploy.sh # interactive; auto-detect + prompts # ./deploy.sh # interactive; auto-detect + prompts
# ./deploy.sh --wsl | --prod # force host type (still prompts rest) # ./deploy.sh --wsl | --prod # force host type (still prompts rest)
# ./deploy.sh --host-ip 10.0.0.50 # preset HOST_IP (internal stack IP) # ./deploy.sh --host-ip 10.0.0.50 # preset HOST_IP (internal stack IP)
# ./deploy.sh --router-ip 10.0.0.9 --router-port 5000 # BMP-facing target # ./deploy.sh --router-ip 10.0.0.9 --router-port 1790 # BMP-facing target
# ./deploy.sh --auth local|authelia # ./deploy.sh --auth local|authelia
# ./deploy.sh --scope core|feeders|full|standalone # ./deploy.sh --scope full-stack|remote|central-store
# ./deploy.sh --central-kafka 10.0.0.50:9092 # for --scope standalone # ./deploy.sh --central-kafka 10.0.0.50:9092 # for --scope remote
# ./deploy.sh --reset # wipe data tree first (guarded on prod) # ./deploy.sh --reset # wipe data tree first (guarded on prod)
# ./deploy.sh --install-prereqs # allow installing docker-ce unattended
# ./deploy.sh --prod --yes # unattended (needs enough presets) # ./deploy.sh --prod --yes # unattended (needs enough presets)
# REPO_DIR=/opt/obmp-docker ./deploy.sh # REPO_DIR=/opt/obmp-docker ./deploy.sh
# #
# ./deploy.sh teardown # stop + remove EVERYTHING, leave it down
# --wipe-data # also wipe $OBMP_DATA_ROOT (guarded;
# # backups/ preserved by default)
# --purge-images # also remove stack images
# --purge-backups # do not preserve backups/ on wipe
# --yes # unattended (requires explicit flags)
#
# SCOPE / DEPLOYMENT LEVELS (what each includes, excludes, ballpark sysreqs): # SCOPE / DEPLOYMENT LEVELS (what each includes, excludes, ballpark sysreqs):
# #
# core Collector core: zookeeper, kafka, psql, collector, psql-app, # core Collector core: zookeeper, kafka, psql, collector, psql-app,
@ -103,7 +111,7 @@
# Mitigations: NVMe with real IOPS; BMP-monitor pre-policy on the RRs only # Mitigations: NVMe with real IOPS; BMP-monitor pre-policy on the RRs only
# (not every client session) to avoid ingesting N reflected copies of the # (not every client session) to avoid ingesting N reflected copies of the
# same table; stagger connects (initial-refresh delay/spread); raise # same table; stagger connects (initial-refresh delay/spread); raise
# KAFKA/PSQL_APP mem; or split ingest with --scope standalone collectors. # KAFKA/PSQL_APP mem; or split ingest with --scope remote collectors.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# #
# HOST TYPE affects default HOST_IP source, default auth/scope, reset guarding, # HOST TYPE affects default HOST_IP source, default auth/scope, reset guarding,
@ -119,9 +127,19 @@ ARG_ROUTER_IP=""
ARG_ROUTER_PORT="" ARG_ROUTER_PORT=""
ARG_AUTH="" # local | authelia ARG_AUTH="" # local | authelia
ARG_SCOPE="" # full-stack | remote | central-store (aliases accepted) ARG_SCOPE="" # full-stack | remote | central-store (aliases accepted)
ARG_CENTRAL_KAFKA="" # host:port for --scope standalone ARG_CENTRAL_KAFKA="" # host:port for --scope remote
ARG_RESET=0 ARG_RESET=0
ARG_YES=0 ARG_YES=0
ARG_INSTALL_PREREQS=0
COMMAND="deploy" # deploy (default) | teardown
ARG_WIPE_DATA=0 # teardown: also wipe $OBMP_DATA_ROOT
ARG_PURGE_IMAGES=0 # teardown: also remove stack images
ARG_PURGE_BACKUPS=0 # teardown: do NOT preserve $OBMP_DATA_ROOT/backups
# First positional arg may be a subcommand.
case "${1:-}" in
teardown) COMMAND="teardown"; shift ;;
esac
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
@ -140,6 +158,10 @@ while [ $# -gt 0 ]; do
--central-kafka) ARG_CENTRAL_KAFKA="${2:?--central-kafka needs host:port}"; shift ;; --central-kafka) ARG_CENTRAL_KAFKA="${2:?--central-kafka needs host:port}"; shift ;;
--central-kafka=*) ARG_CENTRAL_KAFKA="${1#*=}" ;; --central-kafka=*) ARG_CENTRAL_KAFKA="${1#*=}" ;;
--reset) ARG_RESET=1 ;; --reset) ARG_RESET=1 ;;
--install-prereqs) ARG_INSTALL_PREREQS=1 ;;
--wipe-data) ARG_WIPE_DATA=1 ;;
--purge-images) ARG_PURGE_IMAGES=1 ;;
--purge-backups) ARG_PURGE_BACKUPS=1 ;;
--yes|-y) ARG_YES=1 ;; --yes|-y) ARG_YES=1 ;;
-h|--help) grep '^#' "$0" | sed 's/^#//'; exit 0 ;; -h|--help) grep '^#' "$0" | sed 's/^#//'; exit 0 ;;
*) echo "Unknown arg: $1" >&2; exit 1 ;; *) echo "Unknown arg: $1" >&2; exit 1 ;;
@ -147,10 +169,9 @@ while [ $# -gt 0 ]; do
shift shift
done done
log() { printf '\033[1;36m[deploy]\033[0m %s\n' "$*"; } # Shared output/prompt/.env helpers (log ok warn die hr banner step ask
warn() { printf '\033[1;33m[deploy][warn]\033[0m %s\n' "$*" >&2; } # confirm get_env set_env is_ipv4) -- single definition for all deploy tooling.
die() { printf '\033[1;31m[deploy][err]\033[0m %s\n' "$*" >&2; exit 1; } . "$REPO_DIR/scripts/deploy-lib.sh"
hr() { printf '\033[2m%s\033[0m\n' "----------------------------------------------------------------"; }
# Return the host-published TCP ports this deployment type will bind, so we can # Return the host-published TCP ports this deployment type will bind, so we can
# check them for collisions BEFORE bring-up. Ports come from docker-compose.yml # check them for collisions BEFORE bring-up. Ports come from docker-compose.yml
@ -225,32 +246,71 @@ preflight_ports() {
return $conflict return $conflict
} }
# Prompt with an editable default. Honors --yes (accepts default silently).
ask() {
local __var="$1" __prompt="$2" __default="${3:-}" __reply=""
if [ "$ARG_YES" -eq 1 ]; then
printf -v "$__var" '%s' "$__default"; return
fi
if [ -n "$__default" ]; then
read -r -p "$__prompt [$__default]: " __reply
__reply="${__reply:-$__default}"
else
read -r -p "$__prompt: " __reply
fi
printf -v "$__var" '%s' "$__reply"
}
confirm() {
local __prompt="$1" __reply=""
[ "$ARG_YES" -eq 1 ] && return 0
read -r -p "$__prompt [y/N]: " __reply
[ "$__reply" = "y" ] || [ "$__reply" = "Y" ]
}
cd "$REPO_DIR" || die "REPO_DIR '$REPO_DIR' not found" cd "$REPO_DIR" || die "REPO_DIR '$REPO_DIR' not found"
[ -f docker-compose.yml ] || die "no docker-compose.yml in $REPO_DIR - set REPO_DIR" [ -f docker-compose.yml ] || die "no docker-compose.yml in $REPO_DIR - set REPO_DIR"
command -v docker >/dev/null || die "docker not found"
docker compose version >/dev/null 2>&1 || die "docker compose v2 not available" # --- host prerequisites (fresh-WSL friendly) --------------------------------
# A pristine WSL2 Ubuntu has neither docker-ce nor systemd enabled; check the
# whole chain and either remediate (--install-prereqs / interactive offer) or
# print the exact fix instead of dying with a bare message mid-run.
install_docker_ce() {
log "Installing docker-ce from the official Docker apt repo ..."
sudo apt-get update -qq
sudo apt-get install -y -qq ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -qq
sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
ok "docker-ce installed"
}
ensure_systemd_wsl() {
# Native docker in WSL2 needs systemd (the docker service). Pristine
# distros ship with it off; flipping it requires a WSL restart, which we
# cannot do from inside -- set it up and stop with clear instructions.
[ -d /run/systemd/system ] && return 0
grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null || return 0
warn "systemd is not running -- native docker cannot start without it."
if confirm "Enable systemd in /etc/wsl.conf now (needs a WSL restart afterwards)?"; then
if ! grep -q '^\[boot\]' /etc/wsl.conf 2>/dev/null; then
printf '\n[boot]\nsystemd=true\n' | sudo tee -a /etc/wsl.conf > /dev/null
elif ! grep -q '^systemd=true' /etc/wsl.conf; then
sudo sed -i '/^\[boot\]/a systemd=true' /etc/wsl.conf
fi
ok "systemd enabled in /etc/wsl.conf"
die "now run 'wsl --shutdown' from Windows, reopen this distro, and re-run ./deploy.sh"
fi
die "cannot continue without systemd on WSL (docker service will not start)"
}
preflight_host() {
if ! command -v docker >/dev/null; then
warn "docker is not installed on this host."
if [ "$ARG_INSTALL_PREREQS" -eq 1 ] || confirm "Install docker-ce now (official Docker apt repo)?"; then
ensure_systemd_wsl
install_docker_ce
sudo systemctl enable --now docker 2>/dev/null || true
else
die "install docker-ce first (https://docs.docker.com/engine/install/ubuntu/) or re-run with --install-prereqs"
fi
fi
docker compose version >/dev/null 2>&1 \
|| die "docker compose v2 missing - install the docker-compose-plugin package"
if ! docker info >/dev/null 2>&1; then
# client exists but daemon unreachable: systemd off (WSL) or service down
ensure_systemd_wsl
warn "docker daemon is not running - trying to start it ..."
sudo systemctl enable --now docker 2>/dev/null || true
sleep 2
docker info >/dev/null 2>&1 \
|| die "docker daemon still unreachable - check: systemctl status docker"
ok "docker daemon started"
fi
}
preflight_host
# --- Docker Desktop vs native-daemon guard ---------------------------------- # --- Docker Desktop vs native-daemon guard ----------------------------------
# On WSL, Docker Desktop runs the daemon in its OWN separate distro # On WSL, Docker Desktop runs the daemon in its OWN separate distro
@ -285,23 +345,97 @@ if detect_docker_desktop; then
fi fi
fi fi
# --- .env helpers ----------------------------------------------------------- # --- teardown subcommand -----------------------------------------------------
# Leave-it-down teardown: removes every container across ALL compose profiles,
# the named volumes, and the rendered gitignored configs. Data-tree wipe is
# opt-in (--wipe-data or interactive), preserving backups/ unless
# --purge-backups. Images are kept unless --purge-images. Prints the
# Windows-side cleanup it cannot reach from inside WSL.
ALL_PROFILES=(--profile test --profile auth --profile evpn-test --profile scale-out)
cmd_teardown() {
banner "OpenBMP stack teardown"
local dr; dr="$( [ -f .env ] && get_env OBMP_DATA_ROOT || true )"; dr="${dr:-/var/openbmp}"
echo
log "This will remove:"
echo " - all obmp containers (every profile: core, test, auth, evpn-test, scale-out)"
echo " - docker networks + named volumes (obmp_data-volume, obmp_ts-volume)"
echo " - rendered configs (gobgp/gobgpd.conf, gobgp-evpn/gobgpd.conf)"
[ "$ARG_PURGE_IMAGES" -eq 1 ] && echo " - stack images (--purge-images)"
if [ "$ARG_WIPE_DATA" -eq 1 ]; then
echo " - DATA TREE $dr (--wipe-data)$( [ "$ARG_PURGE_BACKUPS" -eq 1 ] && echo ' INCLUDING backups/' || echo ', preserving backups/')"
else
echo " - (data tree $dr is KEPT; add --wipe-data for a full wipe)"
fi
echo " - .env is always kept"
echo
confirm "Tear the stack down?" || die "aborted - nothing was removed"
log "Stopping and removing containers (all profiles) ..."
if [ "$ARG_PURGE_IMAGES" -eq 1 ]; then
docker compose "${ALL_PROFILES[@]}" down -v --remove-orphans --rmi all 2>&1 | tail -3 || true
else
docker compose "${ALL_PROFILES[@]}" down -v --remove-orphans 2>&1 | tail -3 || true
fi
docker volume rm obmp_data-volume obmp_ts-volume 2>/dev/null || true
ok "containers, networks, volumes removed"
rm -f gobgp/gobgpd.conf gobgp-evpn/gobgpd.conf 2>/dev/null || true
ok "rendered configs removed"
if [ "$ARG_WIPE_DATA" -eq 1 ] || { [ "$ARG_YES" -eq 0 ] && confirm "ALSO wipe the data tree $dr (Postgres history, Kafka spool)?"; }; then
if [ -d "$dr" ]; then
if grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then :; else
warn "Native host data wipe - this destroys all BMP history under $dr."
[ "$ARG_YES" -eq 1 ] && [ "$ARG_WIPE_DATA" -eq 0 ] && die "refusing implicit data wipe under --yes; pass --wipe-data explicitly"
if [ "$ARG_YES" -eq 0 ]; then
read -r -p "Type the data root path to confirm deletion: " __c
[ "$__c" = "$dr" ] || die "confirmation did not match - data tree untouched"
fi
fi
if [ "$ARG_PURGE_BACKUPS" -eq 1 ]; then
sudo rm -rf "${dr:?}"/* 2>/dev/null || true
ok "data tree wiped (including backups)"
else
sudo find "$dr" -mindepth 1 -maxdepth 1 ! -name backups -exec rm -rf {} + 2>/dev/null || true
ok "data tree wiped (backups/ preserved)"
fi
fi
fi
echo
hr
log "Linux-side teardown complete. Windows-side leftovers (if this was a"
log "WSL deployment) need an ELEVATED PowerShell:"
echo
echo " powershell -ExecutionPolicy Bypass -File scripts\\wsl-portproxy.ps1 -Remove"
echo
log "Verify clean: docker ps -a | grep obmp (expect nothing)"
hr
exit 0
}
[ "$COMMAND" = "teardown" ] && cmd_teardown
banner "OpenBMP stack deploy"
# --- .env bootstrap ----------------------------------------------------------
if [ ! -f .env ]; then if [ ! -f .env ]; then
[ -f .env.example ] || die "no .env or .env.example present" [ -f .env.example ] || die "no .env or .env.example present"
cp .env.example .env cp .env.example .env
log "Created .env from .env.example" log "Created .env from .env.example"
fi fi
get_env() { grep -E "^$1=" .env | head -1 | cut -d= -f2- || true; }
set_env() { # Prime sudo early if filesystem setup will need it, so the password prompt
local key="$1" val="$2" # happens HERE (visible, at the start) instead of blocking mid-provision.
if grep -qE "^${key}=" .env; then _dr_probe="$(get_env OBMP_DATA_ROOT)"; _dr_probe="${_dr_probe:-/var/openbmp}"
sed -i "s|^${key}=.*|${key}=${val}|" .env if [ ! -w "$(dirname "$_dr_probe")" ] || { [ -d "$_dr_probe" ] && [ ! -w "$_dr_probe" ]; }; then
else if [ "$(id -u)" -ne 0 ]; then
printf '%s=%s\n' "$key" "$val" >> .env log "Filesystem setup under $_dr_probe needs sudo - authenticating now."
sudo -v || die "sudo authentication failed"
fi
fi fi
}
# --- 1. host type: auto-detect, then override ------------------------------- # --- 1. host type: auto-detect, then override -------------------------------
step 1 6 "Host type"
detected="prod" detected="prod"
if grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then detected="wsl"; fi if grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then detected="wsl"; fi
if [ -n "$ARG_HOSTTYPE" ]; then if [ -n "$ARG_HOSTTYPE" ]; then
@ -309,6 +443,8 @@ if [ -n "$ARG_HOSTTYPE" ]; then
log "Host type: $HOSTTYPE (from flag; auto-detect said '$detected')" log "Host type: $HOSTTYPE (from flag; auto-detect said '$detected')"
else else
log "Auto-detected host type: $detected" log "Auto-detected host type: $detected"
echo " wsl = this distro runs under WSL2 (routers reach the stack via the Windows portproxy)"
echo " prod = native Linux host (routers reach the stack directly)"
ask HOSTTYPE "Host type (wsl/prod)" "$detected" ask HOSTTYPE "Host type (wsl/prod)" "$detected"
fi fi
case "$HOSTTYPE" in wsl|prod) ;; *) die "host type must be 'wsl' or 'prod' (got '$HOSTTYPE')";; esac case "$HOSTTYPE" in wsl|prod) ;; *) die "host type must be 'wsl' or 'prod' (got '$HOSTTYPE')";; esac
@ -355,6 +491,7 @@ normalize_type() {
*) echo "" ;; *) echo "" ;;
esac esac
} }
step 2 6 "Deployment type"
if [ -n "$ARG_SCOPE" ]; then if [ -n "$ARG_SCOPE" ]; then
DTYPE="$(normalize_type "$ARG_SCOPE")" DTYPE="$(normalize_type "$ARG_SCOPE")"
[ -n "$DTYPE" ] || die "unknown --scope '$ARG_SCOPE' (use full-stack|remote|central-store)" [ -n "$DTYPE" ] || die "unknown --scope '$ARG_SCOPE' (use full-stack|remote|central-store)"
@ -371,6 +508,12 @@ log "Deployment type: $DTYPE"
# --- 3. HOST_IP: smart default, editable, validated ------------------------- # --- 3. HOST_IP: smart default, editable, validated -------------------------
# HOST_IP is the address the INTERNAL stack advertises/binds (Kafka listener, # HOST_IP is the address the INTERNAL stack advertises/binds (Kafka listener,
# gobgp->collector). On a remote collector it is also this node's own address. # gobgp->collector). On a remote collector it is also this node's own address.
step 3 6 "Addresses (internal bind + what routers target)"
echo " Two different addresses are about to be asked for:"
echo " HOST_IP = where the stack binds INTERNALLY (Kafka, gobgp). On WSL"
echo " this is the WSL VM address."
echo " Router-facing = what routers put in 'bmp server host ...'. On WSL this"
echo " is the WINDOWS LAN IP (portproxy forwards it inward)."
primary_ip="$(ip -o -4 addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1 || true)" primary_ip="$(ip -o -4 addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1 || true)"
wsl_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)" wsl_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
current_ip="$(get_env HOST_IP)" current_ip="$(get_env HOST_IP)"
@ -461,6 +604,13 @@ BMP_STD_PORT=1790
default_router_port="${ARG_ROUTER_PORT:-$BMP_STD_PORT}" default_router_port="${ARG_ROUTER_PORT:-$BMP_STD_PORT}"
if [ -n "$ARG_ROUTER_IP" ]; then ROUTER_IP="$ARG_ROUTER_IP"; else if [ -n "$ARG_ROUTER_IP" ]; then ROUTER_IP="$ARG_ROUTER_IP"; else
ask ROUTER_IP "Router-facing IP (what 'bmp server' on the routers targets)" "$default_router_ip" ask ROUTER_IP "Router-facing IP (what 'bmp server' on the routers targets)" "$default_router_ip"
# A failed Windows-LAN-IP autodetect leaves a <WINDOWS_LAN_IP> placeholder
# as the default; don't let a non-address sail into .env silently.
while ! is_ipv4 "$ROUTER_IP"; do
[ "$ARG_YES" -eq 1 ] && die "router-facing IP '$ROUTER_IP' is not an IPv4 address (autodetect failed?) - pass --router-ip"
warn "'$ROUTER_IP' is not an IPv4 address."
ask ROUTER_IP "Router-facing IP (dotted quad)" ""
done
fi fi
if [ -n "$ARG_ROUTER_PORT" ]; then ROUTER_PORT="$ARG_ROUTER_PORT"; else if [ -n "$ARG_ROUTER_PORT" ]; then ROUTER_PORT="$ARG_ROUTER_PORT"; else
ask ROUTER_PORT "Router-facing BMP port" "$default_router_port" ask ROUTER_PORT "Router-facing BMP port" "$default_router_port"
@ -480,6 +630,7 @@ if [ "$DTYPE" = "remote" ]; then
fi fi
# --- 6. Authelia toggle (independent axis; N/A for remote - no local UI) ----- # --- 6. Authelia toggle (independent axis; N/A for remote - no local UI) -----
step 4 6 "Grafana authentication"
AUTH_MODE="local" AUTH_MODE="local"
OBMP_DOMAIN="$(get_env OBMP_DOMAIN)" OBMP_DOMAIN="$(get_env OBMP_DOMAIN)"
OBMP_COOKIE_DOMAIN="$(get_env OBMP_COOKIE_DOMAIN)" OBMP_COOKIE_DOMAIN="$(get_env OBMP_COOKIE_DOMAIN)"
@ -526,6 +677,7 @@ type_resources() {
central-store) echo "~16 vCPU / 48-64 GB / NVMe >=250 GB (carries all remotes' data)" ;; central-store) echo "~16 vCPU / 48-64 GB / NVMe >=250 GB (carries all remotes' data)" ;;
esac esac
} }
step 5 6 "Plan review"
echo; hr; log "Deployment plan"; hr echo; hr; log "Deployment plan"; hr
# Record what is being deployed - a greenfield deploy is only reproducible if # Record what is being deployed - a greenfield deploy is only reproducible if
# the checkout is pinned (branch + commit go in the plan and the terminal log). # the checkout is pinned (branch + commit go in the plan and the terminal log).
@ -568,9 +720,11 @@ else
fi fi
fi fi
if ! confirm "Proceed with this plan?"; then # NOTE: default answer is No -- hitting Enter here ABORTS (safe default).
die "aborted before making changes - nothing was modified" if ! confirm "Proceed with this plan? (y = deploy, Enter/n = abort)"; then
die "aborted before making changes - nothing was modified (answer 'y' to deploy)"
fi fi
step 6 6 "Provision + staged bring-up"
# --- 8. write .env ---------------------------------------------------------- # --- 8. write .env ----------------------------------------------------------
set_env HOST_IP "$HOST_IP" set_env HOST_IP "$HOST_IP"
@ -598,10 +752,13 @@ if [ "$ARG_RESET" -eq 1 ]; then
else else
warn "Resetting data tree under $OBMP_DATA_ROOT (WSL test host)" warn "Resetting data tree under $OBMP_DATA_ROOT (WSL test host)"
fi fi
docker compose --profile test --profile auth down -v 2>/dev/null || true docker compose "${ALL_PROFILES[@]}" down -v --remove-orphans 2>/dev/null || true
docker volume rm obmp_data-volume obmp_ts-volume 2>/dev/null || true docker volume rm obmp_data-volume obmp_ts-volume 2>/dev/null || true
sudo rm -rf "${OBMP_DATA_ROOT:?}/"* 2>/dev/null || true # Wipe the tree but keep backups/ -- pg-backup.sh dumps live there and a
log "Reset complete" # reset should not silently destroy them (use 'teardown --purge-backups'
# for that).
sudo find "${OBMP_DATA_ROOT:?}" -mindepth 1 -maxdepth 1 ! -name backups -exec rm -rf {} + 2>/dev/null || true
log "Reset complete (backups/ preserved if present)"
fi fi
# --- 10. run setup.sh ------------------------------------------------------- # --- 10. run setup.sh -------------------------------------------------------
@ -642,7 +799,7 @@ wait_kafka() {
done done
[ "$(docker inspect obmp-kafka --format '{{.State.Status}}' 2>/dev/null)" = running ] \ [ "$(docker inspect obmp-kafka --format '{{.State.Status}}' 2>/dev/null)" = running ] \
|| die "kafka did not stabilise - check: docker logs obmp-kafka" || die "kafka did not stabilise - check: docker logs obmp-kafka"
log " kafka up" ok "kafka up"
} }
wait_postgres() { wait_postgres() {
log " waiting for postgres ..." log " waiting for postgres ..."
@ -658,7 +815,7 @@ if [ "$DTYPE" = "remote" ]; then
wait_kafka wait_kafka
log "Stage 2/2 - collector (forwarding to $CENTRAL_KAFKA) ..." log "Stage 2/2 - collector (forwarding to $CENTRAL_KAFKA) ..."
docker compose up -d collector docker compose up -d collector
log " collector up" ok "collector up"
elif [ "$DTYPE" = "central-store" ]; then elif [ "$DTYPE" = "central-store" ]; then
# Store only: Postgres/psql-app/Grafana/feeders, NO local collector. # Store only: Postgres/psql-app/Grafana/feeders, NO local collector.
@ -668,12 +825,12 @@ elif [ "$DTYPE" = "central-store" ]; then
docker compose up -d psql docker compose up -d psql
wait_postgres wait_postgres
docker compose up -d psql-app grafana whois docker compose up -d psql-app grafana whois
log " store up" ok "store up"
log "Stage 3/3 - feeders/consumers (--profile test) ..." log "Stage 3/3 - feeders/consumers (--profile test) ..."
docker compose --profile test up -d docker compose --profile test up -d
# ensure no local collector is running on a central-store node # ensure no local collector is running on a central-store node
docker compose stop collector 2>/dev/null || true docker compose stop collector 2>/dev/null || true
log " feeders up (no local collector)" ok "feeders up (no local collector)"
else else
# full-stack: collector + store + feeders on one host. # full-stack: collector + store + feeders on one host.
@ -686,10 +843,10 @@ else
docker compose up -d psql docker compose up -d psql
wait_postgres wait_postgres
docker compose up -d collector psql-app grafana whois docker compose up -d collector psql-app grafana whois
log " core up" ok "core up"
log "Stage 3/3 - feeders${AUTH_MODE:+ + auth if authelia} (${profiles[*]}) ..." log "Stage 3/3 - feeders${AUTH_MODE:+ + auth if authelia} (${profiles[*]}) ..."
docker compose "${profiles[@]}" up -d docker compose "${profiles[@]}" up -d
log " feeders up" ok "feeders up"
fi fi
# --- 12. verify ------------------------------------------------------------- # --- 12. verify -------------------------------------------------------------
@ -701,6 +858,13 @@ docker logs obmp-collector --tail 8 2>&1 | sed 's/^/ /' || true
# --- 13. post-deploy notes -------------------------------------------------- # --- 13. post-deploy notes --------------------------------------------------
echo echo
printf '\033[1;32m[deploy] done\033[0m\n' printf '\033[1;32m[deploy] done\033[0m\n'
hr
log "Verify (any time):"
echo " docker compose ps # every service Up / healthy"
echo " docker exec -i obmp-psql psql -U openbmp -d openbmp -c 'SELECT name, state FROM routers;'"
echo " (DB schema is created automatically by psql-app on its first run)"
log "Tear down later: ./deploy.sh teardown (add --wipe-data for a full wipe)"
hr
if [ "$DTYPE" = "remote" ]; then if [ "$DTYPE" = "remote" ]; then
cat <<EOF cat <<EOF
Remote collector active. BMP ingest -> $CENTRAL_KAFKA (central store). Remote collector active. BMP ingest -> $CENTRAL_KAFKA (central store).

View File

@ -64,4 +64,4 @@ Storage scales with prefixes: ~1 GB per peer with a full internet table
> All figures are engineering estimates to calibrate against your own > All figures are engineering estimates to calibrate against your own
> watermarking, not measured specs. Replace them with real numbers once a > watermarking, not measured specs. Replace them with real numbers once a
> prod-realistic node has been load-tested (see docs/production-sizing.md). > prod-realistic node has been load-tested (see production-sizing.md).

View File

@ -1,5 +1,14 @@
# OpenBMP + ExaBGP Route Injector — Full Documentation # OpenBMP + ExaBGP Route Injector — Full Documentation
> **LEGACY LAB WALKTHROUGH.** This guide was written against the original
> single-host dev lab and keeps its literal values (e.g. host `10.40.40.202`,
> NETCONF user `webui`) as *illustrations* — substitute your own `.env`
> values (`HOST_IP`, `ROUTER_FACING_IP`/`PORT`, `IOSXR_NETCONF_*`). For
> current deployments start instead at:
> **[deploy.sh + README quickstart](../README.md)** (operators) and
> **[router-integration.md](router-integration.md)** (network engineers).
> Where this file disagrees with those, they win.
## Table of Contents ## Table of Contents
1. [What Is This Project?](#1-what-is-this-project) 1. [What Is This Project?](#1-what-is-this-project)
@ -48,7 +57,7 @@ This is a **BGP Monitoring Platform (BMP) lab stack** deployed via Docker Compos
``` ```
IOS-XR Routers (9x, AS 65020) IOS-XR Routers (9x, AS 65020)
BMP telemetry on TCP 5000 BMP telemetry on TCP 1790 (maps to collector :5000 in-container)
| |
v v
obmp-collector (openbmp/collector:2.2.3) obmp-collector (openbmp/collector:2.2.3)
@ -101,7 +110,7 @@ Traffic Generator (Phase 4):
|-----------|-------|---------|------| |-----------|-------|---------|------|
| obmp-zookeeper | confluentinc/cp-zookeeper:7.1.1 | 2181 (internal) | Kafka coordination | | obmp-zookeeper | confluentinc/cp-zookeeper:7.1.1 | 2181 (internal) | Kafka coordination |
| obmp-kafka | confluentinc/cp-kafka:7.1.1 | 9092 | Message broker | | obmp-kafka | confluentinc/cp-kafka:7.1.1 | 9092 | Message broker |
| obmp-collector | openbmp/collector:2.2.3 | 5000 | BMP receiver | | obmp-collector | openbmp/collector:2.2.3 | 1790 -> 5000 | BMP receiver (external ${ROUTER_FACING_PORT:-1790} maps to in-container 5000) |
| obmp-psql-app | openbmp/psql-app:2.2.2 | 9005 | Kafka→PostgreSQL consumer | | obmp-psql-app | openbmp/psql-app:2.2.2 | 9005 | Kafka→PostgreSQL consumer |
| obmp-psql | openbmp/postgres:2.2.1 | 5432 | TimescaleDB storage | | obmp-psql | openbmp/postgres:2.2.1 | 5432 | TimescaleDB storage |
| obmp-grafana | grafana/grafana:9.1.7 | 3000 | Visualization | | obmp-grafana | grafana/grafana:9.1.7 | 3000 | Visualization |
@ -119,7 +128,7 @@ Traffic Generator (Phase 4):
- Docker Engine (20.10+) and Docker Compose v2 - Docker Engine (20.10+) and Docker Compose v2
- Host IP `10.40.40.202` reachable from the CML management network - Host IP `10.40.40.202` reachable from the CML management network
- CML routers with BMP configured pointing to the collector's router-facing address (see docs/router-bmp-config.md) - CML routers with BMP configured pointing to the collector's router-facing address (see router-bmp-config.md)
- CML CORE routers configured with ExaBGP as eBGP neighbor (see Section 5) - CML CORE routers configured with ExaBGP as eBGP neighbor (see Section 5)
- `OBMP_DATA_ROOT` directory created (default: `/var/openbmp`) - `OBMP_DATA_ROOT` directory created (default: `/var/openbmp`)
@ -181,15 +190,15 @@ mkdir -p ${OBMP_DATA_ROOT}/grafana/dashboards
sudo chmod -R 777 $OBMP_DATA_ROOT sudo chmod -R 777 $OBMP_DATA_ROOT
``` ```
### 4.3 Initialise the database (first run only) ### 4.3 Database initialisation (automatic)
Create the init trigger file — this causes psql-app to create all tables on startup: psql-app creates the full schema **automatically on its first run** and then
drops a marker file `${OBMP_DATA_ROOT}/config/do_not_init_db` so later
restarts skip it. No manual trigger is needed.
```bash > To force a schema re-init against an empty database, delete the marker:
touch ${OBMP_DATA_ROOT}/config/init_db > `rm ${OBMP_DATA_ROOT}/config/do_not_init_db` and restart `obmp-psql-app`.
``` > Never do this against a database whose data you want to keep.
> **Warning:** Do not create this file on subsequent runs unless you want to wipe and recreate the entire database.
### 4.4 Copy Grafana provisioning files ### 4.4 Copy Grafana provisioning files
@ -554,7 +563,7 @@ Default credentials: `admin` / `openbmp` (anonymous access also enabled)
> History dashboards require `ip_rib_log` and `stats_chg_*` table data. Run `inject.py churn` to populate these. > History dashboards require `ip_rib_log` and `stats_chg_*` table data. Run `inject.py churn` to populate these.
### OBMP-Learning Dashboards (folder: `OBMP-Learning`) ### OBMP-Learning Dashboards (folder: `OBMP-Reference`)
Six learning-focused dashboards in a separate folder, designed to teach BGP concepts using live lab data. Six learning-focused dashboards in a separate folder, designed to teach BGP concepts using live lab data.
@ -569,7 +578,7 @@ Six learning-focused dashboards in a separate folder, designed to teach BGP conc
> **RPKI note:** The `rpki_validator` table is populated by a cron job in `psql-app` every 2 hours. Dashboard `obmp-learn-04` will show zero counts until the cron runs — check `ENABLE_RPKI=1` in `docker-compose.yml`. > **RPKI note:** The `rpki_validator` table is populated by a cron job in `psql-app` every 2 hours. Dashboard `obmp-learn-04` will show zero counts until the cron runs — check `ENABLE_RPKI=1` in `docker-compose.yml`.
### Advanced Analytics Dashboards (folder: `OBMP-Learning`) ### Advanced Analytics Dashboards (folder: `OBMP-Reference`)
Four advanced dashboards that go beyond basic BMP monitoring, unlocking TE/SR data and providing heuristic analysis. Four advanced dashboards that go beyond basic BMP monitoring, unlocking TE/SR data and providing heuristic analysis.
@ -584,7 +593,7 @@ Four advanced dashboards that go beyond basic BMP monitoring, unlocking TE/SR da
### Database Schema Reference ### Database Schema Reference
A standalone database schema reference is also available at `docs/DB_SCHEMA.md`. It documents all 33 tables, 11 views, TE/SR columns, enum types, and common query patterns. A standalone database schema reference is also available at `DB_SCHEMA.md`. It documents all 33 tables, 11 views, TE/SR columns, enum types, and common query patterns.
--- ---
@ -653,7 +662,7 @@ Should show topics like `openbmp.parsed.unicast_prefix`, `openbmp.parsed.peer`,
### 9.6 Grafana datasource ### 9.6 Grafana datasource
Open `http://10.40.40.202:3000` → Configuration → Data Sources → OpenBMP → Test. Open `http://${HOST_IP}:3000` → Configuration → Data Sources → PostgreSQL → Test.
Should return "Database Connection OK". Should return "Database Connection OK".
### 9.7 BMP collector receiving data ### 9.7 BMP collector receiving data
@ -822,7 +831,7 @@ Verify: `show bgp 1.1.1.0/24` — should show `Status: s (active), bestpath`.
### Grafana shows no data ### Grafana shows no data
1. Check datasource: Configuration → Data Sources → OpenBMP → Test 1. Check datasource: Configuration → Data Sources → PostgreSQL → Test
2. Verify psql-app is writing: `docker compose -p obmp logs psql-app` 2. Verify psql-app is writing: `docker compose -p obmp logs psql-app`
3. Check the database directly (see database queries above) 3. Check the database directly (see database queries above)
4. History dashboards need route churn — run `python3 inject.py churn` 4. History dashboards need route churn — run `python3 inject.py churn`
@ -881,9 +890,7 @@ Adjust in `docker-compose.yml` under the `psql-app` service environment block.
|----------|---------|-------------| |----------|---------|-------------|
| `EXABGP_LOCAL_IP` | `10.40.40.202` | Host IP ExaBGP binds to and uses as router-id | | `EXABGP_LOCAL_IP` | `10.40.40.202` | Host IP ExaBGP binds to and uses as router-id |
| `EXABGP_LOCAL_AS` | `65100` | ExaBGP's AS number | | `EXABGP_LOCAL_AS` | `65100` | ExaBGP's AS number |
| `EXABGP_PEER_AS` | `65020` | AS of the IOS-XR lab | | `EXABGP_PEERS` | (semicolon list) | Peers as `ip:peer_as:description;...` — replaces the retired `EXABGP_PEER_AS`/`EXABGP_PEER_1`/`EXABGP_PEER_2` |
| `EXABGP_PEER_1` | `10.100.0.100` | First CORE router to peer with |
| `EXABGP_PEER_2` | `10.100.0.200` | Second CORE router to peer with |
| `EXABGP_API_PORT` | `5050` | Flask API port | | `EXABGP_API_PORT` | `5050` | Flask API port |
### psql-app container (key variables) ### psql-app container (key variables)
@ -955,12 +962,13 @@ Expected: gRPC listening on port 57400.
### Telemetry Data Collected ### Telemetry Data Collected
Telegraf subscribes to two IOS-XR YANG paths at 10-second intervals: Telegraf subscribes to two OpenConfig paths (see telegraf/telegraf.conf,
the source of truth):
| Subscription | YANG Path | Data | | Subscription | OpenConfig path | Interval | Data |
|-------------|-----------|------| |-------------|-----------------|----------|------|
| interface_counters | `Cisco-IOS-XR-infra-statsd-oper:infra-statistics/interfaces/interface/latest/generic-counters` | bytes/packets in/out, errors, drops, CRC | | interface_counters | `openconfig-interfaces:/interfaces/interface/state/counters` | 10s | bytes/packets in/out, errors, drops |
| interface_rates | `Cisco-IOS-XR-infra-statsd-oper:infra-statistics/interfaces/interface/latest/data-rate` | bits/sec in/out, packet rate | | interface_state | `openconfig-interfaces:/interfaces/interface/state` | 30s | oper status, MTU, speed |
### InfluxDB Access ### InfluxDB Access
@ -1110,6 +1118,6 @@ The **Combined BMP + Telemetry** dashboard shows both control-plane (BMP BGP upd
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `TRAFFIC_GEN_API_PORT` | `5051` | Flask API listen port | | `TRAFFIC_GEN_PORT` | `5051` | Flask API listen port |
| `TRAFFIC_GEN_MODE` | `sender` | Operating mode: `sender` or `responder` | | `TRAFFIC_GEN_MODE` | `sender` | Operating mode: `sender` or `responder` |
| `INFLUXDB_TOKEN` | `openbmp-telemetry-token` | InfluxDB auth token (Telegraf) | | `INFLUXDB_TOKEN` | `openbmp-telemetry-token` | InfluxDB auth token (Telegraf) |

View File

@ -41,7 +41,7 @@ feeders.
## 6. Size for BMP burst, not steady state ## 6. Size for BMP burst, not steady state
Generic docs say the collector is light — true, and misleading: the store Generic docs say the collector is light — true, and misleading: the store
saturates under burst (full table x monitored sessions, Postgres write saturates under burst (full table x monitored sessions, Postgres write
amplification). See docs/DEPLOYMENT-TYPES.md for the full model. amplification). See DEPLOYMENT-TYPES.md for the full model.
## 7. Ports 5000 and 3000 are crowded ## 7. Ports 5000 and 3000 are crowded
The collector's 5000 and Grafana's 3000 are commonly already occupied. The collector's 5000 and Grafana's 3000 are commonly already occupied.
@ -100,4 +100,4 @@ setup.sh renders the same port into gobgp's BMP export (gobgp is
host-networked and must hit the *published* port), and the cml tooling reads host-networked and must hit the *published* port), and the cml tooling reads
`ROUTER_FACING_IP`/`ROUTER_FACING_PORT` from `.env`. Remaining hands-on step: `ROUTER_FACING_IP`/`ROUTER_FACING_PORT` from `.env`. Remaining hands-on step:
re-apply the config to routers still pointed at 5000. See re-apply the config to routers still pointed at 5000. See
docs/router-bmp-config.md. router-bmp-config.md.

32
docs/README.md Normal file
View File

@ -0,0 +1,32 @@
# Documentation index
Two tracks depending on who you are. Docs higher in each list are the entry
points; the rest are references you'll be routed to when needed.
## Operators — deploying and running the stack
| Doc | What it covers |
|---|---|
| [../README.md](../README.md) | Quickstart: clone, pin the checkout, run `deploy.sh` |
| [DEPLOYMENT-TYPES.md](DEPLOYMENT-TYPES.md) | The three `--scope` types (full-stack / remote / central-store) + burst sizing model |
| [PORTABILITY-FINDINGS.md](PORTABILITY-FINDINGS.md) | Every greenfield trap and how the tooling handles it — read before deploying to a new host |
| [production-sizing.md](production-sizing.md) | Memory limits and host sizing for production |
| [backup-restore.md](backup-restore.md) | Postgres backup/restore (`scripts/pg-backup.sh`) |
| [security-hardening.md](security-hardening.md) | Hardening checklist for exposed deployments |
| [DB_SCHEMA.md](DB_SCHEMA.md) | Full database schema reference (33 tables, views, query patterns) |
| [ROADMAP.md](ROADMAP.md) | Longer-term work items |
## Network engineers — feeding routers into the stack
| Doc | What it covers |
|---|---|
| [router-integration.md](router-integration.md) | **Start here.** The four ingest paths (BMP, BGP-LS, gNMI, NETCONF), bring-up order, verification |
| [router-bmp-config.md](router-bmp-config.md) | BMP deep-dive: address/port selection (WSL vs native), activation scope, troubleshooting |
| [../router-blueprints/](../router-blueprints/) | Copy-paste IOS-XR fragments with `<PLACEHOLDER>` substitutions |
## Legacy
| Doc | Status |
|---|---|
| [DOCS.md](DOCS.md) | Original single-host dev-lab walkthrough. Values are illustrative; where it disagrees with the docs above, they win. |
| [handoff/](handoff/) | Point-in-time session handoff packets, kept for the record. |

View File

@ -96,9 +96,10 @@ Replace hardcoded IPs in `docker-compose.yml` (Kafka listener, ExaBGP env vars).
Replace hardcoded gNMI addresses in `telegraf/telegraf.conf` with env var substitution. Pass `GNMI_TARGETS` from docker-compose.yml. Replace hardcoded gNMI addresses in `telegraf/telegraf.conf` with env var substitution. Pass `GNMI_TARGETS` from docker-compose.yml.
### A6. Fix InfluxDB datasource URL ### A6. Fix InfluxDB datasource URL — DONE
`obmp-grafana/provisioning/datasources/influxdb-ds.yml`: replace `http://10.40.40.202:8086` with `http://obmp-influxdb:8086`. Completed: `obmp-grafana/provisioning/datasources/influxdb-ds.yml` now reads
`http://obmp-influxdb:8086`.
--- ---

View File

@ -115,8 +115,9 @@ EOSQL
``` ```
> Restoring into a **brand-new container**? Bring `obmp-psql` up first and let > Restoring into a **brand-new container**? Bring `obmp-psql` up first and let
> it initialize, but **do not** create the `config/init_db` trigger file — > it initialize, and create the skip-marker BEFORE psql-app's first start —
> the schema comes from the dump, not from psql-app's first-run migration. > `touch ${OBMP_DATA_ROOT}/config/do_not_init_db` — so psql-app does not run
> its first-run schema migration; the schema comes from the dump instead.
### 3. Restore the dump ### 3. Restore the dump

View File

@ -44,7 +44,7 @@ bmp server 1 initial-refresh delay 60 spread 30
The `initial-refresh delay/spread` staggering matters at scale: it spreads The `initial-refresh delay/spread` staggering matters at scale: it spreads
the full-RIB dumps out when many routers (re)connect at once, which is the the full-RIB dumps out when many routers (re)connect at once, which is the
burst that sizes the whole store tier (docs/DEPLOYMENT-TYPES.md). burst that sizes the whole store tier (DEPLOYMENT-TYPES.md).
## Activating monitoring on BGP sessions ## Activating monitoring on BGP sessions
@ -52,7 +52,7 @@ BMP only reports sessions that are `bmp-activate`d:
``` ```
router bgp <ASN> router bgp <ASN>
neighbor-group RR-CLIENTS neighbor-group RR-CLIENT
bmp-activate server 1 bmp-activate server 1
! !
! !

View File

@ -81,9 +81,10 @@ it before configuring. Covers:
`HOST_IP`** on WSL deployments — the WSL VM's NAT address is `HOST_IP`** on WSL deployments — the WSL VM's NAT address is
unreachable from real routers; see unreachable from real routers; see
[PORTABILITY-FINDINGS.md](PORTABILITY-FINDINGS.md) finding 4). [PORTABILITY-FINDINGS.md](PORTABILITY-FINDINGS.md) finding 4).
- Which port (`ROUTER_FACING_PORT`, defaults to 1790 — the IANA BMP port; - Which port (`ROUTER_FACING_PORT` — 1790, the IANA BMP port, standardized
existing lab routers configured for 5000 need `--router-port 5000` 2026-07; the legacy 5000 is retired. Routers still configured for 5000
until reconfigured). must be re-applied — `cml/proxmox_bmp_config.py` pushes the current
`.env` values).
- The BMP `bmp server 1` block, flat formal form. - The BMP `bmp server 1` block, flat formal form.
- Activation via `neighbor-group BMP-MONITORED`. - Activation via `neighbor-group BMP-MONITORED`.
- **RR-scope-only activation** for load reduction — the single biggest - **RR-scope-only activation** for load reduction — the single biggest

View File

@ -5,7 +5,7 @@ present for the OpenBMP Docker stack to ingest data from it. Copy, edit
the `<PLACEHOLDER>` values for your topology, paste. the `<PLACEHOLDER>` values for your topology, paste.
For the walkthrough (what each ingest path does, in what order to apply, For the walkthrough (what each ingest path does, in what order to apply,
how to verify), see [docs/router-config-guide.md](../docs/router-config-guide.md). how to verify), see [docs/router-integration.md](../docs/router-integration.md).
## Layout ## Layout

View File

@ -62,4 +62,5 @@ router bgp <LOCAL_ASN>
! !
! On the stack side: ! On the stack side:
! docker logs obmp-collector 2>&1 | grep -i "peer up" ! docker logs obmp-collector 2>&1 | grep -i "peer up"
! Grafana -> OBMP-Operations -> Base-1001 -> Peers Table ! Grafana -> OBMP-Operations -> Inventory (router appears, connected)
! Grafana -> OBMP-Operations -> Peer Detail (per-session state + prefixes)

View File

@ -32,6 +32,21 @@ router bgp <LOCAL_ASN>
bmp-activate server 1 bmp-activate server 1
address-family ipv4 unicast address-family ipv4 unicast
route-reflector-client route-reflector-client
!
! *** NEXT-HOP-SELF CAVEAT *********************************************
! next-hop-self here ONLY rewrites the next-hop of eBGP-learned and
! locally-originated routes advertised to clients. That is exactly what
! this lab wants: the full table enters at the cores via eBGP (GoBGP/
! ExaBGP) and clients must resolve those routes via the RR's loopback.
!
! IOS-XR will NOT rewrite the next-hop of iBGP-REFLECTED routes unless
! you ALSO configure:
! router bgp <LOCAL_ASN>
! ibgp policy out enforce-modifications
! On a pure route reflector with no eBGP feed, this line is a silent
! no-op and clients must resolve next-hops via the IGP instead. Do not
! copy it into such a design expecting reflected-route NH rewrite.
! **********************************************************************
next-hop-self next-hop-self
! !
address-family link-state link-state address-family link-state link-state
@ -54,4 +69,4 @@ router bgp <LOCAL_ASN>
! show bgp neighbors <CLIENT_LOOPBACK> advertised-routes | count ! show bgp neighbors <CLIENT_LOOPBACK> advertised-routes | count
! !
! On the stack side (once the routers are BMP-reporting and reflecting): ! On the stack side (once the routers are BMP-reporting and reflecting):
! Grafana -> OBMP-Learning -> RR Loc-RIB Diff (should populate) ! Grafana -> OBMP-Reference -> RR Loc-RIB Diff (should populate)

90
scripts/deploy-lib.sh Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env bash
#
# deploy-lib.sh - shared helpers for deploy.sh and setup.sh.
#
# Both scripts source this file so output styling and .env handling stay
# identical and are defined exactly once. Safe to source multiple times.
#
# Provides:
# log / ok / warn / die / hr - consistent colored output
# banner "title" - boxed section banner
# step N M "title" - numbered decision/stage header
# ask VAR "prompt" "default" - editable-default prompt (honors ARG_YES)
# confirm "prompt" - y/N confirm (honors ARG_YES)
# get_env KEY / set_env KEY VAL - .env read/write without sourcing it
# is_ipv4 ADDR - dotted-quad shape check
#
# Conventions: callers may set ARG_YES=1 for unattended runs and TAG to
# change the log prefix (defaults to "deploy").
TAG="${TAG:-deploy}"
ARG_YES="${ARG_YES:-0}"
log() { printf '\033[1;36m[%s]\033[0m %s\n' "$TAG" "$*"; }
ok() { printf '\033[1;32m[%s] \xe2\x9c\x94\033[0m %s\n' "$TAG" "$*"; }
warn() { printf '\033[1;33m[%s][warn]\033[0m %s\n' "$TAG" "$*" >&2; }
die() { printf '\033[1;31m[%s][err]\033[0m %s\n' "$TAG" "$*" >&2; exit 1; }
hr() { printf '\033[2m%s\033[0m\n' "----------------------------------------------------------------"; }
# Boxed banner, e.g.:
# +--------------------------------------------------------------+
# | OpenBMP stack deploy |
# +--------------------------------------------------------------+
banner() {
local title="$1" width=62
printf '\033[1;36m+%s+\n' "$(printf -- '-%.0s' $(seq 1 $width))"
printf '| %-*s|\n' "$((width - 2))" "$title"
printf '+%s+\033[0m\n' "$(printf -- '-%.0s' $(seq 1 $width))"
}
# Numbered header for decisions and stages: step 2 5 "Deployment type"
step() {
local n="$1" total="$2" title="$3"
printf '\n\033[1;36m[%s] Step %s/%s\033[0m \033[1m%s\033[0m\n' "$TAG" "$n" "$total" "$title"
}
# Prompt with an editable default. Honors ARG_YES (accepts default silently).
ask() {
local __var="$1" __prompt="$2" __default="${3:-}" __reply=""
if [ "$ARG_YES" -eq 1 ]; then
printf -v "$__var" '%s' "$__default"; return
fi
if [ -n "$__default" ]; then
read -r -p "$__prompt [$__default]: " __reply
__reply="${__reply:-$__default}"
else
read -r -p "$__prompt: " __reply
fi
printf -v "$__var" '%s' "$__reply"
}
confirm() {
local __prompt="$1" __reply=""
[ "$ARG_YES" -eq 1 ] && return 0
read -r -p "$__prompt [y/N]: " __reply
[ "$__reply" = "y" ] || [ "$__reply" = "Y" ]
}
# Read a single KEY=value from .env without sourcing it -- .env contains keys
# with hyphens (PROX-CML_*) that a shell `source` would choke on.
get_env() { grep -E "^$1=" .env | head -1 | cut -d= -f2- || true; }
# Set KEY=value in .env: replace the line if present, else append.
set_env() {
local key="$1" val="$2"
if grep -qE "^${key}=" .env; then
# `|` delimiter -- values are hex/IPs, no `|`.
sed -i "s|^${key}=.*|${key}=${val}|" .env
else
printf '%s=%s\n' "$key" "$val" >> .env
fi
}
# Dotted-quad shape check (rejects placeholders like <WINDOWS_LAN_IP>).
is_ipv4() {
local ip="$1" o
[[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
IFS=. read -r -a o <<<"$ip"
for x in "${o[@]}"; do [ "$x" -le 255 ] || return 1; done
return 0
}

View File

@ -20,14 +20,19 @@
.PARAMETER WslIp .PARAMETER WslIp
WSL address to forward to. Default: auto-detected via 'wsl hostname -I'. WSL address to forward to. Default: auto-detected via 'wsl hostname -I'.
.PARAMETER Remove
Teardown mode: delete the portproxy + firewall rules this script creates
(pair with './deploy.sh teardown' on the Linux side).
.EXAMPLE .EXAMPLE
powershell -ExecutionPolicy Bypass -File scripts\wsl-portproxy.ps1 powershell -ExecutionPolicy Bypass -File scripts\wsl-portproxy.ps1
powershell -ExecutionPolicy Bypass -File scripts\wsl-portproxy.ps1 -RouterPort 5000 powershell -ExecutionPolicy Bypass -File scripts\wsl-portproxy.ps1 -Remove
#> #>
param( param(
[int]$RouterPort = 1790, [int]$RouterPort = 1790,
[int]$KafkaPort = 9092, [int]$KafkaPort = 9092,
[string]$WslIp = "" [string]$WslIp = "",
[switch]$Remove
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@ -39,6 +44,24 @@ if (-not (New-Object System.Security.Principal.WindowsPrincipal($id)).IsInRole(
exit 1 exit 1
} }
# -Remove: teardown mode. Deletes the portproxy + firewall rules this script
# creates (used by './deploy.sh teardown' guidance). No WSL IP needed.
if ($Remove) {
foreach ($listen in @($RouterPort, $KafkaPort)) {
netsh interface portproxy delete v4tov4 listenport=$listen listenaddress=0.0.0.0 2>$null | Out-Null
Write-Host "portproxy rule removed: 0.0.0.0:$listen"
$ruleName = "OpenBMP $listen"
if (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue) {
Remove-NetFirewallRule -DisplayName $ruleName
Write-Host "firewall rule removed: $ruleName"
}
}
Write-Host ""
Write-Host "Remaining portproxy rules:"
netsh interface portproxy show v4tov4
exit 0
}
if (-not $WslIp) { if (-not $WslIp) {
$WslIp = (wsl hostname -I).Trim().Split(" ")[0] $WslIp = (wsl hostname -I).Trim().Split(" ")[0]
if (-not $WslIp) { Write-Error "Could not auto-detect the WSL IP; pass -WslIp."; exit 1 } if (-not $WslIp) { Write-Error "Could not auto-detect the WSL IP; pass -WslIp."; exit 1 }

View File

@ -27,20 +27,10 @@ if [ ! -f .env ]; then
exit 1 exit 1
fi fi
# Read a single KEY=value from .env without sourcing it — .env contains keys # Shared helpers (log ok warn die hr get_env set_env ...) — same styling as
# with hyphens (PROX-CML_*) that a shell `source` would choke on. # deploy.sh, defined once in scripts/deploy-lib.sh.
get_env() { grep -E "^$1=" .env | head -1 | cut -d= -f2- || true; } TAG=setup
. "$(dirname "$0")/scripts/deploy-lib.sh"
# Set KEY=value in .env: replace the line if present, else append.
set_env() {
local key="$1" val="$2"
if grep -qE "^${key}=" .env; then
# `|` delimiter — values are hex, no `|`.
sed -i "s|^${key}=.*|${key}=${val}|" .env
else
printf '%s=%s\n' "$key" "$val" >> .env
fi
}
OBMP_DATA_ROOT="$(get_env OBMP_DATA_ROOT)" OBMP_DATA_ROOT="$(get_env OBMP_DATA_ROOT)"
OBMP_DATA_ROOT="${OBMP_DATA_ROOT:-/var/openbmp}" OBMP_DATA_ROOT="${OBMP_DATA_ROOT:-/var/openbmp}"