@@ -0,0 +1,220 @@
# 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 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 = ` <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.
## 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).