Compare commits
2 Commits
31286d5d3e
...
8ac156ce86
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ac156ce86 | |||
| cf4e5b07c6 |
56
.env.example
Normal file
56
.env.example
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# OpenBMP stack configuration — copy to .env and fill in.
|
||||||
|
# cp .env.example .env && $EDITOR .env && ./setup.sh
|
||||||
|
# The real .env is git-ignored and never committed.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Core deployment
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Host path for all persistent data (postgres, kafka, grafana, authelia, ...).
|
||||||
|
OBMP_DATA_ROOT=/var/openbmp
|
||||||
|
|
||||||
|
# IP of this host that routers and external clients connect to
|
||||||
|
# (Kafka external listener, BMP source, ExaBGP peering).
|
||||||
|
HOST_IP=changeme
|
||||||
|
|
||||||
|
# Public domain fronting Grafana / Authelia / portal (TLS terminates upstream).
|
||||||
|
OBMP_DOMAIN=changeme.example.com
|
||||||
|
|
||||||
|
# Authelia session-cookie domain — the parent domain of OBMP_DOMAIN so the
|
||||||
|
# cookie is valid across subpaths/subdomains.
|
||||||
|
OBMP_COOKIE_DOMAIN=example.com
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ExaBGP route injector (test profile)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
EXABGP_LOCAL_IP=changeme
|
||||||
|
EXABGP_LOCAL_AS=65100
|
||||||
|
EXABGP_API_PORT=5050
|
||||||
|
# Semicolon-separated peer list, each entry "ip:peer_as:description".
|
||||||
|
EXABGP_PEERS=10.100.0.100:65020:CML-R9K-CORE-01;10.100.0.200:65020:CML-R9K-CORE-02
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CML lab API + IOS-XR NETCONF (used by cml/ automation scripts)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
PROX-CML_URL=http://changeme
|
||||||
|
PROX-CML_USERNAME=changeme
|
||||||
|
PROX-CML_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Default IOS-XR NETCONF credentials, plus the admin-tier override for routers
|
||||||
|
# that use a separate account.
|
||||||
|
IOSXR_NETCONF_USER=changeme
|
||||||
|
IOSXR_NETCONF_PASS=changeme
|
||||||
|
IOSXR_NETCONF_ADMIN_USER=changeme
|
||||||
|
IOSXR_NETCONF_ADMIN_PASS=changeme
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Integrations
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
GITEA_API_KEY=changeme
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Authelia secrets — leave BLANK; setup.sh generates them with openssl on a
|
||||||
|
# fresh host and appends them here. Existing values are never overwritten.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
AUTHELIA_SESSION_SECRET=
|
||||||
|
AUTHELIA_JWT_SECRET=
|
||||||
|
AUTHELIA_STORAGE_ENCRYPTION_KEY=
|
||||||
47
DOCS.md
47
DOCS.md
@ -127,6 +127,37 @@ Traffic Generator (Phase 4):
|
|||||||
|
|
||||||
## 4. Initial Setup (First Time)
|
## 4. Initial Setup (First Time)
|
||||||
|
|
||||||
|
### 4.0 Quick deploy (recommended)
|
||||||
|
|
||||||
|
`setup.sh` bootstraps a fresh host — it creates the data directories, syncs
|
||||||
|
Grafana provisioning, generates Authelia secrets, and renders config. It is
|
||||||
|
idempotent and safe to re-run.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <this-repo-url>
|
||||||
|
cd obmp-docker
|
||||||
|
cp .env.example .env
|
||||||
|
$EDITOR .env # set HOST_IP, OBMP_DOMAIN, OBMP_COOKIE_DOMAIN, credentials
|
||||||
|
./setup.sh
|
||||||
|
docker compose up -d # BMP collector core only
|
||||||
|
docker compose --profile test --profile auth up -d # full stack (lab tools + auth)
|
||||||
|
```
|
||||||
|
|
||||||
|
The stack uses Docker Compose **profiles**:
|
||||||
|
|
||||||
|
| Command | Brings up |
|
||||||
|
|---------|-----------|
|
||||||
|
| `docker compose up -d` | Collector core only — zookeeper, kafka, collector, psql, psql-app, grafana, whois |
|
||||||
|
| `docker compose --profile test up -d` | Core **+** ExaBGP, traffic generator, telegraf, influxdb |
|
||||||
|
| `docker compose --profile auth up -d` | Core **+** Authelia gateway and portal |
|
||||||
|
| `docker compose --profile test --profile auth up -d` | Everything |
|
||||||
|
|
||||||
|
The bare `docker compose up` is the shippable standalone BMP collector — it has
|
||||||
|
no dependency on the lab/test tooling.
|
||||||
|
|
||||||
|
The sections below (4.1–4.6) document the equivalent **manual** steps if you
|
||||||
|
prefer not to use `setup.sh`.
|
||||||
|
|
||||||
### 4.1 Clone the repository
|
### 4.1 Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -248,6 +279,22 @@ See `exabgp/iosxr_bgp_config.md` for a Python/ncclient script that pushes all of
|
|||||||
|
|
||||||
Credentials: `username=webui`, `password=cisco`, port 830.
|
Credentials: `username=webui`, `password=cisco`, port 830.
|
||||||
|
|
||||||
|
### 5.6 Bulk BMP config (`cml/proxmox_bmp_config.py`)
|
||||||
|
|
||||||
|
To point a whole lab of IOS-XR routers at the BMP collector at once,
|
||||||
|
`cml/proxmox_bmp_config.py` applies the `bmp server 1` block over SSH (IOS-XR
|
||||||
|
BMP config is not exposed via NETCONF YANG on current releases). It is
|
||||||
|
idempotent.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install paramiko
|
||||||
|
python3 cml/proxmox_bmp_config.py # all routers in the inventory
|
||||||
|
python3 cml/proxmox_bmp_config.py r9k-05 # a single router (smoke test)
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit the `ROUTERS` list at the top of the script for your inventory and the
|
||||||
|
`COLLECTOR_HOST` constant for the collector address.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Starting and Stopping
|
## 6. Starting and Stopping
|
||||||
|
|||||||
@ -35,6 +35,15 @@ Each docker file contains a readme file, see below:
|
|||||||
|
|
||||||
## Using Docker Compose to run everything
|
## Using Docker Compose to run everything
|
||||||
|
|
||||||
|
> **Quick start (recommended):** copy `.env.example` to `.env`, fill it in, and
|
||||||
|
> run `./setup.sh` — it creates the data directories, syncs Grafana
|
||||||
|
> provisioning, and generates Authelia secrets. Then:
|
||||||
|
> ```
|
||||||
|
> docker compose up -d # BMP collector core
|
||||||
|
> docker compose --profile test --profile auth up -d # full stack
|
||||||
|
> ```
|
||||||
|
> See [DOCS.md](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/)
|
||||||
instructions. Docker compose will run everything, including handling restarts of containers.
|
instructions. Docker compose will run everything, including handling restarts of containers.
|
||||||
|
|||||||
51
authelia/configuration.yml.template
Normal file
51
authelia/configuration.yml.template
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
# Authelia configuration template.
|
||||||
|
# setup.sh renders this to ${OBMP_DATA_ROOT}/authelia/configuration.yml,
|
||||||
|
# substituting the ${...} values from .env. Only rendered if the target
|
||||||
|
# file does not already exist — an existing deployment is never overwritten.
|
||||||
|
theme: dark
|
||||||
|
|
||||||
|
server:
|
||||||
|
address: 'tcp://0.0.0.0:9091/authelia'
|
||||||
|
endpoints:
|
||||||
|
authz:
|
||||||
|
forward-auth:
|
||||||
|
implementation: ForwardAuth
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
|
||||||
|
totp:
|
||||||
|
issuer: openbmp
|
||||||
|
|
||||||
|
authentication_backend:
|
||||||
|
file:
|
||||||
|
path: /config/users_database.yml
|
||||||
|
password:
|
||||||
|
algorithm: bcrypt
|
||||||
|
iterations: 12
|
||||||
|
|
||||||
|
session:
|
||||||
|
name: authelia_session
|
||||||
|
secret: ${AUTHELIA_SESSION_SECRET}
|
||||||
|
expiration: 12h
|
||||||
|
inactivity: 6h
|
||||||
|
cookies:
|
||||||
|
- domain: ${OBMP_COOKIE_DOMAIN}
|
||||||
|
authelia_url: https://${OBMP_DOMAIN}/authelia
|
||||||
|
|
||||||
|
identity_validation:
|
||||||
|
reset_password:
|
||||||
|
jwt_secret: ${AUTHELIA_JWT_SECRET}
|
||||||
|
|
||||||
|
storage:
|
||||||
|
local:
|
||||||
|
path: /config/db.sqlite3
|
||||||
|
encryption_key: ${AUTHELIA_STORAGE_ENCRYPTION_KEY}
|
||||||
|
|
||||||
|
access_control:
|
||||||
|
default_policy: one_factor
|
||||||
|
|
||||||
|
notifier:
|
||||||
|
filesystem:
|
||||||
|
filename: /config/notification.txt
|
||||||
15
authelia/users_database.yml.template
Normal file
15
authelia/users_database.yml.template
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
# Authelia user database template.
|
||||||
|
# setup.sh copies this to ${OBMP_DATA_ROOT}/authelia/users_database.yml only
|
||||||
|
# if that file does not already exist. The bcrypt hash below is the default
|
||||||
|
# demo account (username: openbmp). Change it after first login, or generate
|
||||||
|
# a new hash with:
|
||||||
|
# docker run --rm authelia/authelia:4.38 \
|
||||||
|
# authelia crypto hash generate bcrypt --password '<new-password>'
|
||||||
|
users:
|
||||||
|
openbmp:
|
||||||
|
displayname: "OpenBMP Demo"
|
||||||
|
password: "$2b$12$KQiQo1bYWqadD51HlgfgO.M1JfVlA5qP2YVRoBMTPmWq6osPljUTW"
|
||||||
|
email: demo@apodacalab.com
|
||||||
|
groups:
|
||||||
|
- admins
|
||||||
172
cml/proxmox_bmp_config.py
Normal file
172
cml/proxmox_bmp_config.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Apply the OpenBMP `bmp server 1` config to the Proxmox CML lab routers.
|
||||||
|
|
||||||
|
IOS-XR BMP configuration is not exposed via the device's NETCONF YANG schema
|
||||||
|
on this release, so this applies config over the SSH CLI. It is idempotent —
|
||||||
|
re-applying an identical block commits no changes.
|
||||||
|
|
||||||
|
PROX-R9K-03 was built without `bmp-activate` on its BGP neighbor-group; this
|
||||||
|
script adds it (the other 8 routers already have it from the re-addressing).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
pip install paramiko
|
||||||
|
python3 cml/proxmox_bmp_config.py # all 9 routers
|
||||||
|
python3 cml/proxmox_bmp_config.py r9k-05 # one router (smoke test)
|
||||||
|
|
||||||
|
Verify afterwards in OpenBMP:
|
||||||
|
docker exec -i obmp-psql psql -U openbmp -d openbmp \\
|
||||||
|
-c "SELECT name, ip_address, bgp_id, isconnected FROM routers ORDER BY name;"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
# --- BMP collector ---------------------------------------------------------
|
||||||
|
COLLECTOR_HOST = "10.40.40.202"
|
||||||
|
COLLECTOR_PORT = "5000"
|
||||||
|
|
||||||
|
# `bmp server 1` block — flat formal form, identical to the ESXi lab.
|
||||||
|
# Each line is self-contained and applied at the (config)# prompt; a bare
|
||||||
|
# "bmp server 1" is deliberately omitted (it would drop into the bmp submode
|
||||||
|
# and the remaining flat lines would then be invalid).
|
||||||
|
BMP_LINES = [
|
||||||
|
f"bmp server 1 host {COLLECTOR_HOST} port {COLLECTOR_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",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Only PROX-R9K-03 needs this — its BMP-MONITORED neighbor-group was built
|
||||||
|
# without bmp-activate. AS 65021 is the Proxmox lab.
|
||||||
|
BMP_ACTIVATE_LINE = "router bgp 65021 neighbor-group BMP-MONITORED bmp-activate server 1"
|
||||||
|
|
||||||
|
# --- router inventory ------------------------------------------------------
|
||||||
|
# (name, mgmt_ip, user, password, needs_bmp_activate)
|
||||||
|
ROUTERS = [
|
||||||
|
("PROX-R9K-CORE-01", "10.100.1.100", "admin", "cisco", False),
|
||||||
|
("PROX-R9K-CORE-02", "10.100.1.200", "admin", "cisco", False),
|
||||||
|
("PROX-R9K-01", "10.100.1.1", "webui", "cisco", False),
|
||||||
|
("PROX-R9K-02", "10.100.1.2", "webui", "cisco", False),
|
||||||
|
("PROX-R9K-03", "10.100.1.3", "webui", "cisco", True),
|
||||||
|
("PROX-R9K-04", "10.100.1.4", "webui", "cisco", False),
|
||||||
|
("PROX-R9K-05", "10.100.1.5", "webui", "cisco", False),
|
||||||
|
("PROX-R9K-06", "10.100.1.6", "webui", "cisco", False),
|
||||||
|
("PROX-R9K-07", "10.100.1.7", "admin", "cisco", False),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _drain(shell, settle=1.0, limit=15.0, until=None):
|
||||||
|
"""Read from the shell.
|
||||||
|
|
||||||
|
If `until` is given, keep reading until that substring appears (or `limit`
|
||||||
|
elapses). Otherwise return once output stops arriving for `settle` seconds.
|
||||||
|
"""
|
||||||
|
out = ""
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < limit:
|
||||||
|
time.sleep(settle)
|
||||||
|
if shell.recv_ready():
|
||||||
|
out += shell.recv(65535).decode(errors="replace")
|
||||||
|
if until and until in out:
|
||||||
|
break
|
||||||
|
elif until is None:
|
||||||
|
break
|
||||||
|
elif until in out:
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def apply_router(name, ip, user, pwd, needs_activate):
|
||||||
|
"""Apply the BMP config to one router. Returns True on success."""
|
||||||
|
print(f"\n=== {name} ({ip}) ===")
|
||||||
|
lines = list(BMP_LINES)
|
||||||
|
if needs_activate:
|
||||||
|
lines.append(BMP_ACTIVATE_LINE)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(ip, username=user, password=pwd, timeout=15,
|
||||||
|
look_for_keys=False, allow_agent=False)
|
||||||
|
shell = ssh.invoke_shell(width=220, height=1000)
|
||||||
|
time.sleep(2)
|
||||||
|
shell.recv(65535) # banner
|
||||||
|
|
||||||
|
# "(config)#" is the universal IOS-XR config-prompt suffix — used as
|
||||||
|
# the wait marker so the device hostname is irrelevant.
|
||||||
|
CFG = "(config)#"
|
||||||
|
|
||||||
|
shell.send("terminal length 0\n")
|
||||||
|
_drain(shell, 0.5, 5)
|
||||||
|
|
||||||
|
# Enter config mode. IOS-XR may print an active-session banner first,
|
||||||
|
# so wait specifically for the (config) prompt.
|
||||||
|
shell.send("configure terminal\n")
|
||||||
|
out = _drain(shell, 0.4, 15, until=CFG)
|
||||||
|
if CFG not in out:
|
||||||
|
print(f" FAIL: could not enter config mode\n {out[-200:]}")
|
||||||
|
ssh.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Send config lines, paced.
|
||||||
|
for line in lines:
|
||||||
|
shell.send(line + "\n")
|
||||||
|
time.sleep(0.4)
|
||||||
|
_drain(shell, 0.3, 8, until=CFG)
|
||||||
|
|
||||||
|
# Confirm the candidate actually holds changes before committing.
|
||||||
|
shell.send("show configuration\n")
|
||||||
|
cand = _drain(shell, 0.3, 10, until=CFG)
|
||||||
|
if "bmp server" not in cand:
|
||||||
|
print(" OK: no changes (config already present) — nothing to commit")
|
||||||
|
shell.send("abort\n")
|
||||||
|
_drain(shell, 0.5, 5)
|
||||||
|
ssh.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
shell.send("commit\n")
|
||||||
|
result = _drain(shell, 0.3, 25, until=CFG)
|
||||||
|
if "fail" in result.lower() or "error" in result.lower():
|
||||||
|
print(f" FAIL: commit error\n {result[-300:]}")
|
||||||
|
shell.send("abort\n")
|
||||||
|
_drain(shell, 0.5, 5)
|
||||||
|
ssh.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Leave config mode and fully drain (settle-based, no marker) so the
|
||||||
|
# verify output is clean — not contaminated by echoed config lines.
|
||||||
|
shell.send("end\n")
|
||||||
|
_drain(shell, 1.0, 10)
|
||||||
|
|
||||||
|
shell.send("show run formal bmp\n")
|
||||||
|
verify = _drain(shell, 1.0, 12)
|
||||||
|
ok = f"host {COLLECTOR_HOST} port {COLLECTOR_PORT}" in verify
|
||||||
|
print(f" {'OK' if ok else 'FAIL'}: bmp server 1 "
|
||||||
|
f"{'present' if ok else 'NOT found'} in running config")
|
||||||
|
ssh.close()
|
||||||
|
return ok
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" FAIL: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
target = sys.argv[1].lower() if len(sys.argv) > 1 else None
|
||||||
|
results = {}
|
||||||
|
for name, ip, user, pwd, needs_activate in ROUTERS:
|
||||||
|
if target and target not in name.lower():
|
||||||
|
continue
|
||||||
|
results[name] = apply_router(name, ip, user, pwd, needs_activate)
|
||||||
|
|
||||||
|
print(f"\n{'='*48}\n SUMMARY")
|
||||||
|
for name, ok in results.items():
|
||||||
|
print(f" {name:22s} {'OK' if ok else 'FAILED'}")
|
||||||
|
sys.exit(0 if all(results.values()) else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
version: '3'
|
name: obmp
|
||||||
volumes:
|
volumes:
|
||||||
data-volume:
|
data-volume:
|
||||||
driver_opts:
|
driver_opts:
|
||||||
@ -45,7 +45,7 @@ services:
|
|||||||
# Change/add listeners based on your FQDN that the host and other containers can access. You can use
|
# Change/add listeners based on your FQDN that the host and other containers can access. You can use
|
||||||
# an IP address as well. By default, only within the compose/containers can Kafka be accesssed
|
# an IP address as well. By default, only within the compose/containers can Kafka be accesssed
|
||||||
# using port 29092. Outside access can be enabled, but you should use an FQDN listener.
|
# using port 29092. Outside access can be enabled, but you should use an FQDN listener.
|
||||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://obmp-kafka:29092,PLAINTEXT_HOST://10.40.40.202:9092
|
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://obmp-kafka:29092,PLAINTEXT_HOST://${HOST_IP:-10.40.40.202}:9092
|
||||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
||||||
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
||||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||||
@ -93,7 +93,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=openbmp
|
- GF_SECURITY_ADMIN_PASSWORD=openbmp
|
||||||
- GF_AUTH_ANONYMOUS_ENABLED=false
|
- GF_AUTH_ANONYMOUS_ENABLED=false
|
||||||
- GF_SERVER_ROOT_URL=https://bmp.apodacalab.com/grafana/
|
- GF_SERVER_ROOT_URL=https://${OBMP_DOMAIN:-bmp.apodacalab.com}/grafana/
|
||||||
- GF_SERVER_SERVE_FROM_SUB_PATH=true
|
- GF_SERVER_SERVE_FROM_SUB_PATH=true
|
||||||
- GF_AUTH_PROXY_ENABLED=true
|
- GF_AUTH_PROXY_ENABLED=true
|
||||||
- GF_AUTH_PROXY_HEADER_NAME=Remote-User
|
- GF_AUTH_PROXY_HEADER_NAME=Remote-User
|
||||||
@ -206,22 +206,22 @@ services:
|
|||||||
exabgp:
|
exabgp:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-exabgp
|
container_name: obmp-exabgp
|
||||||
|
profiles: ["test"]
|
||||||
build:
|
build:
|
||||||
context: ./exabgp
|
context: ./exabgp
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
# Host networking so ExaBGP can reach CML routers directly on port 179
|
# Host networking so ExaBGP can reach CML routers directly on port 179
|
||||||
network_mode: host
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
# IP on the host that CML routers can reach (matches Kafka external listener)
|
# IP on the host that CML routers reach (BGP peering source)
|
||||||
- EXABGP_LOCAL_IP=10.40.40.202
|
- EXABGP_LOCAL_IP=${HOST_IP:-10.40.40.202}
|
||||||
# ExaBGP presents as AS 65100 (eBGP peer to your AS 65020 lab)
|
# ExaBGP presents as AS 65100 (eBGP peer to the lab route reflectors)
|
||||||
- EXABGP_LOCAL_AS=65100
|
- EXABGP_LOCAL_AS=${EXABGP_LOCAL_AS:-65100}
|
||||||
- EXABGP_PEER_AS=65020
|
# Peer list — ";"-separated entries of "ip:peer_as:description".
|
||||||
# CORE routers to peer with — these propagate routes into the iBGP mesh
|
# Default covers both labs: AS 65020 (ESXi) and AS 65021 (Proxmox).
|
||||||
- EXABGP_PEER_1=10.100.0.100
|
- EXABGP_PEERS=${EXABGP_PEERS:-10.100.0.100:65020:CML-R9K-CORE-01;10.100.0.200:65020:CML-R9K-CORE-02;10.100.1.100:65021:PROX-R9K-CORE-01;10.100.1.200:65021:PROX-R9K-CORE-02}
|
||||||
- EXABGP_PEER_2=10.100.0.200
|
|
||||||
# Flask API port (also on host network)
|
# Flask API port (also on host network)
|
||||||
- EXABGP_API_PORT=5050
|
- EXABGP_API_PORT=${EXABGP_API_PORT:-5050}
|
||||||
volumes:
|
volumes:
|
||||||
# Mount scenarios dir so you can edit/add scenarios without rebuilding
|
# Mount scenarios dir so you can edit/add scenarios without rebuilding
|
||||||
- ./exabgp/scenarios:/exabgp/scenarios
|
- ./exabgp/scenarios:/exabgp/scenarios
|
||||||
@ -230,6 +230,7 @@ services:
|
|||||||
exabgp-ui:
|
exabgp-ui:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-exabgp-ui
|
container_name: obmp-exabgp-ui
|
||||||
|
profiles: ["test"]
|
||||||
build:
|
build:
|
||||||
context: ./exabgp-ui
|
context: ./exabgp-ui
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -242,6 +243,7 @@ services:
|
|||||||
influxdb:
|
influxdb:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-influxdb
|
container_name: obmp-influxdb
|
||||||
|
profiles: ["test"]
|
||||||
image: influxdb:2.7
|
image: influxdb:2.7
|
||||||
ports:
|
ports:
|
||||||
- "8086:8086"
|
- "8086:8086"
|
||||||
@ -259,6 +261,7 @@ services:
|
|||||||
telegraf:
|
telegraf:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-telegraf
|
container_name: obmp-telegraf
|
||||||
|
profiles: ["test"]
|
||||||
build:
|
build:
|
||||||
context: ./telegraf
|
context: ./telegraf
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -273,6 +276,7 @@ services:
|
|||||||
traffic-gen:
|
traffic-gen:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-traffic-gen
|
container_name: obmp-traffic-gen
|
||||||
|
profiles: ["test"]
|
||||||
build:
|
build:
|
||||||
context: ./traffic-gen
|
context: ./traffic-gen
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -288,6 +292,7 @@ services:
|
|||||||
traffic-gen-ui:
|
traffic-gen-ui:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-traffic-gen-ui
|
container_name: obmp-traffic-gen-ui
|
||||||
|
profiles: ["test"]
|
||||||
build:
|
build:
|
||||||
context: ./traffic-gen-ui
|
context: ./traffic-gen-ui
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -297,6 +302,7 @@ services:
|
|||||||
traffic-gen-responder:
|
traffic-gen-responder:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-traffic-gen-responder
|
container_name: obmp-traffic-gen-responder
|
||||||
|
profiles: ["test"]
|
||||||
build:
|
build:
|
||||||
context: ./traffic-gen
|
context: ./traffic-gen
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -336,6 +342,7 @@ services:
|
|||||||
authelia:
|
authelia:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-authelia
|
container_name: obmp-authelia
|
||||||
|
profiles: ["auth"]
|
||||||
image: authelia/authelia:4.38
|
image: authelia/authelia:4.38
|
||||||
ports:
|
ports:
|
||||||
- "9091:9091"
|
- "9091:9091"
|
||||||
@ -347,6 +354,7 @@ services:
|
|||||||
portal:
|
portal:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: obmp-portal
|
container_name: obmp-portal
|
||||||
|
profiles: ["auth"]
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
|
|||||||
@ -3,16 +3,23 @@ set -e
|
|||||||
|
|
||||||
LOCAL_IP=${EXABGP_LOCAL_IP:-10.40.40.202}
|
LOCAL_IP=${EXABGP_LOCAL_IP:-10.40.40.202}
|
||||||
LOCAL_AS=${EXABGP_LOCAL_AS:-65100}
|
LOCAL_AS=${EXABGP_LOCAL_AS:-65100}
|
||||||
PEER_AS=${EXABGP_PEER_AS:-65020}
|
|
||||||
PEER_1=${EXABGP_PEER_1:-10.100.0.100}
|
|
||||||
PEER_2=${EXABGP_PEER_2:-10.100.0.200}
|
|
||||||
API_PORT=${EXABGP_API_PORT:-5050}
|
API_PORT=${EXABGP_API_PORT:-5050}
|
||||||
|
|
||||||
|
# Peer list — ";"-separated entries of "ip:peer_as:description".
|
||||||
|
# Default reproduces the original single-lab (AS 65020) config.
|
||||||
|
EXABGP_PEERS=${EXABGP_PEERS:-10.100.0.100:65020:CML-R9K-CORE-01;10.100.0.200:65020:CML-R9K-CORE-02}
|
||||||
|
|
||||||
echo "================================================================"
|
echo "================================================================"
|
||||||
echo " ExaBGP Route Injector"
|
echo " ExaBGP Route Injector"
|
||||||
echo " Local: ${LOCAL_IP} AS${LOCAL_AS}"
|
echo " Local: ${LOCAL_IP} AS${LOCAL_AS}"
|
||||||
echo " Peers: ${PEER_1}, ${PEER_2} (AS${PEER_AS})"
|
|
||||||
echo " API: http://0.0.0.0:${API_PORT}"
|
echo " API: http://0.0.0.0:${API_PORT}"
|
||||||
|
echo " Peers:"
|
||||||
|
IFS=';' read -ra PEER_ENTRIES <<< "$EXABGP_PEERS"
|
||||||
|
for entry in "${PEER_ENTRIES[@]}"; do
|
||||||
|
[ -z "$entry" ] && continue
|
||||||
|
IFS=':' read -r p_ip p_as p_desc <<< "$entry"
|
||||||
|
echo " - ${p_ip} AS${p_as} (${p_desc})"
|
||||||
|
done
|
||||||
echo "================================================================"
|
echo "================================================================"
|
||||||
|
|
||||||
# Generate ExaBGP 5.x env file — ExaBGP looks here based on pip install prefix
|
# Generate ExaBGP 5.x env file — ExaBGP looks here based on pip install prefix
|
||||||
@ -22,37 +29,25 @@ sed -i 's/drop = true/drop = false/' /usr/local/etc/exabgp/exabgp.env
|
|||||||
sed -i 's/cli = true/cli = false/' /usr/local/etc/exabgp/exabgp.env
|
sed -i 's/cli = true/cli = false/' /usr/local/etc/exabgp/exabgp.env
|
||||||
sed -i "s/destination = 'stdout'/destination = 'stderr'/" /usr/local/etc/exabgp/exabgp.env
|
sed -i "s/destination = 'stdout'/destination = 'stderr'/" /usr/local/etc/exabgp/exabgp.env
|
||||||
|
|
||||||
# Generate exabgp.conf from environment
|
# Generate exabgp.conf — one neighbor block per peer-list entry
|
||||||
cat > /tmp/exabgp.conf << EOF
|
cat > /tmp/exabgp.conf << EOF
|
||||||
process api {
|
process api {
|
||||||
run /usr/local/bin/python3 /exabgp/api/server.py;
|
run /usr/local/bin/python3 /exabgp/api/server.py;
|
||||||
encoder text;
|
encoder text;
|
||||||
}
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
neighbor ${PEER_1} {
|
for entry in "${PEER_ENTRIES[@]}"; do
|
||||||
|
[ -z "$entry" ] && continue
|
||||||
|
IFS=':' read -r p_ip p_as p_desc <<< "$entry"
|
||||||
|
cat >> /tmp/exabgp.conf << EOF
|
||||||
|
|
||||||
|
neighbor ${p_ip} {
|
||||||
router-id ${LOCAL_IP};
|
router-id ${LOCAL_IP};
|
||||||
local-address ${LOCAL_IP};
|
local-address ${LOCAL_IP};
|
||||||
local-as ${LOCAL_AS};
|
local-as ${LOCAL_AS};
|
||||||
peer-as ${PEER_AS};
|
peer-as ${p_as};
|
||||||
description "CML-R9K-CORE-01";
|
description "${p_desc}";
|
||||||
hold-time 90;
|
|
||||||
|
|
||||||
family {
|
|
||||||
ipv4 unicast;
|
|
||||||
}
|
|
||||||
|
|
||||||
api {
|
|
||||||
processes [ api ];
|
|
||||||
neighbor-changes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
neighbor ${PEER_2} {
|
|
||||||
router-id ${LOCAL_IP};
|
|
||||||
local-address ${LOCAL_IP};
|
|
||||||
local-as ${LOCAL_AS};
|
|
||||||
peer-as ${PEER_AS};
|
|
||||||
description "CML-R9K-CORE-02";
|
|
||||||
hold-time 90;
|
hold-time 90;
|
||||||
|
|
||||||
family {
|
family {
|
||||||
@ -65,5 +60,6 @@ neighbor ${PEER_2} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
exec exabgp server /tmp/exabgp.conf
|
exec exabgp server /tmp/exabgp.conf
|
||||||
|
|||||||
@ -5,7 +5,7 @@ datasources:
|
|||||||
uid: obmp_influxdb
|
uid: obmp_influxdb
|
||||||
type: influxdb
|
type: influxdb
|
||||||
access: proxy
|
access: proxy
|
||||||
url: http://10.40.40.202:8086
|
url: http://obmp-influxdb:8086
|
||||||
jsonData:
|
jsonData:
|
||||||
version: Flux
|
version: Flux
|
||||||
organization: openbmp
|
organization: openbmp
|
||||||
|
|||||||
127
setup.sh
Executable file
127
setup.sh
Executable file
@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# OpenBMP stack bootstrap — idempotent. Safe to run against an existing
|
||||||
|
# deployment: every step is guarded and never overwrites live config.
|
||||||
|
#
|
||||||
|
# cp .env.example .env # first run only
|
||||||
|
# $EDITOR .env # fill in HOST_IP, OBMP_DOMAIN, ...
|
||||||
|
# ./setup.sh
|
||||||
|
# docker compose up -d # collector core
|
||||||
|
# docker compose --profile test --profile auth up -d # full stack
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
AUTHELIA_IMAGE="authelia/authelia:4.38"
|
||||||
|
|
||||||
|
# --- .env -------------------------------------------------------------------
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
echo "Created .env from .env.example."
|
||||||
|
echo "Edit it (HOST_IP, OBMP_DOMAIN, OBMP_COOKIE_DOMAIN, credentials), then re-run ./setup.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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, 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="${OBMP_DATA_ROOT:-/var/openbmp}"
|
||||||
|
OBMP_DOMAIN="$(get_env OBMP_DOMAIN)"
|
||||||
|
OBMP_COOKIE_DOMAIN="$(get_env OBMP_COOKIE_DOMAIN)"
|
||||||
|
HOST_IP="$(get_env HOST_IP)"
|
||||||
|
|
||||||
|
# --- validate ---------------------------------------------------------------
|
||||||
|
fail=0
|
||||||
|
for var in HOST_IP OBMP_DOMAIN OBMP_COOKIE_DOMAIN; do
|
||||||
|
val="$(get_env "$var")"
|
||||||
|
if [ -z "$val" ] || [[ "$val" == changeme* ]]; then
|
||||||
|
echo "ERROR: $var is unset or still 'changeme' in .env" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$fail" -eq 0 ] || { echo "Fix .env and re-run." >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- privilege helper -------------------------------------------------------
|
||||||
|
# $OBMP_DATA_ROOT is often root-owned (e.g. /var/openbmp). Use sudo only if the
|
||||||
|
# current user cannot write the parent directory.
|
||||||
|
parent="$(dirname "$OBMP_DATA_ROOT")"
|
||||||
|
SUDO=""
|
||||||
|
if [ ! -w "$parent" ] || { [ -d "$OBMP_DATA_ROOT" ] && [ ! -w "$OBMP_DATA_ROOT" ]; }; then
|
||||||
|
SUDO="sudo"
|
||||||
|
echo "Note: using sudo for filesystem setup under $OBMP_DATA_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- data-root directory tree -----------------------------------------------
|
||||||
|
echo "Creating data tree under $OBMP_DATA_ROOT ..."
|
||||||
|
for d in config grafana grafana/provisioning kafka-data zk-data zk-log \
|
||||||
|
postgres/data postgres/ts influxdb authelia; do
|
||||||
|
$SUDO mkdir -p "$OBMP_DATA_ROOT/$d"
|
||||||
|
done
|
||||||
|
# Container processes run as assorted UIDs; lab-permissive perms.
|
||||||
|
$SUDO chmod -R 777 "$OBMP_DATA_ROOT" 2>/dev/null || true
|
||||||
|
|
||||||
|
# --- Grafana provisioning ---------------------------------------------------
|
||||||
|
echo "Syncing Grafana provisioning ..."
|
||||||
|
$SUDO cp -r obmp-grafana/provisioning/. "$OBMP_DATA_ROOT/grafana/provisioning/"
|
||||||
|
|
||||||
|
# --- Authelia secrets -------------------------------------------------------
|
||||||
|
# Generate only if blank/absent; never overwrite an existing value.
|
||||||
|
for key in AUTHELIA_SESSION_SECRET AUTHELIA_JWT_SECRET AUTHELIA_STORAGE_ENCRYPTION_KEY; do
|
||||||
|
cur="$(get_env "$key")"
|
||||||
|
if [ -z "$cur" ]; then
|
||||||
|
set_env "$key" "$(openssl rand -hex 32)"
|
||||||
|
echo "Generated $key"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
AUTHELIA_SESSION_SECRET="$(get_env AUTHELIA_SESSION_SECRET)"
|
||||||
|
AUTHELIA_JWT_SECRET="$(get_env AUTHELIA_JWT_SECRET)"
|
||||||
|
AUTHELIA_STORAGE_ENCRYPTION_KEY="$(get_env AUTHELIA_STORAGE_ENCRYPTION_KEY)"
|
||||||
|
|
||||||
|
# --- Authelia config (fresh-deploy only — never clobber a live config) ------
|
||||||
|
export AUTHELIA_SESSION_SECRET AUTHELIA_JWT_SECRET AUTHELIA_STORAGE_ENCRYPTION_KEY \
|
||||||
|
OBMP_DOMAIN OBMP_COOKIE_DOMAIN
|
||||||
|
SUBST='${AUTHELIA_SESSION_SECRET} ${AUTHELIA_JWT_SECRET} ${AUTHELIA_STORAGE_ENCRYPTION_KEY} ${OBMP_DOMAIN} ${OBMP_COOKIE_DOMAIN}'
|
||||||
|
|
||||||
|
if [ ! -f "$OBMP_DATA_ROOT/authelia/configuration.yml" ]; then
|
||||||
|
envsubst "$SUBST" < authelia/configuration.yml.template \
|
||||||
|
| $SUDO tee "$OBMP_DATA_ROOT/authelia/configuration.yml" > /dev/null
|
||||||
|
echo "Rendered authelia/configuration.yml"
|
||||||
|
else
|
||||||
|
echo "authelia/configuration.yml exists — left untouched"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$OBMP_DATA_ROOT/authelia/users_database.yml" ]; then
|
||||||
|
$SUDO cp authelia/users_database.yml.template \
|
||||||
|
"$OBMP_DATA_ROOT/authelia/users_database.yml"
|
||||||
|
echo "Rendered authelia/users_database.yml (demo user: openbmp)"
|
||||||
|
else
|
||||||
|
echo "authelia/users_database.yml exists — left untouched"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- images -----------------------------------------------------------------
|
||||||
|
echo "Pulling and building images ..."
|
||||||
|
docker compose pull --quiet
|
||||||
|
docker compose --profile test --profile auth build
|
||||||
|
|
||||||
|
# --- done -------------------------------------------------------------------
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Setup complete.
|
||||||
|
|
||||||
|
docker compose up -d # BMP collector core
|
||||||
|
docker compose --profile test --profile auth up -d # full stack (lab + auth)
|
||||||
|
|
||||||
|
EOF
|
||||||
Loading…
x
Reference in New Issue
Block a user