### Security-Fixes (K1–K6, W1–W4, W7, N1, N5–N6, V1, V5–V7)
- K1: CSRF-Schutz via Double-Submit-Cookie (httpapi/csrf.go + csrf_helpers.go)
- K2: requireScreenAccess() in allen manage-Handlern (Tenant-Isolation)
- K3: Tenant-Check bei DELETE /api/v1/media/{id}
- K4: requirePlaylistAccess() + GetByItemID() für JSON-API Playlist-Routen
- K5: Admin-Passwort nur noch als [gesetzt] geloggt
- K6: POST /api/v1/screens/register mit Pre-Shared-Secret (MORZ_INFOBOARD_REGISTER_SECRET)
- W1: Race Condition bei order_index behoben (atomare Subquery in AddItem)
- W2: Graceful Shutdown mit 15s Timeout auf SIGTERM/SIGINT
- W3: http.MaxBytesReader (512 MB) in allen Upload-Handlern
- W4: err.Error() nicht mehr an den Client
- W7: Template-Execution via bytes.Buffer (kein partial write bei Fehler)
- N1: Rate-Limiting auf /login (5 Versuche/Minute pro IP, httpapi/ratelimit.go)
- N5: Directory-Listing auf /uploads/ deaktiviert (neuteredFileSystem)
- N6: Uploads nach Tenant getrennt (uploads/{tenantSlug}/)
- V1: Upload-Logik konsolidiert in internal/fileutil/fileutil.go
- V5: Cookie-Name als Konstante reqcontext.SessionCookieName
- V6: Strukturiertes Logging mit log/slog + JSON-Handler
- V7: DB-Pool wird im Graceful-Shutdown geschlossen
### Phase 6: Screenshot-Erzeugung
- player/agent/internal/screenshot/screenshot.go erstellt
- Integration in app.go mit MORZ_INFOBOARD_SCREENSHOT_EVERY Config
### UX: PDF.js Integration
- pdf.min.js + pdf.worker.min.js als lokale Assets eingebettet
- Automatisches Seitendurchblättern im Player
### Ansible: Neue Rollen
- signage_base, signage_server, signage_provision erstellt
- inventory.yml und site.yml erweitert
### Konzept-Docs
- GRUPPEN-KONZEPT.md, KAMPAGNEN-AKTIVIERUNG.md, MONITORING-KONZEPT.md
- PROVISION-KONZEPT.md, TEMPLATE-EDITOR.md, WATCHDOG-KONZEPT.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
610 lines
20 KiB
Markdown
610 lines
20 KiB
Markdown
# Info-Board Neu - Jobrunner-Konzept fuer Ansible-gestuetzte Erstinstallation
|
|
|
|
## Ziel
|
|
|
|
Der Jobrunner fuehrt aus dem Admin-Backend heraus Provisionierungsjobs aus, die ein neues Display technisch in Betrieb nehmen.
|
|
|
|
Dieses Dokument beschreibt:
|
|
|
|
- wie ein Admin einen neuen Screen aus dem Web-UI provisioniert
|
|
- wie der Server Ansible-Playbooeke orchestriert
|
|
- wie der Fortschritt angezeigt wird
|
|
- Sicherheits- und Fehlerbehandlung
|
|
|
|
Grundlagen zur Provisionierungs-Strategie finden sich in `docs/PROVISIONIERUNGSKONZEPT.md`.
|
|
|
|
## 1. Provisionierungs-Workflow im Admin-UI
|
|
|
|
### Seite: Admin → Screens → Neu
|
|
|
|
```
|
|
┌──────────────────────────────────────────┐
|
|
│ Neuen Screen provisionieren │
|
|
├──────────────────────────────────────────┤
|
|
│ │
|
|
│ Schritt 1 — Grunddaten │
|
|
│ │
|
|
│ Screen-ID / Slug * │
|
|
│ [ info10 ] │
|
|
│ (muss eindeutig sein, alphanumerisch) │
|
|
│ │
|
|
│ Anzeigename * │
|
|
│ [ Infowand Bottom-Left ________________ ] │
|
|
│ │
|
|
│ Beschreibung │
|
|
│ [ Neue Infowand Display, pos. 7______ ] │
|
|
│ │
|
|
│ Device Type * │
|
|
│ ⦿ Raspberry Pi 4 │
|
|
│ ○ Raspberry Pi 5 │
|
|
│ ○ x86 Linux Kiosk │
|
|
│ │
|
|
│ Aufloesung * │
|
|
│ [1920 x 1080 ] Standard fuer RPi │
|
|
│ │
|
|
│ Orientierung * │
|
|
│ ⦿ portrait (hochkant) │
|
|
│ ○ landscape (quer) │
|
|
│ │
|
|
│ Tenant-Zuordnung │
|
|
│ [ Dropdown: alle Tenants + "admin" ] │
|
|
│ │
|
|
│ [Weiter >] [Abbrechen] │
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
### Schritt 2 — Netzwerk- und SSH-Einstellung
|
|
|
|
```
|
|
┌──────────────────────────────────────────┐
|
|
│ Schritt 2 — Zugang zur Hardware │
|
|
│ │
|
|
│ Ziel-IP-Adresse * │
|
|
│ [ 192.168.1.50 ] │
|
|
│ │
|
|
│ SSH-Port │
|
|
│ [ 22 ] Standard │
|
|
│ │
|
|
│ Bootstrap-Benutzer * │
|
|
│ ⦿ root │
|
|
│ ○ pi │
|
|
│ ○ custom: [ ________________ ] │
|
|
│ │
|
|
│ Bootstrap-Authentifizierung * │
|
|
│ ⦿ Passwort (initial, wird durch Key │
|
|
│ ersetzt): │
|
|
│ [ Passwort ____________ ] │
|
|
│ ○ SSH-Key (nur wenn vorvorhanden): │
|
|
│ [ Datei auswaehlen ] oder │
|
|
│ [ PEM-Key einfuegen ] │
|
|
│ │
|
|
│ Test-Verbindung │
|
|
│ [SSH Test] [PING Test] │
|
|
│ │
|
|
│ [Weiter >] [Zurueck] [Abbrechen] │
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
### Schritt 3 — Konfiguration und Optionen
|
|
|
|
```
|
|
┌──────────────────────────────────────────┐
|
|
│ Schritt 3 — Konfiguration │
|
|
│ │
|
|
│ Fallback-Verzeichnis (lokal auf Player) │
|
|
│ [ /var/lib/signage/fallback ] │
|
|
│ │
|
|
│ Snapshot-Intervall (Sekunden) │
|
|
│ [ 300 ] 0 = deaktiviert │
|
|
│ │
|
|
│ MQTT-Broker-Adresse (Zielserver) │
|
|
│ [ mqtt.example.com ] auto-gefuellt │
|
|
│ │
|
|
│ Server-API-Adresse │
|
|
│ [ https://signage.example.com/api ] │
|
|
│ auto-gefuellt │
|
|
│ │
|
|
│ Gruppen-Zuordnung (optional) │
|
|
│ [ Checkboxen: wall-all, wall-row-1 ] │
|
|
│ │
|
|
│ Tags / Labels (optional) │
|
|
│ [ mainfloor, hightrafficarea ] │
|
|
│ │
|
|
│ [Weiter >] [Zurueck] [Abbrechen] │
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
### Schritt 4 — Review und Start
|
|
|
|
```
|
|
┌──────────────────────────────────────────┐
|
|
│ Schritt 4 — Uebersicht & Start │
|
|
│ │
|
|
│ Zusammenfassung: │
|
|
│ │
|
|
│ Screen: info10 │
|
|
│ Name: Infowand Bottom-Left │
|
|
│ Typ: Raspberry Pi 4 │
|
|
│ IP: 192.168.1.50 │
|
|
│ Aufloesung: 1920 x 1080 │
|
|
│ Orientierung: portrait │
|
|
│ Tenant: admin │
|
|
│ │
|
|
│ SSH-Verbindung wird hergestellt... │
|
|
│ [✓] SSH-Zugang verifiziert │
|
|
│ [✓] Pfadberechtigungen ok │
|
|
│ [✓] Speicherplatz ausreichend (15GB) │
|
|
│ │
|
|
│ Provisioning-Playbook: │
|
|
│ [ ] site.yml │
|
|
│ ├─ signage_base (Packages, Kernel) │
|
|
│ ├─ signage_display (X11, Chromium) │
|
|
│ ├─ signage_player (Agent, Config) │
|
|
│ └─ signage_provision (Setup-Jobs) │
|
|
│ │
|
|
│ Warnung: │
|
|
│ ! Diesen Prozess kann nicht unterbrochen│
|
|
│ werden. Typische Dauer: 10-15 Min. │
|
|
│ │
|
|
│ [Provisioning starten] [Abbrechen] │
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
## 2. Provisioning-Job: Serverseitige Orchestrierung
|
|
|
|
### Architektur
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Admin-UI HTTP Request │
|
|
│ POST /api/v1/admin/provision │
|
|
└────────────┬────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────┐
|
|
│ Backend API (Go) │
|
|
│ - validiert Eingaben │
|
|
│ - erstellt ProvisioningJob in DB │
|
|
│ - queued Job in Job-Broker (Redis etc) │
|
|
└────────────┬────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────┐
|
|
│ Jobrunner Worker (Goroutine oder │
|
|
│ separater Go-Service) │
|
|
│ - laeuft im Server-Container │
|
|
│ - zeigt Fortschritt via Websocket │
|
|
└────────────┬────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────┐
|
|
│ Ansible Executor │
|
|
│ ansible-playbook site.yml │
|
|
│ -i inventory.ini │
|
|
│ -e vars.yml │
|
|
└────────────┬────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────┐
|
|
│ Target Device (Raspberry Pi) │
|
|
│ SSH: root@192.168.1.50 │
|
|
│ - installiert Packages │
|
|
│ - startet Services │
|
|
│ - synchonisiert Config │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Provisioning-Job-Modell
|
|
|
|
```sql
|
|
CREATE TABLE provisioning_jobs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
screen_id UUID NOT NULL REFERENCES screens(id),
|
|
status TEXT NOT NULL CHECK (status IN (
|
|
'pending', 'running', 'completed', 'failed'
|
|
)),
|
|
started_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
|
|
-- SSH/Ansible-Details
|
|
target_ip TEXT NOT NULL,
|
|
target_port INT NOT NULL DEFAULT 22,
|
|
target_user TEXT NOT NULL,
|
|
|
|
-- Verbrauch von Ressourcen
|
|
ansible_job_id TEXT, -- Job-ID aus Ansible-Executor
|
|
|
|
-- Fehlerbehandlung
|
|
error_log TEXT, -- bei failure
|
|
|
|
created_by_user_id TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
### Provisioning-Log-Modell
|
|
|
|
```sql
|
|
CREATE TABLE provisioning_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
job_id UUID NOT NULL REFERENCES provisioning_jobs(id) ON DELETE CASCADE,
|
|
line_number INT NOT NULL,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Quelle des Logs
|
|
source TEXT NOT NULL CHECK (source IN ('ansible', 'agent', 'system')),
|
|
level TEXT NOT NULL CHECK (level IN ('info', 'warn', 'error')),
|
|
|
|
-- Nachricht
|
|
message TEXT NOT NULL,
|
|
|
|
UNIQUE(job_id, line_number)
|
|
);
|
|
```
|
|
|
|
## 3. Jobrunner-Implementierung
|
|
|
|
### Job-Verarbeitung (Pseudocode)
|
|
|
|
```go
|
|
type ProvisioningJobRunner struct {
|
|
db *sql.DB
|
|
ansibleBinPath string
|
|
logChannel chan ProvisioningLogMessage
|
|
}
|
|
|
|
func (r *ProvisioningJobRunner) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
|
|
// 1. Lade Job aus DB
|
|
job := r.db.GetProvisioningJob(jobID)
|
|
|
|
// 2. Setze Status auf "running"
|
|
r.db.UpdateProvisioningJob(job.ID, map[string]interface{}{
|
|
"status": "running",
|
|
"started_at": time.Now(),
|
|
})
|
|
|
|
// 3. Generiere Ansible-Inventar
|
|
inventory := r.generateInventory(job)
|
|
// [192.168.1.50]
|
|
// ansible_user=root
|
|
// ansible_password=***
|
|
// screen_id=info10
|
|
// ansible_become=yes
|
|
|
|
// 4. Generiere vars.yml
|
|
vars := r.generateVars(job)
|
|
// screen_id: info10
|
|
// display_name: "Infowand Bottom-Left"
|
|
// orientation: portrait
|
|
// mqtt_broker: mqtt.example.com
|
|
// etc.
|
|
|
|
// 5. Fuehre Ansible aus
|
|
cmd := exec.CommandContext(ctx,
|
|
r.ansibleBinPath,
|
|
"site.yml",
|
|
"-i", inventoryPath,
|
|
"-e", varsPath,
|
|
"-v", // verbose
|
|
)
|
|
|
|
// 6. Piping: Ansible-Ausgabe → Log-Dateien + Websocket
|
|
stdout, _ := cmd.StdoutPipe()
|
|
stderr, _ := cmd.StderrPipe()
|
|
|
|
go r.streamLogs(job.ID, stdout, "ansible")
|
|
go r.streamLogs(job.ID, stderr, "ansible")
|
|
|
|
// 7. Warte auf Completion
|
|
err := cmd.Run()
|
|
|
|
// 8. Aktualisiere Job-Status
|
|
if err != nil {
|
|
r.db.UpdateProvisioningJob(job.ID, map[string]interface{}{
|
|
"status": "failed",
|
|
"completed_at": time.Now(),
|
|
"error_log": err.Error(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.db.UpdateProvisioningJob(job.ID, map[string]interface{}{
|
|
"status": "completed",
|
|
"completed_at": time.Now(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *ProvisioningJobRunner) streamLogs(jobID uuid.UUID, reader io.Reader, source string) {
|
|
scanner := bufio.NewScanner(reader)
|
|
lineNum := 1
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// Persistiere in DB
|
|
r.db.InsertProvisioningLog(ProvisioningLog{
|
|
JobID: jobID,
|
|
LineNumber: lineNum,
|
|
Source: source,
|
|
Level: parseLogLevel(line), // heuristic
|
|
Message: line,
|
|
})
|
|
|
|
// Schreibe ins Websocket (siehe Abschnitt "Fortschritt")
|
|
r.logChannel <- ProvisioningLogMessage{
|
|
JobID: jobID,
|
|
Line: line,
|
|
}
|
|
|
|
lineNum++
|
|
}
|
|
}
|
|
```
|
|
|
|
### Ansible-Ausfuehrung mit Jumphost (optional)
|
|
|
|
Falls der Server nicht direkt die Zielgeraete erreicht, kann ein Jumphost verwendet werden:
|
|
|
|
```yaml
|
|
# ansible.cfg
|
|
[defaults]
|
|
inventory = inventory.ini
|
|
host_key_checking = False
|
|
retries = 3
|
|
|
|
[privilege_escalation]
|
|
become = True
|
|
become_method = sudo
|
|
```
|
|
|
|
```ini
|
|
# inventory.ini fuer Jumphost-Szenario
|
|
[targets]
|
|
192.168.1.50 ansible_user=root ansible_password=*** \
|
|
ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p jumphost@example.com"'
|
|
```
|
|
|
|
## 4. Fortschritt und Live-Updates
|
|
|
|
### Websocket-Kanal fuer Echtzeit-Logs
|
|
|
|
**HTTP-Upgrade zu Websocket:**
|
|
|
|
```
|
|
GET /api/v1/admin/provision/{jobID}/logs
|
|
Upgrade: websocket
|
|
Connection: Upgrade
|
|
```
|
|
|
|
**Server sendet kontinuierlich:**
|
|
|
|
```json
|
|
{
|
|
"type": "log_line",
|
|
"timestamp": "2025-03-25T14:22:00Z",
|
|
"line": "TASK [signage_base : Update package cache] **",
|
|
"source": "ansible",
|
|
"level": "info"
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "progress",
|
|
"timestamp": "2025-03-25T14:22:15Z",
|
|
"current_task": "signage_base : Update package cache",
|
|
"task_number": 3,
|
|
"total_tasks": 12,
|
|
"percent": 25
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"type": "status_change",
|
|
"timestamp": "2025-03-25T14:35:00Z",
|
|
"status": "completed",
|
|
"duration_seconds": 780
|
|
}
|
|
```
|
|
|
|
### UI-Anzeige waehrend Provisioning
|
|
|
|
```
|
|
┌──────────────────────────────────────────┐
|
|
│ Provisioning laeuft: info10 │
|
|
│ Gestartet: vor 5 Min. │
|
|
│ Geschaetzte verbleibende Zeit: 8 Min. │
|
|
├──────────────────────────────────────────┤
|
|
│ │
|
|
│ [████████████░░░░░░░░░░░░░░] 33% │
|
|
│ │
|
|
│ Aktuelle Aufgabe: │
|
|
│ ⊙ signage_base : Update package cache │
|
|
│ │
|
|
│ Letzte Logs: │
|
|
│ ├─ [14:22:00] TASK [signage_base ...] │
|
|
│ ├─ [14:22:05] ok: [192.168.1.50] │
|
|
│ ├─ [14:22:10] TASK [signage_display] │
|
|
│ ├─ [14:22:15] Chromium wird installiert│
|
|
│ └─ [14:22:20] ... │
|
|
│ │
|
|
│ [Auto-Refresh] [Pause] [Abbrechen] │
|
|
│ (Abbrechen: SSH-Verbindung wird nicht │
|
|
│ sofort getrennt, aber Job gestoppt) │
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
## 5. Fehlerbehandlung und Recovery
|
|
|
|
### Fehlerszenarien
|
|
|
|
| Fehler | Grund | Recovery |
|
|
|---|---|---|
|
|
| SSH-Verbindung fehlgeschlagen | IP falsch, Passwort falsch, Firewall | Logs zeigen SSH-Error, Admin kann Credentials korrigieren und neu starten |
|
|
| Ansible-Playbook fehlgeschlagen | Paket-Versionskonflikt, Platz voll | Logs zeigen welcher Task fehlgeschlagen, Admin kann manuell SSH-en oder Job wiederholen |
|
|
| Timeout nach 30 Min. | Sehr langsame Netzwerk oder Device haengt | Job wird abgebrochen, Admin kann Verbindung checken und neu starten |
|
|
| Package-Download fehlgeschlagen | Mirror offline, Netzwerk unterbrochen | Ansible retry automatisch 3x, Logs zeigen wget-Error |
|
|
|
|
### Retry-Logik
|
|
|
|
```
|
|
Strategie: Exponentieller Backoff fuer Playbook-Fehler
|
|
Fehler 1: Sofort wiederholen
|
|
Fehler 2: Warte 5s, wiederhole
|
|
Fehler 3: Warte 15s, wiederhole
|
|
Fehler 4+: Gib auf, zeige Fehler
|
|
```
|
|
|
|
### Admin-Recovery
|
|
|
|
Falls ein Job fehlgeschlagen ist:
|
|
|
|
```
|
|
┌──────────────────────────────────────────┐
|
|
│ Provisioning fehlgeschlagen: info10 │
|
|
│ │
|
|
│ Fehler: │
|
|
│ ssh: Could not resolve hostname │
|
|
│ (DNS-Fehler oder Geraet nicht erreichbar)│
|
|
│ │
|
|
│ Empfehlung: │
|
|
│ 1. IP-Adresse pruefen │
|
|
│ 2. Geraet von Hand SSH-en und testen │
|
|
│ 3. Job neu starten: [Neuer Versuch] │
|
|
│ │
|
|
│ Komplette Logs herunterladen: │
|
|
│ [logs-info10-20250325.txt] │
|
|
│ │
|
|
│ [Neuer Versuch] [Logs zeigen] [Zurueck]│
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
## 6. Sicherheitsaspekte
|
|
|
|
### SSH-Key-Verwaltung
|
|
|
|
**Phase 1 — Bootstrap mit Passwort:**
|
|
|
|
```
|
|
Admin gibt Passwort ein
|
|
↓
|
|
Server speichert Passwort NICHT
|
|
↓
|
|
Server uebergibt an Ansible nur waehrend dieser Session
|
|
↓
|
|
Ansible loggt sich ein, generiert SSH-Key
|
|
↓
|
|
SSH-Key wird auf dem Geraet als authorized_key eingetragen
|
|
↓
|
|
Passwort wird auf dem Geraet gelöscht oder deaktiviert
|
|
```
|
|
|
|
**Phase 2 — Dauerhaft mit SSH-Key:**
|
|
|
|
```
|
|
Server speichert SSH-Key in Secrets-Backend (z.B. HashiCorp Vault)
|
|
Zukuenftige Ansible-Lauefe verwenden den Key
|
|
```
|
|
|
|
### Ansible-Vault fuer sensitive Daten
|
|
|
|
```yaml
|
|
# roles/signage_player/defaults/main.yml
|
|
server_api_key: !vault |
|
|
$ANSIBLE_VAULT;1.1;AES256
|
|
abcd1234...
|
|
```
|
|
|
|
Die Vault-Passphrase wird:
|
|
|
|
- nie im Klartext gelagert
|
|
- vom Server nur zur Laufzeit an Ansible uebergeben
|
|
- in Logs nicht ausgegeben
|
|
|
|
### Sudo ohne Passwort
|
|
|
|
Ansible erhoeht die Rechte per `sudo` ohne Passwort-Eingabe:
|
|
|
|
```sudoers
|
|
# /etc/sudoers.d/ansible-signage
|
|
ansible ALL=(ALL) NOPASSWD: ALL
|
|
```
|
|
|
|
(Alternativ: mit Passwort, das Ansible am Anfang einmal abfragt)
|
|
|
|
## 7. Verbindung zum bestehenden System
|
|
|
|
### Provisioning-Trigger aus Admin-UI
|
|
|
|
```
|
|
Admin-Seite: Screens → "+ Neuer Screen"
|
|
↓
|
|
Formular sammelt Grunddaten
|
|
↓
|
|
POST /api/v1/admin/provision
|
|
↓
|
|
Backend:
|
|
1. Screen in `screens` Tabelle eintragen
|
|
2. ProvisioningJob in `provisioning_jobs` anlegen
|
|
3. Job in Broker queuen
|
|
↓
|
|
Jobrunner:
|
|
1. Holt Job aus Broker
|
|
2. Startet Ansible
|
|
3. Streamt Logs via Websocket
|
|
4. Aktualisiert Job-Status bei Completion
|
|
↓
|
|
Admin sieht Live-Updates im UI
|
|
```
|
|
|
|
### Nach erfolgreichem Provisioning
|
|
|
|
```
|
|
Job-Status: "completed"
|
|
↓
|
|
Agent auf dem Display startet
|
|
↓
|
|
Agent registriert sich beim Server
|
|
↓
|
|
Server setzt Screen-Status auf "online"
|
|
↓
|
|
Admin sieht Screen in Tabelle mit Status "online"
|
|
↓
|
|
Admin kann sofort Kampagnen/Playlists zuweisen
|
|
```
|
|
|
|
## 8. Konfigurierbare Parameter
|
|
|
|
In `/etc/signage/provision.yml`:
|
|
|
|
```yaml
|
|
jobrunner:
|
|
max_concurrent_jobs: 3
|
|
ansible_timeout_sec: 1800
|
|
playbook_path: "/srv/ansible/site.yml"
|
|
inventory_template_path: "/srv/ansible/inventory.ini.tpl"
|
|
vars_template_path: "/srv/ansible/vars.yml.tpl"
|
|
|
|
ssh:
|
|
known_hosts_file: "/etc/signage/.ssh/known_hosts"
|
|
key_storage: "vault" # oder "filesystem"
|
|
|
|
ansible:
|
|
verbosity: "-vv" # oder "-v", "-vvv"
|
|
extra_args: ""
|
|
```
|
|
|
|
## 9. Zusammenfassung
|
|
|
|
Der Jobrunner:
|
|
|
|
- **ist web-gesteuert** — Provisioning-UI mit Multi-Step-Wizard
|
|
- **ist automatisiert** — Ansible Playbooks, nicht manuelle SSH-Kommandos
|
|
- **ist transparent** — Live-Logs und Fortschritt-Anzeige
|
|
- **ist sicher** — SSH-Keys, Ansible-Vault, keine Plaintext-Credentials in Logs
|
|
- **ist resilient** — Retry-Logik und Error-Recovery
|
|
- **ist erweiterbar** — neue Rollen und Tasks koennen hinzugefuegt werden ohne UI-Aenderung
|