ipam.device_sync mirrors commissioned machines as dcim devices: serial, device type and role (created when missing), status, interfaces with MAC objects, Rollcall's addresses assigned to them and the management address as primary IP. Site, location and rack are matched by name from the machine's location and never created. Ownership by tag and rollcall_machine; a foreign device of the same name is a conflict. lab/netbox/install.sh sets up a NetBox on the lab segment so the drivers are tested without writing to a real NetBox. Refs #10 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
7.4 KiB
Bash
151 lines
7.4 KiB
Bash
#!/bin/bash
|
|
# Lab NetBox for Rollcall's NetBox drivers (IPAM, device sync): a Debian 13
|
|
# container on the lab segment, so tests never write to a real NetBox.
|
|
# Same install as netops (altyapi hosts/netops/kurulum.sh) without PowerDNS,
|
|
# ACME and backups; plain HTTP on :80 (the segment is isolated).
|
|
#
|
|
# Run as root inside the container; rerunnable. Secrets are generated here:
|
|
# /etc/lab-netbox/secrets.env (admin password, Rollcall's API token)
|
|
#
|
|
# On pve1 (example):
|
|
# pct create 225 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst --hostname lab-netbox \
|
|
# --cores 2 --memory 2048 --swap 512 --rootfs zfspool:8 --unprivileged 1 \
|
|
# --net0 name=eth0,bridge=pxenet,ip=10.213.77.6/24,gw=10.213.77.1 --nameserver 10.213.77.1
|
|
# pct start 225 && pct push 225 install.sh /root/install.sh && pct exec 225 -- bash /root/install.sh
|
|
set -euo pipefail
|
|
NETBOX_VERSION=${NETBOX_VERSION:-4.7.4}
|
|
MYIP=${MYIP:-10.213.77.6}
|
|
SITE=${SITE:-lab}
|
|
PREFIXES=${PREFIXES:-"10.213.77.0/24 10.213.78.0/24 10.213.79.0/24 fd00:213:77::/64 fd00:213:78::/64"}
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
apt-get update -q
|
|
apt-get install -yq postgresql valkey-server caddy \
|
|
python3 python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev \
|
|
libpq-dev libssl-dev zlib1g-dev git curl jq
|
|
|
|
install -d -m 700 /etc/lab-netbox
|
|
S=/etc/lab-netbox/secrets.env
|
|
if [ ! -f $S ]; then
|
|
r() { python3 -c "import secrets,sys;print(secrets.token_urlsafe(int(sys.argv[1])))" "$1"; }
|
|
cat > $S <<EOF
|
|
NETBOX_DB_PASS=$(r 24)
|
|
NETBOX_SECRET_KEY=$(r 50)
|
|
NETBOX_PEPPER=$(r 50)
|
|
NETBOX_ADMIN_PASS=$(r 18)
|
|
ROLLCALL_PASS=$(r 18)
|
|
EOF
|
|
chmod 600 $S
|
|
fi
|
|
. $S
|
|
|
|
# ------------------------------------------------------------------ PostgreSQL, Valkey
|
|
sudo() { runuser -u "$2" -- "${@:3}"; } # "sudo -u postgres ..." without sudo
|
|
if [ -z "$(sudo -u postgres psql -tAqc "SELECT 1 FROM pg_roles WHERE rolname='netbox'")" ]; then
|
|
sudo -u postgres psql -qc "CREATE ROLE netbox LOGIN PASSWORD '$NETBOX_DB_PASS'"
|
|
# UTF-8 whatever the container's locale (C in the Proxmox template)
|
|
sudo -u postgres psql -qc "CREATE DATABASE netbox OWNER netbox ENCODING 'UTF8' LC_COLLATE 'C.UTF-8' LC_CTYPE 'C.UTF-8' TEMPLATE template0"
|
|
fi
|
|
sed -i 's/^# *databases .*/databases 16/' /etc/valkey/valkey.conf || true
|
|
systemctl enable --now valkey-server
|
|
|
|
# ------------------------------------------------------------------ NetBox
|
|
id netbox >/dev/null 2>&1 || adduser --system --group --home /opt/netbox netbox
|
|
D=/opt/netbox-$NETBOX_VERSION
|
|
if [ ! -d $D ]; then
|
|
curl -fsSL https://github.com/netbox-community/netbox/archive/refs/tags/v$NETBOX_VERSION.tar.gz | tar -xz -C /opt
|
|
fi
|
|
[ -L /opt/netbox ] || { [ -d /opt/netbox ] && rmdir /opt/netbox 2>/dev/null || true; }
|
|
ln -sfn $D /opt/netbox
|
|
cat > /opt/netbox/netbox/netbox/configuration.py <<EOF
|
|
# pxestuff lab/netbox/install.sh generates this file
|
|
ALLOWED_HOSTS = ['$MYIP', 'localhost', '127.0.0.1']
|
|
DATABASES = {'default': {'NAME': 'netbox', 'USER': 'netbox', 'PASSWORD': '$NETBOX_DB_PASS',
|
|
'HOST': '127.0.0.1', 'PORT': '', 'CONN_MAX_AGE': 300}}
|
|
REDIS = {
|
|
'tasks': {'HOST': '127.0.0.1', 'PORT': 6379, 'USERNAME': '', 'PASSWORD': '', 'DATABASE': 0, 'SSL': False},
|
|
'caching': {'HOST': '127.0.0.1', 'PORT': 6379, 'USERNAME': '', 'PASSWORD': '', 'DATABASE': 1, 'SSL': False},
|
|
}
|
|
SECRET_KEY = '$NETBOX_SECRET_KEY'
|
|
API_TOKEN_PEPPERS = {1: '$NETBOX_PEPPER'}
|
|
TIME_ZONE = 'Europe/Stockholm'
|
|
CSRF_TRUSTED_ORIGINS = ['http://$MYIP']
|
|
EOF
|
|
chown -R netbox:netbox $D/netbox/media $D/netbox/reports $D/netbox/scripts 2>/dev/null || true
|
|
cd /opt/netbox && PYTHON=/usr/bin/python3 ./upgrade.sh >/var/log/netbox-upgrade.log 2>&1 || { tail -40 /var/log/netbox-upgrade.log; exit 1; }
|
|
chown -R netbox:netbox /opt/netbox/netbox/media
|
|
cp /opt/netbox/contrib/gunicorn.py /opt/netbox/gunicorn.py
|
|
sed -i "s/^bind = .*/bind = '127.0.0.1:8001'/" /opt/netbox/gunicorn.py
|
|
cp /opt/netbox/contrib/netbox.service /opt/netbox/contrib/netbox-rq.service /etc/systemd/system/
|
|
systemctl daemon-reload
|
|
systemctl enable --now netbox netbox-rq
|
|
systemctl restart netbox netbox-rq
|
|
|
|
cat > /etc/caddy/Caddyfile <<EOF
|
|
# pxestuff lab/netbox/install.sh
|
|
http://$MYIP, http://localhost {
|
|
handle_path /static/* {
|
|
root * /opt/netbox/netbox/static
|
|
file_server
|
|
}
|
|
reverse_proxy 127.0.0.1:8001
|
|
}
|
|
EOF
|
|
systemctl enable caddy && systemctl restart caddy
|
|
|
|
# ------------------------------------------------------------------ what Rollcall needs
|
|
# admin and rollcall users, the ownership tag and field, Rollcall's permissions
|
|
# (as altyapi hosts/netops/seed/rollcall-izinler.py on netops), the lab site
|
|
# and its prefixes delegated to Rollcall (tagged).
|
|
NB_ADMIN_PASS=$NETBOX_ADMIN_PASS NB_ROLLCALL_PASS=$ROLLCALL_PASS NB_SITE=$SITE NB_PREFIXES=$PREFIXES \
|
|
/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py shell -c '
|
|
import os
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from users.models import User, ObjectPermission
|
|
from extras.models import Tag, CustomField
|
|
from dcim.models import Site
|
|
from ipam.models import Prefix
|
|
|
|
def user(name, pw, su):
|
|
u, _ = User.objects.get_or_create(username=name)
|
|
u.set_password(pw); u.is_superuser = su; u.is_active = True; u.save()
|
|
return u
|
|
|
|
user("admin", os.environ["NB_ADMIN_PASS"], True)
|
|
rc = user("rollcall", os.environ["NB_ROLLCALL_PASS"], False)
|
|
tag, _ = Tag.objects.get_or_create(slug="rollcall", defaults={"name": "rollcall", "color": "2196f3"})
|
|
cf, _ = CustomField.objects.get_or_create(name="rollcall_machine", defaults={"type": "text", "label": "Rollcall machine"})
|
|
for m in ("ipam.ipaddress", "dcim.device"):
|
|
cf.object_types.add(ContentType.objects.get_by_natural_key(*m.split(".")))
|
|
perms = {
|
|
"rollcall-ipam-write": (["ipam.ipaddress"], ["view", "add", "change", "delete"]),
|
|
"rollcall-read": (["ipam.prefix", "ipam.vlan", "ipam.iprange", "extras.tag", "dcim.site"], ["view"]),
|
|
"rollcall-dcim-write": (["dcim.device", "dcim.interface", "dcim.macaddress"], ["view", "add", "change", "delete"]),
|
|
"rollcall-dcim-types": (["dcim.manufacturer", "dcim.devicetype", "dcim.devicerole"], ["view", "add"]),
|
|
"rollcall-dcim-read": (["dcim.location", "dcim.rack"], ["view"]),
|
|
}
|
|
for name, (types, actions) in perms.items():
|
|
p, _ = ObjectPermission.objects.get_or_create(name=name, defaults={"actions": actions})
|
|
p.actions = actions; p.enabled = True; p.save()
|
|
p.object_types.set([ContentType.objects.get_by_natural_key(*t.split(".")) for t in types])
|
|
p.users.add(rc)
|
|
site, _ = Site.objects.get_or_create(slug=os.environ["NB_SITE"], defaults={"name": os.environ["NB_SITE"].capitalize()})
|
|
for cidr in os.environ["NB_PREFIXES"].split():
|
|
p, _ = Prefix.objects.get_or_create(prefix=cidr, defaults={"description": "Rollcall lab", "scope": site})
|
|
p.tags.add(tag)
|
|
print("rollcall objects ready")
|
|
'
|
|
|
|
# Rollcall's API token (v2), made once through the API with its password
|
|
if ! grep -q '^NETBOX_TOKEN_ROLLCALL=' $S; then
|
|
curl -fsS http://127.0.0.1:8001/api/users/tokens/provision/ -H "Host: $MYIP" \
|
|
-H 'Content-Type: application/json' -H 'Accept: application/json' \
|
|
-d "{\"username\":\"rollcall\",\"password\":\"$ROLLCALL_PASS\",\"description\":\"rollcall region\",\"write_enabled\":true}" |
|
|
python3 -c 'import json,sys;d=json.load(sys.stdin);print("NETBOX_TOKEN_ROLLCALL=nbt_%s.%s"%(d["key"],d["token"]))' >> $S
|
|
fi
|
|
|
|
echo "--- ready"
|
|
systemctl is-active postgresql valkey-server netbox netbox-rq caddy | paste -sd' '
|
|
. $S
|
|
curl -fsS -o /dev/null -w "rollcall token: %{http_code}\n" -H "Authorization: Bearer $NETBOX_TOKEN_ROLLCALL" http://$MYIP/api/ipam/prefixes/
|