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

214 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```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.