Compare commits

..

2 Commits

Author SHA1 Message Date
Sam
9e67ef10ba docs: consolidate all markdown under docs/, archive the handoff packets
Repo root keeps only README.md; DOCS.md and DB_SCHEMA.md move to docs/ with
their cross-references updated. The chat-session handoff packets land in
docs/handoff/ as point-in-time records with a README marking the repo docs
as the live source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:04:40 -07:00
Sam
05f723556e docs+tooling: land the deploy bundle docs, portproxy script, checkout pinning
deploy.sh referenced DEPLOYMENT-TYPES.md and router-bmp-config.md, which were
never committed (they lived only in a chat session's outputs). Land them under
docs/ along with PORTABILITY-FINDINGS.md covering all 12 greenfield findings,
including the two from today's deploy (chmod-777-vs-psql, telegraf docker API)
and the open router-side gap (cml tooling still targets HOST_IP:5000).

- scripts/wsl-portproxy.ps1: idempotent elevated-PS helper for the Windows
  portproxy + firewall rules; auto-detects the current WSL IP. Replaces the
  copy-paste netsh block as the primary path (raw commands kept as fallback).
- deploy.sh: plan now prints the branch @ commit (+dirty marker) so every
  deploy records exactly what it ran from; doc references point at docs/.
- README: greenfield quickstart with pinned-checkout guidance; warning on the
  manual chmod path that breaks existing Postgres trees (finding 10).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:02:36 -07:00
13 changed files with 677 additions and 9 deletions

View File

@ -33,16 +33,39 @@ Each docker file contains a readme file, see below:
* [PSQL Consumer](psql-app/README.md) * [PSQL Consumer](psql-app/README.md)
## Greenfield deploy (recommended): deploy.sh
Deploying onto a new host? Use `deploy.sh` — it reconciles the host-specific
`.env` values (HOST_IP, router-facing IP/port, auth mode) *before* running
setup.sh, guards against the known portability traps
([docs/PORTABILITY-FINDINGS.md](docs/PORTABILITY-FINDINGS.md)), and brings the
stack up in stages:
```
git clone <repo-url> && cd obmp-docker
git checkout <branch> # pin the deploy: note the branch AND commit
git log -1 --oneline # record what you deployed
./deploy.sh # interactive; or --wsl/--prod --scope ... --yes
```
A greenfield deploy is only reproducible if the checkout is pinned — "clone
and run" silently depends on whatever branch was checked out. Record the
branch + commit with the deployment.
Deployment types (`--scope full-stack|remote|central-store`) are described in
[docs/DEPLOYMENT-TYPES.md](docs/DEPLOYMENT-TYPES.md). Router-side BMP config:
[docs/router-bmp-config.md](docs/router-bmp-config.md).
## Using Docker Compose to run everything ## Using Docker Compose to run everything
> **Quick start (recommended):** copy `.env.example` to `.env`, fill it in, and > **Quick start:** copy `.env.example` to `.env`, fill it in, and
> run `./setup.sh` — it creates the data directories, syncs Grafana > run `./setup.sh` — it creates the data directories, syncs Grafana
> provisioning, and generates Authelia secrets. Then: > provisioning, and generates Authelia secrets. Then:
> ``` > ```
> docker compose up -d # BMP collector core > docker compose up -d # BMP collector core
> docker compose --profile test --profile auth up -d # full stack > docker compose --profile test --profile auth up -d # full stack
> ``` > ```
> See [DOCS.md](DOCS.md) section 4 for details and the manual alternative below. > See [docs/DOCS.md](docs/DOCS.md) section 4 for details and the manual alternative below.
### Install Docker Compose ### Install Docker Compose
You will need docker-compose. You can install that via [Docker Compose](https://docs.docker.com/compose/install/) You will need docker-compose. You can install that via [Docker Compose](https://docs.docker.com/compose/install/)
@ -74,6 +97,11 @@ mkdir -p ${OBMP_DATA_ROOT}/grafana/dashboards
sudo chmod -R 7777 $OBMP_DATA_ROOT sudo chmod -R 7777 $OBMP_DATA_ROOT
``` ```
> **WARNING:** on a host with an *existing* Postgres data tree, a recursive
> chmod makes `psql_server.key` group/world-accessible and Postgres will
> refuse to start. Skip `postgres/` (setup.sh does this for you — see
> [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 > In order to init the DB tables, you must create the file ```${OBMP_DATA_ROOT}/config/init_db```. This should
> only be done once or whenever you want to completely wipe out the DB and start over. > only be done once or whenever you want to completely wipe out the DB and start over.

View File

@ -314,7 +314,7 @@ 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
# --- 2. deployment TYPE (described menu, resolved before auth/endpoints) ----- # --- 2. deployment TYPE (described menu, resolved before auth/endpoints) -----
# Three types from DEPLOYMENT-TYPES.md. Old scope names still accepted as # Three types from docs/DEPLOYMENT-TYPES.md. Old scope names still accepted as
# aliases via --scope so existing invocations don't break. # aliases via --scope so existing invocations don't break.
# full-stack = collocated ingest + store (the old 'feeders'/'full') # full-stack = collocated ingest + store (the old 'feeders'/'full')
# remote = collector/forwarder only (the old 'standalone') # remote = collector/forwarder only (the old 'standalone')
@ -527,8 +527,13 @@ type_resources() {
esac esac
} }
echo; hr; log "Deployment plan"; hr echo; hr; log "Deployment plan"; hr
# 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).
git_desc="$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
[ -n "$git_desc" ] && git_desc="$git_desc @ $(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null)$(git -C "$REPO_DIR" diff --quiet 2>/dev/null || echo ' (dirty)')"
cat <<EOF cat <<EOF
Repo dir : $REPO_DIR Repo dir : $REPO_DIR
Checkout : ${git_desc:-not a git checkout}
Host type : $HOSTTYPE (auto-detected: $detected) Host type : $HOSTTYPE (auto-detected: $detected)
Deployment type : $DTYPE Deployment type : $DTYPE
$(type_desc "$DTYPE") $(type_desc "$DTYPE")
@ -725,21 +730,25 @@ if [ "$HOSTTYPE" = "wsl" ] && [ "$DTYPE" != "central-store" ]; then
WSL note: HOST_IP ${HOST_IP} is the WSL VM address - reachable from Windows WSL note: HOST_IP ${HOST_IP} is the WSL VM address - reachable from Windows
and the compose network, but NOT from physical routers, and it changes on and the compose network, but NOT from physical routers, and it changes on
'wsl --shutdown'. Routers must target the router-facing address 'wsl --shutdown'. Routers must target the router-facing address
(${ROUTER_IP}:${ROUTER_PORT}) via a portproxy on the Windows host. Run (${ROUTER_IP}:${ROUTER_PORT}) via a portproxy on the Windows host. In an
(elevated PowerShell), substituting the real Windows LAN IP if you left a ELEVATED PowerShell, run the repo's helper (auto-detects the current WSL IP,
placeholder above: idempotent):
powershell -ExecutionPolicy Bypass -File scripts/wsl-portproxy.ps1 -RouterPort ${ROUTER_PORT}
or add the rules by hand:
netsh interface portproxy add v4tov4 listenport=${ROUTER_PORT} listenaddress=0.0.0.0 connectport=${COLLECTOR_PORT} connectaddress=${HOST_IP} netsh interface portproxy add v4tov4 listenport=${ROUTER_PORT} listenaddress=0.0.0.0 connectport=${COLLECTOR_PORT} connectaddress=${HOST_IP}
netsh interface portproxy add v4tov4 listenport=9092 listenaddress=0.0.0.0 connectport=9092 connectaddress=${HOST_IP} netsh interface portproxy add v4tov4 listenport=9092 listenaddress=0.0.0.0 connectport=9092 connectaddress=${HOST_IP}
New-NetFirewallRule -DisplayName "OpenBMP ${ROUTER_PORT}" -Direction Inbound -LocalPort ${ROUTER_PORT} -Protocol TCP -Action Allow New-NetFirewallRule -DisplayName "OpenBMP ${ROUTER_PORT}" -Direction Inbound -LocalPort ${ROUTER_PORT} -Protocol TCP -Action Allow
Re-run the portproxy adds after any WSL restart. See router-bmp-config.md. Re-run after any WSL restart (the WSL IP changes). See docs/router-bmp-config.md.
EOF EOF
elif [ "$DTYPE" != "central-store" ]; then elif [ "$DTYPE" != "central-store" ]; then
cat <<EOF cat <<EOF
Native host. Ensure the firewall permits inbound TCP ${ROUTER_PORT} from the Native host. Ensure the firewall permits inbound TCP ${ROUTER_PORT} from the
router mgmt network. Router config: see router-bmp-config.md (use router mgmt network. Router config: see docs/router-bmp-config.md (use
${ROUTER_IP}:${ROUTER_PORT} as the bmp server host/port). ${ROUTER_IP}:${ROUTER_PORT} as the bmp server host/port).
EOF EOF
fi fi

67
docs/DEPLOYMENT-TYPES.md Normal file
View File

@ -0,0 +1,67 @@
# Deployment types
`deploy.sh` deploys one of three types (`--scope`). This is the reference for
what each includes, when to use it, and what to size it for. Old scope names
(`core`/`feeders`/`full`/`standalone`) are accepted as aliases.
## 1. full-stack
Everything on one host: collector + Kafka + Postgres/TimescaleDB + Grafana +
whois + lab feeders (`--profile test`: ExaBGP, GoBGP, traffic-gen, InfluxDB,
telegraf, rib-poller, churn/kafka-lag monitors). BMP terminates here and data
is stored here.
- **Use when:** one node monitors the whole fabric (lab, or small prod).
- **Excludes:** nothing except Authelia (separate toggle, `--auth authelia` +
`--profile auth`).
- **Resources:** prod-realistic 16 vCPU / 48-64 GB / NVMe >=250 GB;
lab-only 4 vCPU / 16 GB / SSD.
## 2. remote
Collector + local Kafka only. Forwards parsed/raw data to a **central store's
Kafka** (`--central-kafka HOST:PORT`, written to `KAFKA_FQDN`). No local
Postgres/psql-app/Grafana.
- **Use when:** scaling ingest out — run these near the routers so each node's
Kafka spool absorbs only its own routers' RIB dumps instead of every table
hitting one host at once.
- **Resources:** light — 2-4 vCPU / 4-8 GB / 20-40 GB fast disk (the Kafka
spool must buffer a full connect/flap burst from its local routers).
## 3. central-store
The store half: Kafka + Postgres + Grafana + feeders, **no local collector**.
Ingest arrives from remote collectors over Kafka. Run one of these behind N
`remote` nodes.
- **Use when:** you've split ingest with remote collectors.
- **Resources:** same as the full-stack store tier — 16 vCPU / 48-64 GB /
NVMe >=250 GB (it carries all remotes' data).
## Sizing: dimension for BMP burst, not steady state
BMP is event-driven and bursty. Steady state is trivial; the sizing events are
(1) the initial RIB dump on PEER_UP, (2) all routers reconnecting at once
(collector restart / RR failover), (3) reconvergence churn.
The multiplier that actually bites is **full table x monitored sessions**. A
full v4+v6 table is ~1.18M paths; in an RR topology the same table is
reflected to every client, so BMP ingests it once **per monitored session**.
Monitoring the RR-clients group means absorbing full-table x (RRs x clients)
simultaneously — that is what saturates a single host. The wall is Postgres
write latency + write amplification backpressuring psql-app -> Kafka -> the
BMP TCP sessions; the collector process itself is light and is *not* the
sizing driver.
Mitigations: NVMe with real IOPS headroom (>=5000); BMP-monitor pre-policy on
the RRs only (not every client session); stagger connects
(`initial-refresh delay/spread` on the routers); raise `PSQL_MEM_LIMIT` /
Kafka memory; or split ingest with `remote` collectors.
Storage scales with prefixes: ~1 GB per peer with a full internet table
(+~50 MB/day of timeseries); internal peers far less.
> All figures are engineering estimates to calibrate against your own
> watermarking, not measured specs. Replace them with real numbers once a
> prod-realistic node has been load-tested (see docs/production-sizing.md).

View File

@ -584,7 +584,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 `DB_SCHEMA.md` in the repo root. 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 `docs/DB_SCHEMA.md`. It documents all 33 tables, 11 views, TE/SR columns, enum types, and common query patterns.
--- ---

View File

@ -0,0 +1,99 @@
# Portability findings — what breaks on a greenfield deploy
Every artifact hit while standing this stack up on a fresh host (WSL2
`NWE-LT02`, Ubuntu 24.04, native docker-ce 29.x — 2026-07), ordered roughly as
encountered. Each one is something a clean deploy has to survive; most are now
handled automatically by `deploy.sh`/`setup.sh`. Kept as a checklist for the
next new host and as the rationale for the deploy tooling.
## 1. Bind-mounted volumes need pre-existing source dirs
`docker-compose.yml` binds `${OBMP_DATA_ROOT}/postgres/{data,ts}` with
`type:none,o:bind`. Docker will not create these; if absent, Postgres fails
with `no such file or directory`. `setup.sh` creates them — the error only
appears when setup.sh wasn't run on the target. deploy.sh double-checks the
dirs exist after setup.sh.
## 2. Stale HOST_IP survives a copied .env
A `.env` copied from another host carries that host's `HOST_IP`; setup.sh
validation only checks non-empty/not-"changeme", so the stale IP passes and
gets rendered into `gobgpd.conf` and the Grafana root URL. **This is the core
reason deploy.sh exists** — it reconciles HOST_IP (and warns when the .env
value isn't a local address) *before* setup.sh renders anything.
## 3. A populated .env is not a deployed host
The same copied .env had late-stage artifacts (Authelia secrets) while
early-stage host state (Postgres dirs) was missing — proof that setup.sh
completed on a *different* host. Run setup.sh per target, always.
## 4. WSL: HOST_IP is the NAT address; routers can't reach it
`hostname -I` in WSL returns the VM's NAT address — it changes on
`wsl --shutdown` and physical routers cannot reach it. Routers must target the
**Windows LAN IP**, forwarded into WSL via `netsh portproxy`
(`scripts/wsl-portproxy.ps1`). deploy.sh separates "router-facing IP:port"
from HOST_IP and auto-detects the Windows LAN IP via powershell.exe interop.
## 5. Cold-start contention masquerades as component bugs
Bringing the whole stack up at once crash-looped Kafka on a preflight
"writable" check that was actually resource contention, not permissions.
deploy.sh stages the bring-up: infra (zookeeper/kafka) -> psql -> core ->
feeders.
## 6. Size for BMP burst, not steady state
Generic docs say the collector is light — true, and misleading: the store
saturates under burst (full table x monitored sessions, Postgres write
amplification). See docs/DEPLOYMENT-TYPES.md for the full model.
## 7. Ports 5000 and 3000 are crowded
The collector's 5000 and Grafana's 3000 are commonly already occupied.
deploy.sh runs a port-collision preflight before touching anything. The
router-facing BMP port default moved 5000 -> **1790** (the IANA BMP port);
the collector still listens on 5000 inside the container.
## 8. Docker Desktop breaks host bind mounts on WSL (the big one)
Symptom: Postgres mount fails `no such file or directory` on a path `ls`
shows exists. Cause: Docker Desktop runs the daemon in its own WSL distro
(`docker-desktop` VM), so host bind mounts resolve against *that* filesystem,
not the distro you ran compose from. No mkdir fixes it. **Fix: native
docker-in-WSL** — disable Desktop's WSL integration, install docker-ce,
`systemctl enable --now docker`, and confirm
`docker info | grep 'Operating System'` does not say Docker Desktop.
deploy.sh detects Docker Desktop and warns/refuses.
### 8b. Desktop residue splits the mount namespace
After installing native docker, the CLI couldn't reach `/run/docker.sock`
even though dockerd held it — a stale mount-namespace split left by Desktop.
`wsl --shutdown` and reopening the distro cleared it.
## 9. Grafana customizations must live in the repo, not grafana.db
Dashboards hand-built in the UI exist only in that host's `grafana.db` and
vanish on a greenfield deploy. All dashboards are now committed as JSON under
`obmp-grafana/dashboards/` and setup.sh syncs both provisioning YAML *and*
dashboard JSONs to the data root. If you edit a dashboard in the UI, export
and commit the JSON.
## 10. Blanket `chmod -R 777` on the data root breaks Postgres re-deploys
setup.sh's lab-permissive chmod over an *existing* data tree made
`psql_server.key` world-accessible; Postgres refuses to boot
(`FATAL: private key file ... has group or world access`) and the whole core
cascades down. Fresh installs never hit it (the key is generated after the
chmod) — it is strictly a re-deploy bug. setup.sh now skips `postgres/`
(the image entrypoint owns those perms) and re-tightens the key/cert to 0600
if a previous run already clobbered them.
## 11. docker-ce 29 rejects Docker API < 1.40 old pinned clients break
telegraf 1.28's `[[inputs.docker]]` hardcodes Docker API version 1.24, which
docker-ce >= 29 refuses (`client version 1.24 is too old`). Every
per-container stack metric silently never reached InfluxDB; the stack
dashboards sat empty while `disk`/`postgresql_*` inputs worked fine.
`DOCKER_API_VERSION` env is ignored (version pinned in code) — the fix is
telegraf >= 1.30, which negotiates the API version (telegraf/Dockerfile now
pins 1.33). Watch for the same failure in any other tooling that talks to the
daemon with an old vendored client.
## 12. OPEN: router-side automation still targets the old lab's values
`cml/proxmox_bmp_config.py` sends routers at `HOST_IP` port `5000`, and
`cml/xrd-node-definition.yaml` bakes in `10.40.40.202:5000` — both correct on
the old native-Linux dev lab, wrong on a WSL host (finding 4: routers must
target the router-facing address, and the port default is now 1790). Fold
`ROUTER_FACING_IP`/`ROUTER_FACING_PORT` from `.env` into that tooling when
the 5000-vs-1790 decision lands. See docs/router-bmp-config.md.

View File

@ -0,0 +1,95 @@
# Handoff brief — OpenBMP greenfield deployment work
**From:** Claude (chat instance) working with Sam on deployment/portability.
**To:** Claude (VSCode instance) working directly in the `obmp-docker` Gitea repo.
**Date:** 2026-07-20.
We don't share memory. This packet is the bridge. Sam is the channel between us.
---
## One-paragraph summary
Sam is standing the `obmp-docker` stack up on a **new WSL2 host** (`NWE-LT02`,
Ubuntu 24.04), separate from the dev lab, to shake out greenfield-portability
problems before prod. Over this session we built a `deploy.sh` wrapper around
the repo's existing `setup.sh` that reconciles host-specific `.env` values
*before* provisioning, plus supporting docs. We hit and fixed a long chain of
deployment artifacts (details in `02-FINDINGS.md`). The stack is now **finally
pulling images and coming up on a native docker daemon** after the big
blocker — Docker Desktop's cross-VM bind-mount failure — was resolved by
switching to native docker-in-WSL. **Current open thread: Grafana came up
without its expected customizations, and we're not sure the repo checkout is on
the right branch.** That's where you come in — you can see the repo directly.
---
## What this session produced (lives in Sam's chat outputs, not the repo yet)
A `deploy.sh` + docs bundle intended to drop into the repo root. Sam has these
files; they are NOT yet committed to the repo (that's a decision for you + Sam):
- `deploy.sh` — interactive greenfield deploy wrapper (see `01-DEPLOY-SCRIPT.md`)
- `DEPLOYMENT-TYPES.md` — the three deployment types, locked
- `PORTABILITY-FINDINGS.md` — everything that broke and why
- `router-bmp-config.md` — IOS-XR BMP config reference
- `README.md` — bundle overview
If you want to integrate these into the repo, coordinate branch/placement with
Sam. They were written against the repo's current `setup.sh`, `.env.example`,
and `docker-compose.yml` as pasted into chat.
---
## THE OPEN QUESTION FOR YOU (highest priority)
Grafana is up but **its customizations are missing**. Sam expected them to be
"part of the repo." Then Sam raised the real doubt: **"did I even pull the
correct branch?"**
You can answer this directly — you have the repo. Please determine:
1. **What branch/commit is the working checkout on?**
`git branch --show-current && git log --oneline -5 && git status`
2. **Is the Grafana provisioning committed, and on which branch?**
The stack provisions Grafana from `obmp-grafana/provisioning/` (setup.sh
copies it to `${OBMP_DATA_ROOT}/grafana/provisioning/`, bind-mounted into
the container). Check whether dashboards/datasources actually exist in the
tree, and on which branch:
```
git ls-tree -r --name-only HEAD -- obmp-grafana/provisioning/
git branch -a
git ls-tree -r --name-only origin/main -- obmp-grafana/provisioning/
# and any feature branch you've been working on
```
3. **Were the Grafana customizations ever committed as code**, or did they only
ever exist as UI state in a `grafana.db` (i.e. hand-built, never exported to
provisioning)? This is the crux: "supposed to be in the repo" may have been
intent, not reality. You'd know — did you commit dashboard JSON?
The fork:
- **On wrong branch** → check out the right one, redeploy; provisioning returns.
- **Right branch, provisioning present but not loading** → permissions/timing;
`docker compose restart grafana` after confirming files are in
`/var/openbmp/grafana/provisioning/`.
- **Provisioning never committed** → the customizations were UI-only; they need
exporting to the repo now so future greenfield deploys reproduce them. This is
itself a portability finding.
---
## Immediate deployment state (as of handoff)
- Host: WSL2, Ubuntu 24.04.4, `NWE-LT02`, 22 vCPU / ~15 GB RAM.
- Daemon: **native docker-ce 29.6.2** (NOT Docker Desktop — that was the bug).
`docker info` confirms `Operating System: Ubuntu 24.04.4 LTS`.
- `OBMP_DATA_ROOT=/var/openbmp` (native ext4, `/dev/sdg`).
- Deploy command in use: `./deploy.sh --wsl --scope full-stack --auth local`
- Stack was pulling images and progressing past the volume-mount failure that
blocked every earlier attempt.
- Grafana reachable at `http://<wsl-ip>:3000/grafana/`, login `admin`/`openbmp`.
See `02-FINDINGS.md` for the full list of what broke and how it was fixed, and
`03-NEXT-STEPS.md` for what remains.

View File

@ -0,0 +1,55 @@
# deploy.sh — what it is and how it behaves
An interactive wrapper around the repo's `setup.sh`. Its job: reconcile
**host-specific** values in `.env` for THIS host *before* `setup.sh` renders
config from them, then bring the stack up in stages. It exists because copying
a populated `.env` between hosts silently propagates stale values (see
`02-FINDINGS.md`, finding 2/3).
## Flow (what happens on a run)
1. Detect host type (WSL vs native Linux); let user override.
2. Guard: refuse/warn if the active daemon is **Docker Desktop** (bind mounts
break under it — see findings).
3. Resolve deployment **type** (menu with descriptions + resource estimates).
4. Resolve `HOST_IP` (internal bind), **router-facing IP+port** (separate — what
`bmp server` targets), auth mode, and for `remote` type the central Kafka.
5. On WSL: auto-detect the **Windows LAN IP** via `powershell.exe` interop for
the router-facing default (with graceful fallback to a placeholder).
6. **Port-collision preflight** — checks host ports the type will bind
(ss/netstat//proc), warns/refuses on conflict.
7. Show a plan; one confirmation.
8. Write reconciled values to `.env`.
9. Clear stale volume handles → run `setup.sh` → verify bind dirs exist →
clear handles again → **staged bring-up** → verify.
10. Print post-deploy notes (WSL: netsh portproxy commands; native: firewall +
router `bmp server` snippet).
## Deployment types (also in DEPLOYMENT-TYPES.md)
- `full-stack` — collector + Kafka + Postgres + Grafana + feeders, one host.
- `remote` — collector + local Kafka only, forwards to a central store's Kafka.
Needs `--central-kafka HOST:PORT`. No local store.
- `central-store` — store + Grafana + feeders, NO local collector; ingest via
remotes.
Old scope names (`core`/`feeders`/`full`/`standalone`) are accepted as aliases.
## Flags
`--wsl|--prod`, `--host-ip`, `--router-ip`, `--router-port` (default **1790**,
the IANA BMP port — collector still listens 5000 internally, mapped),
`--auth local|authelia`, `--scope full-stack|remote|central-store`,
`--central-kafka HOST:PORT`, `--reset`, `--yes`.
## Notes for repo integration
- It calls `./setup.sh` and checks its exit status. It does NOT reimplement
setup.sh's provisioning — it drives it with correct inputs.
- It assumes `.env.example`, `setup.sh`, `docker-compose.yml` are in the same
dir. If `setup.sh` key names change, update deploy.sh's `get_env`/`set_env`
references.
- Router port default was changed to 1790; the lab routers are currently
configured to send BMP to **5000**, so until they're reconfigured, deploy
with `--router-port 5000` OR update the routers. This is an open decision
(see 03-NEXT-STEPS.md).

View File

@ -0,0 +1,74 @@
# Deployment findings — what broke on greenfield and why
Ordered roughly as encountered. Each is a real portability artifact that a
clean deploy has to survive. These are captured in full in the bundle's
`PORTABILITY-FINDINGS.md`; this is the digest for you.
## 1. Bind-mounted volumes need pre-existing source dirs
`docker-compose.yml` binds `${OBMP_DATA_ROOT}/postgres/{data,ts}` with
`type:none,o:bind`. Docker won't create these; if absent, Postgres fails with
`no such file or directory`. `setup.sh` DOES create them (its data-tree loop) —
the error only appears when setup.sh wasn't run on the target.
## 2. Stale HOST_IP survives a copied .env, and setup.sh doesn't catch it
`.env` carried `HOST_IP=10.13.21.65` from another host — not an address on the
new box. setup.sh validation only checks non-empty/not-"changeme", so a stale
IP passes and gets rendered into `gobgpd.conf` + Grafana URL. deploy.sh fixes
HOST_IP before setup.sh runs. **This is the core reason deploy.sh exists.**
## 3. .env travels, host state doesn't
Authelia secrets were populated in the copied `.env` (late setup.sh step) but
the Postgres dirs (early step) were missing — proving setup.sh completed on a
*different* host and the .env was copied over without re-running setup.sh here.
Lesson: a populated .env is not a deployed host; run setup.sh per target.
## 4. WSL: HOST_IP is the NAT addr; not router-reachable
`hostname -I` on WSL returns the VM's NAT address — changes on `wsl --shutdown`,
not reachable by physical routers. Routers must target the **Windows LAN IP**,
forwarded via `netsh portproxy` into WSL. deploy.sh now auto-detects the Windows
LAN IP via `powershell.exe` interop (read-only, no admin needed) and separates
"router-facing IP" from "HOST_IP".
## 5. Cold-start contention looked like a Kafka permission bug
Bringing the whole stack up at once crash-looped Kafka on a preflight
"writable" check that was actually resource contention, not permissions.
deploy.sh stages the bring-up (infra → core → feeders).
## 6. Sizing: dimension for BMP burst, not steady state
Generic docs say the collector is light. True for steady state, misleading for
sizing: the store (Postgres) saturates under burst. Sam's lab is GoBGP pulling
full v4/v6 (~1.18M paths) reflected through RRs to clients — load is
full-table × monitored-sessions, and Postgres write amplification is the wall.
Numbers in DEPLOYMENT-TYPES.md are engineering estimates to calibrate against
Sam's watermarking, explicitly not measured specs.
## 7. Port 5000/3000 collisions
5000 (OpenBMP collector) and 3000 (Grafana) are commonly occupied. deploy.sh
now runs a port-collision preflight before bring-up. Also: router-facing BMP
port default moved 5000 → **1790** (IANA standard) to avoid the crowded 5000;
collector still listens 5000 internally, external maps down.
## 8. **Docker Desktop breaks host bind mounts on WSL** (the big one)
Symptom: Postgres mount fails `no such file or directory` on a path that `ls`
shows exists. Cause: Docker Desktop runs the daemon in its OWN wsl distro
(`docker-desktop` VM); host bind mounts resolve against THAT filesystem, not
the working distro. Two different filesystems. No `mkdir` in the working distro
fixes it. **Fix: switch to native docker-in-WSL** (disable Desktop WSL
integration, install docker-ce, `systemctl enable --now docker`). Confirm with
`docker info | grep 'Operating System'` — must NOT say Docker Desktop. This also
makes the WSL box behave like the native-Linux prod host. deploy.sh now detects
Docker Desktop and warns/refuses. Resolving this is what finally let the stack
pull images and progress.
## 8b. Native docker socket namespace snag (post-switch)
After installing native docker, the CLI couldn't reach the socket
(`/run/docker.sock` listened-on by dockerd but not visible to the shell — mount
namespace split left by Desktop residue). Fixed by `wsl --shutdown` and
reopening, which cleared the stale namespaces. After restart `docker info`
correctly reported Ubuntu.
## 9. OPEN: Grafana customizations missing / branch uncertainty
Grafana came up without expected customizations. Provisioning is supposed to be
repo-committed (`obmp-grafana/provisioning/`). Sam then questioned whether the
correct branch was even checked out. **This is the handoff question for the
VSCode instance** — see 00-HANDOFF-BRIEF.md.

View File

@ -0,0 +1,66 @@
# Next steps & open decisions
## Immediate (blocking the current deploy)
1. **Resolve the branch/provisioning question** (VSCode instance — you own this):
- Confirm which branch the checkout is on and whether it's the intended one.
- Confirm whether `obmp-grafana/provisioning/` (dashboards + datasources) is
committed, and on which branch.
- If provisioning exists but Grafana didn't load it: verify files reached
`/var/openbmp/grafana/provisioning/`, check ownership (Grafana UID 472),
`docker compose restart grafana`, check logs for provisioning errors.
- If provisioning was never committed (UI-only): export current/expected
dashboards to JSON and commit them so greenfield deploys reproduce them.
This closes a real portability gap.
2. **Pin the deploy to a known branch/commit.** A greenfield deploy is only
reproducible if the checkout is pinned. Recommend documenting "clone, check
out <branch> at <commit>, run deploy.sh" — otherwise "clone and run" silently
depends on whatever branch was checked out. Consider adding a branch/commit
check to deploy.sh or the README.
## Deployment decisions still open
3. **Router BMP port: 5000 vs 1790.** deploy.sh now defaults router-facing to
1790 (IANA standard, avoids crowded 5000). BUT the lab routers are currently
configured to send to **5000**. Decision: either
- reconfigure the routers to 1790 (Sam leaned this way — cleaner, avoids
common-port collisions), which needs the IOS-XR `bmp server` port change
on each device and a compose mapping `1790:5000` on native hosts; or
- keep 5000 and deploy with `--router-port 5000`.
If moving to 1790: router-bmp-config.md needs the port updated, and the
native-host compose `ports:` for the collector needs `1790:5000`.
4. **Sizing profiles.** DEPLOYMENT-TYPES.md has three types with ballpark
resources (estimates, not measured). Sam is watermarking a prod-realistic
node to replace estimates with real numbers. When those land, update the
figures. There was an idea to add a `--sizing lab|prod-lite|prod` selector
that writes `*_MEM_LIMIT` blocks into `.env`; deferred until watermarks exist.
5. **Doc name sync.** deploy.sh was renamed to the three deployment types after
the docs were written; a couple of README/doc tables may still reference the
old scope names (feeders/full/standalone). Cosmetic — aliases keep it
working — but worth a sync pass.
## Integration decisions
6. **Where do the bundle files live?** deploy.sh + the 4 docs currently exist
only in Sam's chat outputs. Decide whether they get committed to the repo
(and to which branch), and whether deploy.sh supersedes or complements any
existing deploy tooling in the repo you may know about that I don't.
7. **Reconcile with whatever you (VSCode) have been building.** I have no
visibility into your repo work — the EVPN consumer, gobgp config, any CI, or
other tooling. If there's overlap or conflict between deploy.sh and existing
repo scripts, that's a reconciliation for you + Sam. Flag anything I've
duplicated or contradicted.
## Things that are DONE (don't redo)
- Native docker-in-WSL is installed and working; Docker Desktop is the wrong
substrate (finding 8). Don't re-recommend Desktop.
- The `.env` HOST_IP/auth reconciliation, staged bring-up, port preflight,
Windows-LAN-IP detection, and Docker Desktop guard are all built and tested
in deploy.sh.
- The bind-dir + stale-volume handling in deploy.sh is hardened (down+rm before
AND after setup.sh, plus a dir-existence check).

10
docs/handoff/README.md Normal file
View File

@ -0,0 +1,10 @@
# Handoff packets
Point-in-time bridge documents written by one Claude session for another
during the 2026-07 greenfield portability push (chat instance -> repo/VSCode
instance, with Sam as the channel). Kept for the record; **the live versions
of their content are the repo docs** — findings in
[../PORTABILITY-FINDINGS.md](../PORTABILITY-FINDINGS.md), deployment types in
[../DEPLOYMENT-TYPES.md](../DEPLOYMENT-TYPES.md), router config in
[../router-bmp-config.md](../router-bmp-config.md). Where these packets and
the repo disagree, the repo is right.

98
docs/router-bmp-config.md Normal file
View File

@ -0,0 +1,98 @@
# Router BMP configuration (IOS-XR)
How to point routers at the OpenBMP collector, and which address/port they
must target depending on where the stack runs. Companion automation:
`cml/proxmox_bmp_config.py` (applies the block below over SSH to the Proxmox
lab routers) and `cml/xrd-node-definition.yaml` (bakes it into XRd images).
## What address do routers target?
**Never `HOST_IP` on a WSL deployment.** `HOST_IP` is the WSL VM's NAT
address — unreachable from physical routers and changed by `wsl --shutdown`.
deploy.sh records the correct target in `.env` as
`ROUTER_FACING_IP` / `ROUTER_FACING_PORT`:
| Stack host | Routers target | Path to the collector |
|---|---|---|
| Native Linux | `ROUTER_FACING_IP` (= host IP) : `ROUTER_FACING_PORT` | compose port map -> collector :5000 |
| WSL2 | Windows LAN IP : `ROUTER_FACING_PORT` | `netsh portproxy` (Windows) -> WSL `HOST_IP` -> collector :5000 |
On WSL the Windows-side forwarding must exist first — run
`scripts/wsl-portproxy.ps1` in an **elevated** PowerShell, and re-run it after
every WSL restart (the WSL address changes).
The router-facing port defaults to **1790** (the IANA-registered BMP port);
the collector always listens on **5000 inside the container**. The lab
routers were historically configured for 5000 — until they are reconfigured,
deploy with `--router-port 5000` (see the open decision in
docs/PORTABILITY-FINDINGS.md, finding 12).
## `bmp server` block
Flat formal form (submode-free, safe to paste line-by-line at `(config)#`
the form `proxmox_bmp_config.py` applies):
```
bmp server 1 host <ROUTER_FACING_IP> port <ROUTER_FACING_PORT>
bmp server 1 description OpenBMP-Collector
bmp server 1 update-source MgmtEth0/RP0/CPU0/0
bmp server 1 initial-delay 60
bmp server 1 stats-reporting-period 300
bmp server 1 initial-refresh delay 60 spread 30
```
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
burst that sizes the whole store tier (docs/DEPLOYMENT-TYPES.md).
## Activating monitoring on BGP sessions
BMP only reports sessions that are `bmp-activate`d:
```
router bgp <ASN>
neighbor-group RR-CLIENTS
bmp-activate server 1
!
!
```
**Monitor on the RRs, pre-policy, not on every client session.** In an RR
topology the same full table is reflected to every client; activating BMP on
all reflected sessions makes the collector ingest full-table x sessions
copies simultaneously. Activating on the RR side only is the single biggest
load reduction available.
## Verification
Router side:
```
show bgp bmp server 1 ! state should be ESTAB, uptime climbing
show bgp bmp summary
```
Collector side:
```
docker logs obmp-collector --tail 20 # look for ROUTER_INIT / PEER_UP
docker exec -i obmp-psql psql -U openbmp -d openbmp \
-c "SELECT name, ip_address, bgp_id, isconnected FROM routers ORDER BY name;"
```
Grafana: the OBMP-Operations / Inventory and Peer Detail dashboards populate
within a minute of the first PEER_UP; RIB contents follow as the initial dump
is consumed (large tables take longer — watch the OBMP-Telemetry / Kafka Lag
dashboard drain).
## Troubleshooting
- **Session cycles ESTAB -> idle:** the collector accepted TCP but the
pipeline is backpressured (Postgres write latency) — check the stack
dashboards, not the router.
- **No TCP at all from a WSL deployment:** portproxy rule missing/stale
(re-run `scripts/wsl-portproxy.ps1`), or Windows firewall lacks the inbound
allow for the router-facing port.
- **Router config rejected:** IOS-XR BMP config is not in the NETCONF YANG
schema on the lab release — apply via SSH CLI (which is why
`proxmox_bmp_config.py` drives the CLI).

67
scripts/wsl-portproxy.ps1 Normal file
View File

@ -0,0 +1,67 @@
<#
.SYNOPSIS
Forward router-facing OpenBMP ports from the Windows host into WSL.
.DESCRIPTION
Physical routers cannot reach the WSL VM's NAT address, and that address
changes on every 'wsl --shutdown' (portability finding 4). This script
(re)creates the netsh portproxy rules that forward the router-facing BMP
port and Kafka from the Windows LAN IP into the current WSL address, plus
the matching inbound firewall rules. Idempotent: existing rules for the
same listen ports are replaced, so RE-RUN IT AFTER EVERY WSL RESTART.
Must run in an ELEVATED PowerShell.
.PARAMETER RouterPort
Router-facing BMP listen port on Windows (default 1790, the IANA BMP port).
Forwards to the collector's in-container port 5000.
.PARAMETER WslIp
WSL address to forward to. Default: auto-detected via 'wsl hostname -I'.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File scripts\wsl-portproxy.ps1
powershell -ExecutionPolicy Bypass -File scripts\wsl-portproxy.ps1 -RouterPort 5000
#>
param(
[int]$RouterPort = 1790,
[int]$CollectorPort = 5000,
[int]$KafkaPort = 9092,
[string]$WslIp = ""
)
$ErrorActionPreference = "Stop"
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
if (-not (New-Object System.Security.Principal.WindowsPrincipal($id)).IsInRole(
[System.Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error "Run this from an ELEVATED PowerShell (netsh portproxy needs admin)."
exit 1
}
if (-not $WslIp) {
$WslIp = (wsl hostname -I).Trim().Split(" ")[0]
if (-not $WslIp) { Write-Error "Could not auto-detect the WSL IP; pass -WslIp."; exit 1 }
}
Write-Host "WSL address: $WslIp"
# listenport => connectport ($RouterPort maps DOWN to the collector's 5000)
$maps = @{ $RouterPort = $CollectorPort; $KafkaPort = $KafkaPort }
foreach ($listen in $maps.Keys) {
netsh interface portproxy delete v4tov4 listenport=$listen listenaddress=0.0.0.0 2>$null | Out-Null
netsh interface portproxy add v4tov4 listenport=$listen listenaddress=0.0.0.0 `
connectport=$($maps[$listen]) connectaddress=$WslIp | Out-Null
Write-Host "portproxy 0.0.0.0:$listen -> ${WslIp}:$($maps[$listen])"
$ruleName = "OpenBMP $listen"
if (-not (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound `
-LocalPort $listen -Protocol TCP -Action Allow | Out-Null
Write-Host "firewall rule added: $ruleName"
}
}
Write-Host ""
Write-Host "Active portproxy rules:"
netsh interface portproxy show v4tov4
Write-Host "Routers target the Windows LAN IP on port $RouterPort (see docs/router-bmp-config.md)."