Dateien nach "/" hochladen
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# --- phpipam ---
|
||||||
|
PHPIPAM_BASE_URL=https://ipam.firma.local
|
||||||
|
PHPIPAM_APP_ID=fastapi-sync
|
||||||
|
|
||||||
|
# Variante A: statischer App-Token (empfohlen, App-Security-Type "Token" in phpipam)
|
||||||
|
PHPIPAM_APP_TOKEN=
|
||||||
|
|
||||||
|
# Variante B: alternativ Benutzername/Passwort (Token wird automatisch geholt)
|
||||||
|
PHPIPAM_USERNAME=
|
||||||
|
PHPIPAM_PASSWORD=
|
||||||
|
|
||||||
|
# Auf false setzen, falls phpipam ein selbstsigniertes Zertifikat nutzt (nur intern!)
|
||||||
|
PHPIPAM_VERIFY_SSL=true
|
||||||
|
|
||||||
|
# --- Outline ---
|
||||||
|
OUTLINE_BASE_URL=https://outline.firma.local
|
||||||
|
OUTLINE_API_TOKEN=
|
||||||
|
OUTLINE_COLLECTION_ID=
|
||||||
|
# Optional: ID der "Netze"-Seite. Wenn gesetzt, werden alle Subnetz-Dokumente
|
||||||
|
# als Unterseiten davon angelegt statt lose in der Collection.
|
||||||
|
OUTLINE_PARENT_DOCUMENT_ID=
|
||||||
|
OUTLINE_VERIFY_SSL=true
|
||||||
|
|
||||||
|
# --- Sync ---
|
||||||
|
SYNC_DRY_RUN=false
|
||||||
|
|
||||||
|
# Automatischer Zeitplan (Cron-Syntax: Minute Stunde Tag Monat Wochentag)
|
||||||
|
# Standard: jede volle Stunde. Auf false setzen, um nur manuell über POST /sync zu syncen.
|
||||||
|
SYNC_SCHEDULE_ENABLED=true
|
||||||
|
SYNC_SCHEDULE_CRON=0 * * * *
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# System-Zertifikate aktuell halten (wichtig falls phpipam/Outline
|
||||||
|
# selbstsignierte oder interne CA-Zertifikate nutzen)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app ./app
|
||||||
|
|
||||||
|
RUN useradd --create-home --uid 1000 appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)" || exit 1
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -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).
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
services:
|
||||||
|
phpipam-outline-sync:
|
||||||
|
image: python:3.12-slim
|
||||||
|
container_name: phpipam-outline-sync
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /code
|
||||||
|
command: sh -c "pip install --no-cache-dir -r requirements.txt && uvicorn app.main:app --host 0.0.0.0 --port 8000"
|
||||||
|
volumes:
|
||||||
|
# Hier landet der Projektordner (app/ + requirements.txt), den du im
|
||||||
|
# nächsten Schritt auf den Server überträgst. Pfad bei Bedarf anpassen
|
||||||
|
# oder als Portainer-Stack-Variable APP_DIR überschreiben.
|
||||||
|
- ${APP_DIR:-/opt/stacks/phpipam-outline-sync}:/code
|
||||||
|
# Cache, damit pip die Pakete nicht bei jedem Neustart neu herunterlädt
|
||||||
|
- pip_cache:/root/.cache/pip
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
PHPIPAM_BASE_URL: ${PHPIPAM_BASE_URL}
|
||||||
|
PHPIPAM_APP_ID: ${PHPIPAM_APP_ID}
|
||||||
|
PHPIPAM_APP_TOKEN: ${PHPIPAM_APP_TOKEN:-}
|
||||||
|
PHPIPAM_USERNAME: ${PHPIPAM_USERNAME:-}
|
||||||
|
PHPIPAM_PASSWORD: ${PHPIPAM_PASSWORD:-}
|
||||||
|
PHPIPAM_VERIFY_SSL: ${PHPIPAM_VERIFY_SSL:-true}
|
||||||
|
OUTLINE_BASE_URL: ${OUTLINE_BASE_URL}
|
||||||
|
OUTLINE_API_TOKEN: ${OUTLINE_API_TOKEN}
|
||||||
|
OUTLINE_COLLECTION_ID: ${OUTLINE_COLLECTION_ID}
|
||||||
|
OUTLINE_PARENT_DOCUMENT_ID: ${OUTLINE_PARENT_DOCUMENT_ID:-}
|
||||||
|
OUTLINE_VERIFY_SSL: ${OUTLINE_VERIFY_SSL:-true}
|
||||||
|
SYNC_DRY_RUN: ${SYNC_DRY_RUN:-false}
|
||||||
|
SYNC_SCHEDULE_ENABLED: ${SYNC_SCHEDULE_ENABLED:-true}
|
||||||
|
SYNC_SCHEDULE_CRON: ${SYNC_SCHEDULE_CRON:-0 * * * *}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 15s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# Alternative statt Bind-Mount oben, falls du keinen direkten
|
||||||
|
# Dateisystemzugriff auf den Docker-Host hast und Dateien stattdessen
|
||||||
|
# über Portainers Volume-Browser hochladen willst:
|
||||||
|
# volumes:
|
||||||
|
# - app_code:/code
|
||||||
|
# - pip_cache:/root/.cache/pip
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pip_cache:
|
||||||
|
# app_code: # nur nötig für die Named-Volume-Alternative oben
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
phpipam-outline-sync:
|
||||||
|
build: .
|
||||||
|
container_name: phpipam-outline-sync
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
# Falls phpipam/Outline selbst als Docker-Container laufen und du sie über
|
||||||
|
# ihren Container-Namen statt über eine öffentliche URL erreichen willst,
|
||||||
|
# dieses Netzwerk mit dem/den bestehenden Netzwerk(en) ersetzen und unten
|
||||||
|
# in "networks:" als "external: true" eintragen. Siehe README.
|
||||||
|
# networks:
|
||||||
|
# - infra
|
||||||
|
|
||||||
|
# networks:
|
||||||
|
# infra:
|
||||||
|
# external: true
|
||||||
|
# name: <name-des-bestehenden-netzwerks>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi>=0.111
|
||||||
|
uvicorn[standard]>=0.30
|
||||||
|
httpx>=0.27
|
||||||
|
pydantic-settings>=2.3
|
||||||
|
apscheduler>=3.10
|
||||||
Reference in New Issue
Block a user