2026-02-27 20:46:59 -07:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# NetBox Diode Project - Bootstrap Script
|
|
|
|
|
# Generates secrets, writes configs, and prepares all services for startup.
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
NETBOX_DOCKER_DIR="/home/user/netbox-docker"
|
|
|
|
|
HOST_IP="172.19.77.160"
|
|
|
|
|
|
|
|
|
|
echo "==> Generating secrets..."
|
|
|
|
|
|
|
|
|
|
gen_secret() {
|
|
|
|
|
openssl rand -hex 32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
REDIS_PASSWORD="$(gen_secret)"
|
|
|
|
|
DIODE_DB_PASSWORD="$(gen_secret)"
|
|
|
|
|
HYDRA_DB_PASSWORD="$(gen_secret)"
|
|
|
|
|
HYDRA_SYSTEM_SECRET="$(gen_secret)"
|
|
|
|
|
|
|
|
|
|
# OAuth2 client secrets (used by Diode services and Orb Agent)
|
|
|
|
|
INGESTER_CLIENT_SECRET="$(gen_secret)"
|
|
|
|
|
RECONCILER_CLIENT_SECRET="$(gen_secret)"
|
|
|
|
|
NETBOX_TO_DIODE_CLIENT_SECRET="$(gen_secret)"
|
|
|
|
|
|
|
|
|
|
echo "==> Writing .env file..."
|
|
|
|
|
cat > "${SCRIPT_DIR}/.env" <<EOF
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Generated by setup.sh — do not edit manually
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
# Host networking
|
|
|
|
|
HOST_IP=${HOST_IP}
|
|
|
|
|
|
|
|
|
|
# Redis
|
|
|
|
|
REDIS_PASSWORD=${REDIS_PASSWORD}
|
|
|
|
|
|
|
|
|
|
# Postgres (Diode)
|
|
|
|
|
DIODE_DB_NAME=diode
|
|
|
|
|
DIODE_DB_USER=diode
|
|
|
|
|
DIODE_DB_PASSWORD=${DIODE_DB_PASSWORD}
|
|
|
|
|
|
|
|
|
|
# Postgres (Hydra)
|
|
|
|
|
HYDRA_DB_NAME=hydra
|
|
|
|
|
HYDRA_DB_USER=hydra
|
|
|
|
|
HYDRA_DB_PASSWORD=${HYDRA_DB_PASSWORD}
|
|
|
|
|
|
|
|
|
|
# Hydra
|
|
|
|
|
HYDRA_SYSTEM_SECRET=${HYDRA_SYSTEM_SECRET}
|
|
|
|
|
|
|
|
|
|
# OAuth2 client IDs (fixed)
|
|
|
|
|
INGESTER_CLIENT_ID=diode-ingester
|
|
|
|
|
RECONCILER_CLIENT_ID=diode-reconciler
|
|
|
|
|
NETBOX_TO_DIODE_CLIENT_ID=netbox-to-diode
|
|
|
|
|
|
|
|
|
|
# OAuth2 client secrets
|
|
|
|
|
INGESTER_CLIENT_SECRET=${INGESTER_CLIENT_SECRET}
|
|
|
|
|
RECONCILER_CLIENT_SECRET=${RECONCILER_CLIENT_SECRET}
|
|
|
|
|
NETBOX_TO_DIODE_CLIENT_SECRET=${NETBOX_TO_DIODE_CLIENT_SECRET}
|
|
|
|
|
|
|
|
|
|
# NetBox connection
|
|
|
|
|
NETBOX_API_URL=http://${HOST_IP}:8000
|
|
|
|
|
NETBOX_API_TOKEN=0123456789abcdef0123456789abcdef01234567
|
|
|
|
|
NETBOX_DIODE_PLUGIN_VERSION=1.7.0
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
echo "==> Writing OAuth2 client credentials..."
|
|
|
|
|
cat > "${SCRIPT_DIR}/oauth2/client/client-credentials.json" <<EOF
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"client_id": "diode-ingester",
|
|
|
|
|
"client_secret": "${INGESTER_CLIENT_SECRET}",
|
|
|
|
|
"client_name": "Diode Ingester",
|
|
|
|
|
"grant_types": ["client_credentials"],
|
|
|
|
|
"token_endpoint_auth_method": "client_secret_post",
|
2026-02-27 21:51:40 -07:00
|
|
|
"scope": "diode:ingest",
|
2026-02-27 20:46:59 -07:00
|
|
|
"audience": ["diode-ingester"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"client_id": "diode-reconciler",
|
|
|
|
|
"client_secret": "${RECONCILER_CLIENT_SECRET}",
|
|
|
|
|
"client_name": "Diode Reconciler",
|
|
|
|
|
"grant_types": ["client_credentials"],
|
|
|
|
|
"token_endpoint_auth_method": "client_secret_post",
|
2026-02-28 01:55:37 -07:00
|
|
|
"scope": "diode:reconcile",
|
2026-02-27 20:46:59 -07:00
|
|
|
"audience": ["diode-reconciler"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"client_id": "netbox-to-diode",
|
|
|
|
|
"client_secret": "${NETBOX_TO_DIODE_CLIENT_SECRET}",
|
|
|
|
|
"client_name": "NetBox to Diode",
|
|
|
|
|
"grant_types": ["client_credentials"],
|
|
|
|
|
"token_endpoint_auth_method": "client_secret_post",
|
2026-02-28 01:55:37 -07:00
|
|
|
"scope": "diode:read diode:write netbox:read netbox:write",
|
2026-02-27 20:46:59 -07:00
|
|
|
"audience": ["netbox-to-diode"]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
echo "==> Writing OAuth2 bootstrap script..."
|
|
|
|
|
cat > "${SCRIPT_DIR}/oauth2/client/bootstrap-clients.sh" <<'BOOTSTRAP_EOF'
|
2026-02-28 01:55:37 -07:00
|
|
|
#!/usr/bin/env bash
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
set -euo pipefail
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
# Constants
|
|
|
|
|
CREDENTIALS_FILE="/etc/config/oauth2/client/client-credentials.json"
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
# Create the credentials file if it doesn't exist
|
|
|
|
|
if [ ! -f "$CREDENTIALS_FILE" ]; then
|
|
|
|
|
echo "ERROR: credentials file $CREDENTIALS_FILE not found"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
# Wait for Hydra to be ready
|
|
|
|
|
sleep 3
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
# Function to create client
|
|
|
|
|
create_client() {
|
|
|
|
|
local client_id=$1
|
|
|
|
|
local client_secret=$2
|
|
|
|
|
local scope=$3
|
|
|
|
|
local exists_in_hydra=false
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
# Check if client exists in Hydra
|
|
|
|
|
if hydra get oauth2-client $client_id --endpoint $HYDRA_ADMIN_URL >/dev/null 2>&1; then
|
|
|
|
|
exists_in_hydra=true
|
2026-02-27 20:46:59 -07:00
|
|
|
fi
|
|
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
# Upsert behavior: remove stale client definition so scope/secret updates are applied.
|
|
|
|
|
if [ "$exists_in_hydra" = true ]; then
|
|
|
|
|
echo "INFO: client $client_id exists in Hydra, replacing to refresh scope/secret"
|
|
|
|
|
hydra delete oauth2-client "$client_id" --endpoint "$HYDRA_ADMIN_URL" >/dev/null
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
hydra create oauth2-client --endpoint "$HYDRA_ADMIN_URL" \
|
|
|
|
|
--id "$client_id" \
|
|
|
|
|
--secret "$client_secret" \
|
|
|
|
|
--grant-type "client_credentials" \
|
|
|
|
|
--response-type "token" \
|
|
|
|
|
--scope "$scope" \
|
|
|
|
|
--token-endpoint-auth-method "client_secret_post" \
|
|
|
|
|
--format json >/dev/null
|
|
|
|
|
|
|
|
|
|
echo "INFO: client $client_id created/updated"
|
|
|
|
|
}
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
# Load client credentials
|
|
|
|
|
jq -c '.[]' "$CREDENTIALS_FILE" | while read -r client; do
|
|
|
|
|
client_id=$(echo "$client" | jq -r '.client_id')
|
|
|
|
|
client_secret=$(echo "$client" | jq -r '.client_secret')
|
|
|
|
|
scope=$(echo "$client" | jq -r '.scope')
|
|
|
|
|
create_client "$client_id" "$client_secret" "$scope"
|
|
|
|
|
done
|
2026-02-27 20:46:59 -07:00
|
|
|
BOOTSTRAP_EOF
|
|
|
|
|
chmod +x "${SCRIPT_DIR}/oauth2/client/bootstrap-clients.sh"
|
|
|
|
|
|
|
|
|
|
echo "==> Writing Orb Agent config..."
|
|
|
|
|
cat > "${SCRIPT_DIR}/orb-agent/agent.yaml" <<EOF
|
|
|
|
|
orb:
|
2026-02-27 21:51:40 -07:00
|
|
|
config_manager:
|
|
|
|
|
active: local
|
2026-02-27 20:46:59 -07:00
|
|
|
backends:
|
|
|
|
|
network_discovery:
|
|
|
|
|
snmp_discovery:
|
2026-02-27 21:51:40 -07:00
|
|
|
common:
|
2026-02-27 20:46:59 -07:00
|
|
|
diode:
|
2026-02-27 21:51:40 -07:00
|
|
|
target: grpc://${HOST_IP}:8080/diode
|
|
|
|
|
client_id: diode-ingester
|
|
|
|
|
client_secret: ${INGESTER_CLIENT_SECRET}
|
|
|
|
|
agent_name: orb-agent-01
|
|
|
|
|
policies:
|
|
|
|
|
network_discovery:
|
|
|
|
|
nmap_scan:
|
2026-02-27 20:46:59 -07:00
|
|
|
config:
|
2026-02-27 21:51:40 -07:00
|
|
|
schedule: "*/30 * * * *"
|
|
|
|
|
timeout: 300
|
|
|
|
|
defaults:
|
|
|
|
|
site: "main"
|
|
|
|
|
scope:
|
|
|
|
|
targets:
|
|
|
|
|
- "10.0.0.0/24"
|
|
|
|
|
snmp_discovery:
|
|
|
|
|
snmp_scan:
|
|
|
|
|
config:
|
2026-02-28 01:55:37 -07:00
|
|
|
# Staged SNMP rollout: crawl known hosts with more tolerant timing to avoid partial walks.
|
|
|
|
|
schedule: "15 */6 * * *"
|
|
|
|
|
timeout: 180
|
|
|
|
|
snmp_timeout: 15
|
|
|
|
|
retries: 2
|
2026-02-27 21:51:40 -07:00
|
|
|
defaults:
|
|
|
|
|
site: "main"
|
2026-02-28 01:55:37 -07:00
|
|
|
role: "Network Device"
|
|
|
|
|
if_type: "other"
|
|
|
|
|
device:
|
|
|
|
|
manufacturer: "Unknown SNMP Manufacturer"
|
|
|
|
|
model: "Unknown SNMP Model"
|
|
|
|
|
interface_patterns:
|
|
|
|
|
- match: ".*"
|
|
|
|
|
type: "other"
|
|
|
|
|
interface:
|
|
|
|
|
description: "Discovered by orb snmp_discovery"
|
2026-02-27 21:51:40 -07:00
|
|
|
scope:
|
|
|
|
|
targets:
|
2026-02-28 01:55:37 -07:00
|
|
|
- host: "10.10.20.182"
|
|
|
|
|
- host: "10.10.20.55"
|
2026-02-27 21:51:40 -07:00
|
|
|
authentication:
|
|
|
|
|
protocol_version: "SNMPv2c"
|
|
|
|
|
community: "public"
|
|
|
|
|
# device_discovery:
|
|
|
|
|
# napalm_scan:
|
|
|
|
|
# config:
|
|
|
|
|
# schedule: "0 */12 * * *"
|
|
|
|
|
# defaults:
|
|
|
|
|
# site: "main"
|
|
|
|
|
# scope:
|
|
|
|
|
# - driver: ios
|
|
|
|
|
# hostname: 10.0.0.1
|
|
|
|
|
# username: admin
|
|
|
|
|
# password: admin
|
2026-02-27 20:46:59 -07:00
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
echo "==> Writing nginx.conf..."
|
|
|
|
|
mkdir -p "${SCRIPT_DIR}/nginx"
|
|
|
|
|
cat > "${SCRIPT_DIR}/nginx/nginx.conf" <<'NGINX_EOF'
|
2026-02-28 01:55:37 -07:00
|
|
|
upstream diode-ingester {
|
|
|
|
|
server diode-ingester:8081;
|
|
|
|
|
}
|
2026-02-27 20:46:59 -07:00
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
upstream diode-reconciler {
|
|
|
|
|
server diode-reconciler:8081;
|
2026-02-27 20:46:59 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 01:55:37 -07:00
|
|
|
upstream diode-auth {
|
|
|
|
|
server diode-auth:8080;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
server {
|
|
|
|
|
listen 8080;
|
|
|
|
|
listen [::]:8080;
|
|
|
|
|
http2 on;
|
|
|
|
|
server_name localhost;
|
|
|
|
|
client_max_body_size 25m;
|
|
|
|
|
|
|
|
|
|
location /auth/introspect {
|
|
|
|
|
internal;
|
|
|
|
|
proxy_method POST;
|
|
|
|
|
proxy_pass http://diode-auth/introspect;
|
|
|
|
|
proxy_pass_request_body off;
|
|
|
|
|
proxy_set_header Content-Length "";
|
|
|
|
|
proxy_set_header X-Original-URI $request_uri;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location /diode/auth {
|
|
|
|
|
rewrite /diode/auth/(.*) /$1 break;
|
|
|
|
|
proxy_pass http://diode-auth;
|
|
|
|
|
|
|
|
|
|
proxy_set_header Host $host;
|
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location /diode/diode.v1.IngesterService {
|
|
|
|
|
auth_request /auth/introspect;
|
|
|
|
|
auth_request_set $auth_status $upstream_status;
|
|
|
|
|
error_page 401 = @error401;
|
|
|
|
|
error_page 403 = @error403;
|
|
|
|
|
|
|
|
|
|
rewrite /diode/(.*) /$1 break;
|
|
|
|
|
grpc_pass grpc://diode-ingester;
|
|
|
|
|
grpc_set_header Host $host;
|
|
|
|
|
grpc_set_header X-Real-IP $remote_addr;
|
|
|
|
|
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
|
|
grpc_set_header X-Forwarded-Proto $scheme;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location /diode/diode.v1.ReconcilerService {
|
|
|
|
|
auth_request /auth/introspect;
|
|
|
|
|
auth_request_set $auth_status $upstream_status;
|
|
|
|
|
error_page 401 = @error401;
|
|
|
|
|
error_page 403 = @error403;
|
|
|
|
|
|
|
|
|
|
rewrite /diode/(.*) /$1 break;
|
|
|
|
|
grpc_pass grpc://diode-reconciler;
|
|
|
|
|
grpc_set_header Host $host;
|
|
|
|
|
grpc_set_header X-Real-IP $remote_addr;
|
|
|
|
|
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
|
|
grpc_set_header X-Forwarded-Proto $scheme;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location /health {
|
|
|
|
|
return 200 'OK';
|
|
|
|
|
add_header Content-Type text/plain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location @error401 {
|
|
|
|
|
return 401 '{"error":"unauthorized","error_description":"Authentication required"}';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location @error403 {
|
|
|
|
|
return 403 '{"error":"forbidden","error_description":"Access denied"}';
|
|
|
|
|
}
|
2026-02-27 20:46:59 -07:00
|
|
|
}
|
|
|
|
|
NGINX_EOF
|
|
|
|
|
|
|
|
|
|
echo "==> Updating NetBox plugins.py..."
|
|
|
|
|
PLUGINS_FILE="${NETBOX_DOCKER_DIR}/configuration/plugins.py"
|
|
|
|
|
cat > "${PLUGINS_FILE}" <<EOF
|
|
|
|
|
PLUGINS = ["netbox_diode_plugin"]
|
|
|
|
|
|
|
|
|
|
PLUGINS_CONFIG = {
|
|
|
|
|
"netbox_diode_plugin": {
|
|
|
|
|
"diode_target_override": "grpc://${HOST_IP}:8080",
|
|
|
|
|
"diode_username": "admin",
|
|
|
|
|
"netbox_to_diode_client_secret": "${NETBOX_TO_DIODE_CLIENT_SECRET}",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
echo "==> Creating NetBox Diode plugin Dockerfile..."
|
|
|
|
|
cat > "${NETBOX_DOCKER_DIR}/Dockerfile.diode-plugin" <<'DOCKERFILE_EOF'
|
|
|
|
|
FROM netboxcommunity/netbox:v4.5-4.0.1
|
|
|
|
|
|
|
|
|
|
# Install the Diode NetBox plugin
|
|
|
|
|
RUN /usr/local/bin/uv pip install --python /opt/netbox/venv/bin/python netboxlabs-diode-netbox-plugin==1.7.0
|
|
|
|
|
DOCKERFILE_EOF
|
|
|
|
|
|
|
|
|
|
echo "==> Updating NetBox docker-compose.override.yml..."
|
|
|
|
|
cat > "${NETBOX_DOCKER_DIR}/docker-compose.override.yml" <<'OVERRIDE_EOF'
|
|
|
|
|
services:
|
|
|
|
|
netbox: &netbox-override
|
|
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
dockerfile: Dockerfile.diode-plugin
|
|
|
|
|
ports:
|
|
|
|
|
- "8000:8080"
|
|
|
|
|
environment:
|
|
|
|
|
SKIP_SUPERUSER: "false"
|
|
|
|
|
SUPERUSER_API_TOKEN: "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
|
SUPERUSER_EMAIL: "admin@example.com"
|
|
|
|
|
SUPERUSER_NAME: "admin"
|
|
|
|
|
SUPERUSER_PASSWORD: "admin"
|
|
|
|
|
ALLOWED_HOSTS: "*"
|
|
|
|
|
extra_hosts:
|
|
|
|
|
- "host.docker.internal:host-gateway"
|
|
|
|
|
netbox-worker:
|
|
|
|
|
<<: *netbox-override
|
|
|
|
|
ports: []
|
|
|
|
|
OVERRIDE_EOF
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "============================================================"
|
|
|
|
|
echo " Setup complete!"
|
|
|
|
|
echo "============================================================"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Generated files:"
|
|
|
|
|
echo " ${SCRIPT_DIR}/.env"
|
|
|
|
|
echo " ${SCRIPT_DIR}/nginx/nginx.conf"
|
|
|
|
|
echo " ${SCRIPT_DIR}/oauth2/client/client-credentials.json"
|
|
|
|
|
echo " ${SCRIPT_DIR}/oauth2/client/bootstrap-clients.sh"
|
|
|
|
|
echo " ${SCRIPT_DIR}/orb-agent/agent.yaml"
|
|
|
|
|
echo " ${NETBOX_DOCKER_DIR}/Dockerfile.diode-plugin"
|
|
|
|
|
echo " ${NETBOX_DOCKER_DIR}/docker-compose.override.yml"
|
|
|
|
|
echo " ${NETBOX_DOCKER_DIR}/configuration/plugins.py"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Next steps:"
|
|
|
|
|
echo " 1. cd ${NETBOX_DOCKER_DIR} && docker compose build --no-cache && docker compose up -d"
|
|
|
|
|
echo " 2. docker compose exec netbox python /opt/netbox/netbox/manage.py migrate netbox_diode_plugin"
|
|
|
|
|
echo " 3. cd ${SCRIPT_DIR} && docker compose up -d"
|
|
|
|
|
echo ""
|