9.7 KiB
phpipam-outline-sync
Kleine FastAPI-App: liest Subnetze + IP-Adressen aus phpipam und legt pro Subnetz ein Dokument in Outline an bzw. aktualisiert es.
1. API-Token besorgen
phpipam
- Als Admin einloggen → Administration → API.
- Add API → App-ID vergeben (z.B.
fastapi-sync, das ist der Wert fürPHPIPAM_APP_ID). - Bei App permissions mind.
Readsetzen (für diesen Sync reicht Lesezugriff). - Bei App security gibt es zwei sinnvolle Optionen:
- SSL with App code token oder SSL → erzeugt einen festen Token,
der direkt als
PHPIPAM_APP_TOKENeingetragen wird (kein Login nötig). - User token → kein fester Token, stattdessen trägst du
PHPIPAM_USERNAME/PHPIPAM_PASSWORDeines phpipam-Users mit API-Leserechten ein; die App holt sich den Token bei jedem Start selbst.
- SSL with App code token oder SSL → erzeugt einen festen Token,
der direkt als
- Speichern.
Outline
-
In Outline oben rechts auf den Avatar → Settings → API & Apps.
-
New API key → Namen vergeben (z.B. "phpipam sync"), erstellen.
-
Token kopieren →
OUTLINE_API_TOKEN. -
Die Ziel-Collection öffnen, die ID aus der URL kopieren (
.../collection/<id>-slug) →OUTLINE_COLLECTION_ID. -
Optional: die Zielseite "Netze" öffnen, ID aus der URL kopieren (
.../doc/netze-<id>) →OUTLINE_PARENT_DOCUMENT_ID. Ist das gesetzt, legt der Sync alle Subnetz-Dokumente als Unterseiten von "Netze" an statt lose in der Collection.Hinweis: die ID in der Browser-URL ist bei neueren Outline-Versionen nur eine gekürzte "Friendly URL"-ID, keine vollständige UUID. Die API (
documents.search/documents.create) verlangt fürcollectionId/parentDocumentIdaber eine echte UUID. Der Sync löst das selbst auf (collections.info/documents.info), du musst dich also nicht darum kümmern — einfach die ID aus der URL eintragen, egal ob kurz oder lang.
Schreibzugriff auf "IT → Netzwerke → Netze" beschränken
Outline kennt zwei unabhängige Einschränkungsebenen für API-Keys:
- Endpoint-Scope am Key selbst: beim Erstellen des API-Keys kannst du
unter "Scopes" eine Liste erlaubter Endpunkte eintragen, z.B.
collections.info documents.info documents.search documents.create documents.update. (collections.info/documents.infowerden gebraucht, damit der Sync die ID aus der URL in eine echte UUID auflösen kann — siehe Hinweis oben.) Das schränkt ein, welche Aktionen der Key ausführen darf (kein Löschen, keine User-/Collection-Verwaltung etc.) — aber nicht, wo (in welcher Collection/Seite) er schreiben darf. Das ist rein endpunktbasiert, nicht ortsbasiert. - Ort-Einschränkung über den Benutzer: ein API-Key erbt immer die Rechte
des Outline-Users, der ihn erstellt hat. Um den Zugriff auf "IT →
Netzwerke → Netze" zu begrenzen, brauchst du einen eigenen Service-User:
- Neuen Outline-User anlegen (z.B.
phpipam-sync@firma.local), kein Workspace-Admin. - Die "IT"-Collection (oder direkt die "Netze"-Seite, falls Outline bei euch Document-Sharing unterstützt) auf "Privat"/eingeschränkt stellen und nur diesen User mit "Can edit" hinzufügen. Überall sonst bekommt der User keinen Zugriff.
- Mit diesem User einloggen und dort den API-Key erstellen (Schritt 1–3 oben), zusätzlich mit dem Endpoint-Scope aus Punkt 1.
- Neuen Outline-User anlegen (z.B.
Kombiniert bekommst du: Key kann nur lesen/schreiben/suchen (kein Löschen,
keine Admin-Aktionen) UND kann inhaltlich nur auf das, worauf der
Service-User Zugriff hat. Über OUTLINE_PARENT_DOCUMENT_ID sorgt der Code
zusätzlich dafür, dass alle Dokumente tatsächlich unter "Netze" landen statt
irgendwo sonst in der Collection.
2. Setup
cd phpipam-outline-sync
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
.env mit den Werten aus Schritt 1 ausfüllen.
3. Starten
Lokal (ohne Docker)
uvicorn app.main:app --reload
Mit Docker
cp .env.example .env # falls noch nicht geschehen, dann ausfüllen
docker compose up -d --build
Baut das Image (Dockerfile, Python 3.12-slim, läuft als Non-Root-User) und
startet den Container mit den Werten aus .env. Neustart bei Absturz/Reboot
ist über restart: unless-stopped bereits konfiguriert; ein Healthcheck
prüft alle 30s /health.
Wenn phpipam/Outline selbst in Docker laufen und du sie über den
Container-Namen statt eine öffentliche URL erreichen willst (z.B.
PHPIPAM_BASE_URL=http://phpipam:80), muss der Sync-Container im selben
Docker-Netzwerk hängen:
docker network ls # Name des Netzwerks von phpipam/Outline finden
In docker-compose.yml den networks:-Block unten einkommentieren, den
echten Netzwerknamen eintragen und beim Service den networks:-Eintrag
ebenfalls einkommentieren. Danach zeigen PHPIPAM_BASE_URL /
OUTLINE_BASE_URL in der .env auf die Container-Namen statt auf externe
Hostnamen.
Ohne Docker-Netzwerk-Trick funktioniert es genauso über die normalen
(externen) URLs von phpipam/Outline — dann einfach docker compose up -d
ohne weitere Anpassung.
Als Portainer-Stack (Code per Volume, kein Image-Build)
Für diesen Fall gibt es docker-compose.portainer.yml: statt eines eigenen
Images wird das offizielle python:3.12-slim-Image verwendet, der Code
kommt per Bind-Mount in den Container, pip install läuft bei jedem Start
neu (dank Cache-Volume schnell). Änderungen an den Skripten = Dateien im
Volume ersetzen + Container neu starten, kein Rebuild nötig.
- Zielordner auf dem Docker-Server anlegen, z.B.
/opt/stacks/phpipam-outline-sync. Dort muss am Ende genau das liegen, was auch lokal im Projekt liegt: derapp/-Ordner +requirements.txt(Dockerfile, docker-compose*.yml, README, .env werden dort nicht gebraucht). - Dateien übertragen – je nachdem, was du für Zugriff auf den Server
hast:
- Direkter Host-Zugriff (SSH/SCP/WinSCP): Ordner einfach nach
/opt/stacks/phpipam-outline-synckopieren. - Nur Portainer-UI: in
docker-compose.portainer.ymlden Bind-Mount-Block gegen den auskommentiertenapp_code-Named-Volume- Block tauschen, Stack einmal starten (legt das leere Volume an), dann in Portainer unter Volumes → app_code → Browse die Dateien hochladen (Portainer ≥ 2.19 hat einen Datei-Browser für Volumes).
- Direkter Host-Zugriff (SSH/SCP/WinSCP): Ordner einfach nach
- Stack in Portainer anlegen: Stacks → Add stack → Name vergeben
(z.B.
phpipam-outline-sync) → Build method Web editor → Inhalt vondocker-compose.portainer.ymleinfügen. - Umgebungsvariablen setzen: im Abschnitt Environment variables
unten im Stack-Editor auf Advanced mode umschalten und den Inhalt von
.env.example(ausgefüllt, siehe Schritt 1 oben) reinpasten. Falls der Zielordner von Schritt 1 vom Default abweicht, zusätzlichAPP_DIR=/dein/pfadsetzen. - Deploy the stack klicken. Portainer zieht
python:3.12-slim, mountet den Ordner/das Volume, installiert die Abhängigkeiten und startet uvicorn. Logs dazu findest du im Container-Detail in Portainer.
Sobald das läuft und du zufrieden bist, kannst du optional auf das
Dockerfile + docker-compose.yml aus dem Abschnitt oben umsteigen (Code
fest im Image, schnellerer Start, kein pip install bei jedem Neustart) –
funktional identisch, nur ohne den Volume-Umweg.
Danach ist die App unter http://127.0.0.1:8000 erreichbar, interaktive
Doku unter http://127.0.0.1:8000/docs. Logs: docker compose logs -f.
Stoppen: docker compose down.
4. Testen
- Nur phpipam-Verbindung prüfen (schreibt nichts):
GET http://127.0.0.1:8000/subnets - Trockenlauf (zeigt, was übertragen würde, schreibt noch nichts nach
Outline):
POST http://127.0.0.1:8000/sync?dry_run=true - Echter Sync:
POST http://127.0.0.1:8000/sync
Ein Dokument wird pro Subnetz angelegt (Titel = <Netz>/<Maske> – <Beschreibung>)
mit einer Markdown-Tabelle aller belegten Adressen. Läuft der Sync erneut,
wird ein bereits vorhandenes Dokument mit gleichem Titel aktualisiert statt
dupliziert (Suche über documents.search).
5. Automatischer Zeitplan
Solange die App läuft (egal ob lokal, docker compose oder Portainer-Stack),
führt sie den Sync automatisch nach Zeitplan aus — Standard: jede volle
Stunde (SYNC_SCHEDULE_CRON=0 * * * *). Kein zusätzlicher Cron-Job o.ä.
nötig, das übernimmt die App selbst über einen internen Scheduler
(APScheduler).
- Zeitplan ändern:
SYNC_SCHEDULE_CRONin der.envbzw. den Portainer-Umgebungsvariablen anpassen (Standard-Cron-Syntax:Minute Stunde Tag Monat Wochentag, z.B.*/30 * * * *für alle 30 Minuten oder0 6,18 * * *für 6 und 18 Uhr). - Ganz deaktivieren (nur manueller Sync über
POST /sync):SYNC_SCHEDULE_ENABLED=false. - Nächsten Lauf prüfen:
liefert z.B.
GET http://127.0.0.1:8000/schedule{"enabled": true, "cron": "0 * * * *", "next_run": "2026-07-24T11:00:00+02:00"}. - Geplante Läufe respektieren
SYNC_DRY_RUNgenauso wie manuelle Aufrufe ohne?dry_run=-Parameter. Ergebnis/Fehler landen im Container-Log (docker compose logs -fbzw. Portainer-Container-Logs).
Wichtig: nur einen Container/eine Instanz dieser App gleichzeitig
laufen lassen (kein docker compose up --scale ...>1, keine mehreren
Uvicorn-Worker). Sonst würde der Sync pro Instanz einmal pro Stunde laufen
und Outline-Dokumente unnötig oft aktualisieren.
Hinweis
phpipam und Outline laufen in deinem lokalen Netz — ich kann den Sync von
hier aus nicht selbst live testen. Bitte einmal /subnets und danach
/sync?dry_run=true aufrufen und mir sagen, ob die Ausgabe passt (z.B. falls
Feldnamen in eurer phpipam-Version abweichen).