# 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/-slug`) → `OUTLINE_COLLECTION_ID`. 5. Optional: die Zielseite "Netze" öffnen, ID aus der URL kopieren (`.../doc/netze-`) → `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 1–3 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 ```bash 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) ```bash uvicorn app.main:app --reload ``` ### Mit Docker ```bash 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: ```bash 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 = `/`) 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. ## 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).