Files
phpipam-outline-sync/README.md
T
2026-07-24 08:39:52 +00:00

9.4 KiB
Raw Blame History

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

  1. Als Admin einloggen → Administration → API.
  2. Add API → App-ID vergeben (z.B. fastapi-sync, das ist der Wert für PHPIPAM_APP_ID).
  3. Bei App permissions mind. Read setzen (für diesen Sync reicht Lesezugriff).
  4. Bei App security gibt es zwei sinnvolle Optionen:
    • SSL with App code token oder SSL → erzeugt einen festen Token, der direkt als PHPIPAM_APP_TOKEN eingetragen wird (kein Login nötig).
    • User token → kein fester Token, stattdessen trägst du PHPIPAM_USERNAME / PHPIPAM_PASSWORD eines phpipam-Users mit API-Leserechten ein; die App holt sich den Token bei jedem Start selbst.
  5. Speichern.

Outline

  1. In Outline oben rechts auf den Avatar → Settings → API & Apps.

  2. New API key → Namen vergeben (z.B. "phpipam sync"), erstellen.

  3. Token kopieren → OUTLINE_API_TOKEN.

  4. Die Ziel-Collection öffnen, die ID aus der URL kopieren (.../collection/<id>-slug) → OUTLINE_COLLECTION_ID.

  5. 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ür collectionId/ parentDocumentId aber 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.info werden 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:
    1. Neuen Outline-User anlegen (z.B. phpipam-sync@firma.local), kein Workspace-Admin.
    2. 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.
    3. Mit diesem User einloggen und dort den API-Key erstellen (Schritt 13 oben), zusätzlich mit dem Endpoint-Scope aus Punkt 1.

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.

  1. 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: der app/-Ordner + requirements.txt (Dockerfile, docker-compose*.yml, README, .env werden dort nicht gebraucht).
  2. 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-sync kopieren.
    • Nur Portainer-UI: in docker-compose.portainer.yml den Bind-Mount-Block gegen den auskommentierten app_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).
  3. Stack in Portainer anlegen: Stacks → Add stack → Name vergeben (z.B. phpipam-outline-sync) → Build method Web editor → Inhalt von docker-compose.portainer.yml einfügen.
  4. 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ätzlich APP_DIR=/dein/pfad setzen.
  5. 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

  1. Nur phpipam-Verbindung prüfen (schreibt nichts):
    GET http://127.0.0.1:8000/subnets
    
  2. Trockenlauf (zeigt, was übertragen würde, schreibt noch nichts nach Outline):
    POST http://127.0.0.1:8000/sync?dry_run=true
    
  3. 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_CRON in der .env bzw. den Portainer-Umgebungsvariablen anpassen (Standard-Cron-Syntax: Minute Stunde Tag Monat Wochentag, z.B. */30 * * * * für alle 30 Minuten oder 0 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:
    GET http://127.0.0.1:8000/schedule
    
    liefert z.B. {"enabled": true, "cron": "0 * * * *", "next_run": "2026-07-24T11:00:00+02:00"}.
  • Geplante Läufe respektieren SYNC_DRY_RUN genauso wie manuelle Aufrufe ohne ?dry_run=-Parameter. Ergebnis/Fehler landen im Container-Log (docker compose logs -f bzw. 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.