Compare commits
15 Commits
main
...
feature/UI
| Author | SHA1 | Date | |
|---|---|---|---|
| 6523fe1131 | |||
| 2efe1bb6d3 | |||
| b62d455d77 | |||
| 83e20cd365 | |||
| 191666736b | |||
| 2637715e48 | |||
| bdd79a66cb | |||
| 3eac9a659e | |||
| d1bafa28d9 | |||
| 28be78dceb | |||
| f68db3ab1e | |||
| 905ce67742 | |||
| 8cd07a73a0 | |||
| 77ac0577e2 | |||
| 7f7cab75e5 |
297
CHANGELOG.md
297
CHANGELOG.md
@ -1,297 +0,0 @@
|
|||||||
# CHANGELOG
|
|
||||||
|
|
||||||
## 2025-12-03 — SNMP Live-Dashboard (Architektur: Service + API)
|
|
||||||
|
|
||||||
### Übersicht
|
|
||||||
|
|
||||||
Implementierung eines **dualen SNMP-Status-Systems**:
|
|
||||||
1. **Server-seitig (Initial Load):** `DashboardController` → `SnmpServerStatusService`
|
|
||||||
2. **Client-seitig (Live-Updates):** Browser-JavaScript → API-Endpunkt (`snmp_status.php`)
|
|
||||||
|
|
||||||
Ergebnis: **Beste User Experience** (sofortige Daten beim Laden) + **Redundanz** (Live-Polling läuft weiter, auch wenn Service fehlt).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Komponente 1: Service (`app/Services/Snmp/SnmpServerStatusService.php`) — modernisiert
|
|
||||||
|
|
||||||
### Was wurde geändert?
|
|
||||||
|
|
||||||
Die alte Version ist zu streng gewesen (wirft Exception wenn "Physical Memory" nicht exakt gefunden wird).
|
|
||||||
Neue Version hat **intelligente Fallback-Logik**:
|
|
||||||
|
|
||||||
**RAM-Erkennung (in dieser Reihenfolge):**
|
|
||||||
1. Heuristik: Suche nach "Physical Memory" (exakt)
|
|
||||||
2. Fallback 1: Suche nach "physical" ODER "memory" ODER "ram" (Case-insensitive)
|
|
||||||
3. Fallback 2: Nimm den **kleinsten Storage-Eintrag** (wahrscheinlich RAM)
|
|
||||||
4. Falls alles fehlschlägt: Exception → abgefangen im Controller → 0% angezeigt
|
|
||||||
|
|
||||||
**Disk-Erkennung (in dieser Reihenfolge):**
|
|
||||||
1. Heuristik: Suche nach "C:\\" (Windows, exakt)
|
|
||||||
2. Fallback 1: Suche nach "C:" ODER "root" ODER "/" (Case-insensitive)
|
|
||||||
3. Fallback 2: Nimm den **größten Storage-Eintrag > 1 GB** (wahrscheinlich Hauptlaufwerk)
|
|
||||||
4. Falls alles fehlschlägt: Exception → abgefangen im Controller → 0% angezeigt
|
|
||||||
|
|
||||||
### Featureset
|
|
||||||
|
|
||||||
- ✅ Robuste Fehlerbehandlung (aussagekräftige `RuntimeException` für Debugging)
|
|
||||||
- ✅ Intelligente Fallbacks bei unerwarteten OID-Beschreibungen
|
|
||||||
- ✅ Unterstützung Windows + Linux
|
|
||||||
- ✅ Prüfung auf SNMP-Erweiterung und Konfiguration
|
|
||||||
- ✅ Uptime in lesbares Format konvertieren (z. B. "1 Tage, 10:17:36")
|
|
||||||
- ✅ CPU-Durchschnitt über alle Kerne
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Komponente 2: Controller (`app/Controllers/DashboardController.php`) — neu strukturiert
|
|
||||||
|
|
||||||
### Was wurde geändert?
|
|
||||||
|
|
||||||
Der Controller hatte nur eine Zeile geändert; jetzt **fehlertolerante Abfrage**:
|
|
||||||
|
|
||||||
```php
|
|
||||||
try {
|
|
||||||
$serverStatus = $this->snmpService->getServerStatus();
|
|
||||||
} catch (\RuntimeException $e) {
|
|
||||||
error_log('SNMP-Fehler beim initialen Laden - ' . $e->getMessage());
|
|
||||||
// Fallback-Werte werden verwendet (siehe oben)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Effekt:**
|
|
||||||
- Wenn Service fehlschlägt: Dashboard wird trotzdem geladen (mit 0% oder 'n/a')
|
|
||||||
- Fehler wird geloggt (PHP Error-Log)
|
|
||||||
- Live-Polling (API) läuft trotzdem weiter und kann Daten liefern
|
|
||||||
- Bessere User Experience statt "Error 500"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Komponente 3: API (`public/api/snmp_status.php`)
|
|
||||||
|
|
||||||
Siehe vorherige CHANGELOG-Einträge. Wichtig:
|
|
||||||
- **Identische Fallback-Logik** wie der Service
|
|
||||||
- **Session-Validierung** (nur Admins)
|
|
||||||
- **Caching** (10 Sekunden)
|
|
||||||
- **Detailliertes Logging** in `public/logs/snmp_api.log`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Komponente 4: View & JavaScript (`public/views/dashboard.php`)
|
|
||||||
|
|
||||||
Siehe vorherige CHANGELOG-Einträge.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architektur: Dual-Layer System
|
|
||||||
|
|
||||||
### Layer 1: Initial Load (Server-seitig)
|
|
||||||
|
|
||||||
```
|
|
||||||
Nutzer öffnet Dashboard
|
|
||||||
↓
|
|
||||||
DashboardController::show()
|
|
||||||
↓
|
|
||||||
SnmpServerStatusService::getServerStatus()
|
|
||||||
↓
|
|
||||||
SNMP-Abfrage (mit Fallbacks)
|
|
||||||
↓
|
|
||||||
Daten sofort in View angezeigt
|
|
||||||
↓
|
|
||||||
(Bei Fehler: Fallback-Werte wie 0%, 'n/a')
|
|
||||||
↓
|
|
||||||
Logging in PHP Error-Log
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vorteile:**
|
|
||||||
- ✅ Daten sind **sofort** sichtbar (gute UX)
|
|
||||||
- ✅ Fallbacks verhindern "Error 500"
|
|
||||||
- ✅ Service wird nur 1x pro Seitenladung aufgerufen (sparsam)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Layer 2: Live-Updates (Client-seitig)
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser lädt Dashboard
|
|
||||||
↓
|
|
||||||
JavaScript addEventListener('DOMContentLoaded')
|
|
||||||
↓
|
|
||||||
Sofort erste Abfrage: fetch('api/snmp_status.php')
|
|
||||||
↓
|
|
||||||
Antwort: JSON mit aktuellen Daten
|
|
||||||
↓
|
|
||||||
updateUI() aktualisiert die Karten
|
|
||||||
↓
|
|
||||||
Alle 5 Sekunden wiederholt (setInterval)
|
|
||||||
↓
|
|
||||||
Logging in public/logs/snmp_api.log (Server-seitig)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vorteile:**
|
|
||||||
- ✅ **Live-Updates** ohne Seite zu reload-en
|
|
||||||
- ✅ **Session-Schutz** (nur Admins können Endpunkt aufrufen)
|
|
||||||
- ✅ **Caching** reduziert SNMP-Last (10s TTL)
|
|
||||||
- ✅ **Fallback-Logik** im API unabhängig vom Service
|
|
||||||
- ✅ **Redundanz:** Wenn Service fehlt, läuft API trotzdem
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
### PHP Error-Log (Service-Fehler)
|
|
||||||
|
|
||||||
- **Ort:** Abhängig von PHP-Konfiguration (meist `/var/log/php-errors.log` oder Windows Event-Log)
|
|
||||||
- **Format:** Standard PHP Error-Log
|
|
||||||
- **Inhalt:** SNMP-Fehler beim initialen Laden (z. B. "SNMP-Konfiguration ist unvollständig")
|
|
||||||
- **Trigger:** Nur wenn Service-Abfrage fehlschlägt
|
|
||||||
|
|
||||||
**Beispiel:**
|
|
||||||
```
|
|
||||||
[03-Dec-2025 12:05:00 UTC] DashboardController: SNMP-Fehler beim initialen Laden - SNMP-Konfiguration ist unvollständig (host fehlt).
|
|
||||||
```
|
|
||||||
|
|
||||||
### SNMP API Log (`public/logs/snmp_api.log`)
|
|
||||||
|
|
||||||
- **Ort:** `public/logs/snmp_api.log` (wird automatisch angelegt)
|
|
||||||
- **Format:** `[YYYY-MM-DD HH:MM:SS] Nachricht`
|
|
||||||
- **Inhalt:**
|
|
||||||
- Cache-Hits/Misses
|
|
||||||
- SNMP-Konfiguration
|
|
||||||
- Alle Storage-Einträge
|
|
||||||
- Erkannte Disk/RAM mit Prozentsätzen
|
|
||||||
- Fallback-Aktionen
|
|
||||||
- Finale Werte
|
|
||||||
- Fehler
|
|
||||||
|
|
||||||
**Beispiel:**
|
|
||||||
```
|
|
||||||
[2025-12-03 12:05:00] --- SNMP-Abfrage gestartet ---
|
|
||||||
[2025-12-03 12:05:00] SNMP-Host: 127.0.0.1, Community: public_ro, Timeout: 2s
|
|
||||||
[2025-12-03 12:05:00] Uptime OID: 1.3.6.1.2.1.1.3.0, Raw: "Timeticks: (1234567) 14 days, 6:14:27.67"
|
|
||||||
[2025-12-03 12:05:00] Storage[1]: Desc='Physical Memory', Size=16777216, Used=8388608, Units=1024
|
|
||||||
[2025-12-03 12:05:00] Speicher erkannt (Index 1): Physical Memory → 50.00%
|
|
||||||
[2025-12-03 12:05:00] Storage[2]: Desc='C:\\ ', Size=536870912, Used=268435456, Units=512
|
|
||||||
[2025-12-03 12:05:00] Datenträger erkannt (Index 2): C:\\ → 50.00%
|
|
||||||
[2025-12-03 12:05:00] RESULT: CPU=25, Mem=50.00, Disk=50.00
|
|
||||||
[2025-12-03 12:05:00] Cache geschrieben, TTL: 10s
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fehlerszenarien & Behavior
|
|
||||||
|
|
||||||
### Szenario 1: SNMP läuft, alles OK
|
|
||||||
```
|
|
||||||
Service: ✅ Daten sofort angezeigt
|
|
||||||
API: ✅ Live-Updates alle 5s
|
|
||||||
Logs: ✅ Beide Logs ganz normal
|
|
||||||
```
|
|
||||||
|
|
||||||
### Szenario 2: SNMP-Erweiterung fehlt
|
|
||||||
```
|
|
||||||
Service: ❌ Exception → abgefangen → 0%, 'n/a' angezeigt
|
|
||||||
API: ❌ Exception → abgefangen → {"error": "snmp_extension_missing"}
|
|
||||||
Logs: ⚠️ Beide Logs zeigen Fehler
|
|
||||||
User-View: "Metriken werden angezeigt (0%), aber nicht aktualisiert"
|
|
||||||
Aktion: Admin sieht Fehler im Log und installiert SNMP
|
|
||||||
```
|
|
||||||
|
|
||||||
### Szenario 3: SNMP antwortet, aber Beschreibungen sind unbekannt
|
|
||||||
```
|
|
||||||
Service: ✅ Fallback-Logik findet RAM/Disk trotzdem
|
|
||||||
API: ✅ Fallback-Logik findet RAM/Disk trotzdem
|
|
||||||
Logs: ℹ️ `Fallback RAM gefunden` / `Fallback Disk gefunden`
|
|
||||||
User-View: ✅ Daten werden angezeigt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Szenario 4: Service fehlt, API läuft
|
|
||||||
```
|
|
||||||
Service: ❌ Exception beim Laden
|
|
||||||
API: ✅ Live-Updates funktionieren normal
|
|
||||||
User-View: "Beim Laden: 0%, nach 5s: aktuelle Werte"
|
|
||||||
Gut genug!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing-Anleitung
|
|
||||||
|
|
||||||
### 1. Initialer Load testen
|
|
||||||
```bash
|
|
||||||
# Browser öffnen, als Admin einloggen
|
|
||||||
# Dashboard öffnen
|
|
||||||
# → Sollten Werte sichtbar sein (entweder echte oder 0%)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Service-Fehler simulieren
|
|
||||||
```php
|
|
||||||
// In DashboardController.php: Service-Aufruf kommentieren
|
|
||||||
// $serverStatus = [... Fallback-Werte ...];
|
|
||||||
// → Dashboard sollte trotzdem laden (mit 0%)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. API testen
|
|
||||||
```bash
|
|
||||||
# Browser DevTools → Network → api/snmp_status.php
|
|
||||||
# → Sollte JSON zurückgeben
|
|
||||||
# Bei 401 → Session fehlt (erwartet wenn nicht angemeldet)
|
|
||||||
# Sollte aber funktionieren wenn angemeldet
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Logs prüfen
|
|
||||||
```bash
|
|
||||||
# PHP Error-Log
|
|
||||||
error_log() Output ansehen
|
|
||||||
|
|
||||||
# SNMP API Log
|
|
||||||
cat public/logs/snmp_api.log
|
|
||||||
# Sollte Einträge zeigen (mit Timestamps)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Cache prüfen
|
|
||||||
```bash
|
|
||||||
# Temp-Datei prüfen
|
|
||||||
# Windows: %TEMP%\snmp_status_cache.json
|
|
||||||
# Linux: /tmp/snmp_status_cache.json
|
|
||||||
# → Sollte JSON enthalten
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Issues & Limitations
|
|
||||||
|
|
||||||
1. **Disk/RAM-Heuristik:** Bei sehr ungewöhnlichen Storage-Labels können Fallbacks greifen, die nicht ideal sind
|
|
||||||
- **Lösung:** Log prüfen (`Storage[X]:` Einträge) und ggf. Heuristiken anpassen
|
|
||||||
|
|
||||||
2. **Cache-Speicher:** Erfordert Schreibzugriff auf `sys_get_temp_dir()`
|
|
||||||
- **Lösung:** Falls nicht verfügbar → Cache-Code entfernen oder APCu/Redis nutzen
|
|
||||||
|
|
||||||
3. **OS-Feld:** Hardcoded auf "Windows Server" (TODO: Dynamisch per OID 1.3.6.1.2.1.1.1.0)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
- **Service-Abfrage:** 1x pro Seitenladung (~100-500ms je nach SNMP-Timeout)
|
|
||||||
- **API-Abfrage:** Alle 5s, aber gecacht für 10s → effektiv alle 10s eine echte SNMP-Abfrage
|
|
||||||
- **JavaScript Polling:** 5s Intervall (Browser-seitig, keine Last auf Server)
|
|
||||||
- **Gesamt:** Sehr effizient, auch bei vielen gleichzeitigen Nutzern
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary für Kollegen
|
|
||||||
|
|
||||||
✅ **Live-Dashboard mit zwei Ebenen:**
|
|
||||||
1. Initial Load via Service (sofortige Daten)
|
|
||||||
2. Live-Polling via API (kontinuierliche Updates)
|
|
||||||
|
|
||||||
✅ **Robuste Fallback-Logik** für RAM und Disk (findet die Werte auch bei unbekannten Labels)
|
|
||||||
|
|
||||||
✅ **Dual Logging:**
|
|
||||||
- PHP Error-Log für Service-Fehler
|
|
||||||
- `public/logs/snmp_api.log` für API-Aktivitäten
|
|
||||||
|
|
||||||
✅ **Session-Geschützt:** Nur Admins können Status abrufen
|
|
||||||
|
|
||||||
✅ **Gecacht:** 10 Sekunden TTL reduziert SNMP-Load
|
|
||||||
|
|
||||||
✅ **Error-tolerant:** Dashboard funktioniert auch wenn SNMP fehlt (zeigt 0%, wartet auf Live-Updates)
|
|
||||||
@ -6,7 +6,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Services\Ldap\LdapAuthService;
|
use App\Services\Ldap\LdapAuthService;
|
||||||
use App\Services\Logging\LoggingService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zuständig für alles rund um den Login:
|
* Zuständig für alles rund um den Login:
|
||||||
@ -27,9 +26,6 @@ class AuthController
|
|||||||
/** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */
|
/** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */
|
||||||
private LdapAuthService $ldapAuthService;
|
private LdapAuthService $ldapAuthService;
|
||||||
|
|
||||||
/** @var LoggingService Logger für technische Fehler */
|
|
||||||
private LoggingService $logger;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice.
|
* Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice.
|
||||||
*
|
*
|
||||||
@ -43,9 +39,6 @@ class AuthController
|
|||||||
// LdapAuthService mit dem Teilbereich "ldap" aus der Konfiguration initialisieren.
|
// LdapAuthService mit dem Teilbereich "ldap" aus der Konfiguration initialisieren.
|
||||||
// Wenn 'ldap' nicht gesetzt ist, wird ein leeres Array übergeben (Fail fast erfolgt dann im Service).
|
// Wenn 'ldap' nicht gesetzt ist, wird ein leeres Array übergeben (Fail fast erfolgt dann im Service).
|
||||||
$this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []);
|
$this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []);
|
||||||
|
|
||||||
// LoggingService mit dem Teilbereich "logging" aus der Konfiguration initialisieren.
|
|
||||||
$this->logger = new LoggingService($config['logging'] ?? []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,25 +86,10 @@ class AuthController
|
|||||||
// false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch)
|
// false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch)
|
||||||
$authenticated = $this->ldapAuthService->authenticate($username, $password);
|
$authenticated = $this->ldapAuthService->authenticate($username, $password);
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
// HIER ist vorher dein Fehler entstanden:
|
// Technischer Fehler (z. B. LDAP-Server nicht erreichbar, falsche Konfiguration).
|
||||||
// - showLoginForm() wurde nur aufgerufen, das Ergebnis aber ignoriert
|
// In diesem Fall wird eine technische Fehlermeldung im Login-Formular angezeigt.
|
||||||
// - danach kam ein "return;" ohne Rückgabewert → Rückgabetyp array wurde verletzt
|
|
||||||
|
|
||||||
// Technischen Fehler ausführlich ins Log schreiben
|
|
||||||
$this->logger->logException(
|
|
||||||
'Technischer Fehler bei der Anmeldung.',
|
|
||||||
$exception,
|
|
||||||
[
|
|
||||||
'route' => 'login.submit',
|
|
||||||
'username' => $username,
|
|
||||||
'remote_addr'=> $_SERVER['REMOTE_ADDR'] ?? null,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Für den Benutzer nur eine allgemeine, aber verständliche Meldung anzeigen
|
|
||||||
return $this->showLoginForm(
|
return $this->showLoginForm(
|
||||||
'Technischer Fehler bei der Anmeldung. Bitte versuchen Sie es später erneut '
|
'Technischer Fehler bei der Anmeldung: ' . $exception->getMessage()
|
||||||
. 'oder wenden Sie sich an den Administrator.'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
@ -7,56 +9,63 @@ use App\Services\Snmp\SnmpServerStatusService;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller für das Dashboard.
|
* Controller für das Dashboard.
|
||||||
|
* Zuständig für:
|
||||||
|
* - Abrufen des Serverstatus (über SnmpServerStatusService)
|
||||||
|
* - Auswählen und Rendern der Dashboard-View
|
||||||
*
|
*
|
||||||
* Zeigt Serverstatus-Metriken über SNMP an:
|
* NEU:
|
||||||
* - Initial Load: Server-seitiger Service-Aufruf (sofortige Daten)
|
* - Gibt ein View-Result zurück, das von index.php + Layout gerendert wird.
|
||||||
* - Live-Updates: Client-seitiges JavaScript-Polling alle 5s
|
|
||||||
*/
|
*/
|
||||||
class DashboardController
|
class DashboardController
|
||||||
{
|
{
|
||||||
|
/** @var array<string, mixed> Vollständige Anwendungskonfiguration (aus config.php) */
|
||||||
private array $config;
|
private array $config;
|
||||||
|
|
||||||
|
/** @var SnmpServerStatusService Service, der den Serverstatus (später per SNMP) liefert */
|
||||||
private SnmpServerStatusService $snmpService;
|
private SnmpServerStatusService $snmpService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Übergibt die Konfiguration an den Controller und initialisiert den SNMP-Statusservice.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
|
||||||
|
*/
|
||||||
public function __construct(array $config)
|
public function __construct(array $config)
|
||||||
{
|
{
|
||||||
|
// Komplette Config lokal speichern (falls später weitere Werte benötigt werden).
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
|
||||||
|
// Teilbereich "snmp" aus der Konfiguration ziehen.
|
||||||
|
// Wenn nicht vorhanden, wird ein leeres Array übergeben (der Service prüft das selbst).
|
||||||
$snmpConfig = $config['snmp'] ?? [];
|
$snmpConfig = $config['snmp'] ?? [];
|
||||||
|
|
||||||
|
// SNMP-Service initialisieren, der den Serverstatus liefert.
|
||||||
$this->snmpService = new SnmpServerStatusService($snmpConfig);
|
$this->snmpService = new SnmpServerStatusService($snmpConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt das Dashboard an.
|
* Zeigt das Dashboard an.
|
||||||
|
* Holt die Serverstatus-Daten aus dem SnmpServerStatusService und übergibt sie an die View.
|
||||||
*
|
*
|
||||||
* Beim initialen Laden wird der Service aufgerufen, um sofort Daten anzuzeigen.
|
* @return array<string, mixed> View-Result für das zentrale Layout
|
||||||
* Live-Updates erfolgen anschließend via JavaScript-Polling (api/snmp_status.php alle 5s).
|
|
||||||
*/
|
*/
|
||||||
public function show(): array
|
public function show(): array
|
||||||
{
|
{
|
||||||
$serverStatus = [
|
// Serverstatus über den SNMP-Service ermitteln.
|
||||||
'hostname' => 'n/a',
|
// In der aktuellen Version liefert der Service noch Demo-Daten.
|
||||||
'os' => 'n/a',
|
|
||||||
'uptime' => 'n/a',
|
|
||||||
'cpu_usage' => 0,
|
|
||||||
'memory_usage' => 0,
|
|
||||||
'disk_usage_c' => 0,
|
|
||||||
'last_update' => date('d.m.Y H:i:s'),
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$serverStatus = $this->snmpService->getServerStatus();
|
$serverStatus = $this->snmpService->getServerStatus();
|
||||||
} catch (\RuntimeException $e) {
|
|
||||||
error_log('DashboardController: SNMP-Fehler beim initialen Laden - ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Pfad zur Dashboard-View (Template-Datei) ermitteln.
|
||||||
$viewPath = __DIR__ . '/../../public/views/dashboard.php';
|
$viewPath = __DIR__ . '/../../public/views/dashboard.php';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'view' => $viewPath,
|
'view' => $viewPath,
|
||||||
'data' => [
|
'data' => [
|
||||||
|
// Die View erwartet aktuell $serverStatus.
|
||||||
'serverStatus' => $serverStatus,
|
'serverStatus' => $serverStatus,
|
||||||
'loginPage' => false,
|
'loginPage' => false,
|
||||||
],
|
],
|
||||||
'pageTitle' => 'Dashboard',
|
'pageTitle' => 'Dashboard',
|
||||||
|
// In der Sidebar soll der Dashboard-Menüpunkt aktiv sein.
|
||||||
'activeMenu' => 'dashboard',
|
'activeMenu' => 'dashboard',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Services\Ldap\LdapDirectoryService;
|
use App\Services\Ldap\LdapDirectoryService;
|
||||||
use App\Services\Logging\LoggingService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller für die Benutzer- und Gruppenanzeige.
|
* Controller für die Benutzer- und Gruppenanzeige.
|
||||||
@ -31,9 +30,6 @@ class UserManagementController
|
|||||||
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
|
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
|
||||||
private LdapDirectoryService $directoryService;
|
private LdapDirectoryService $directoryService;
|
||||||
|
|
||||||
/** @var LoggingService Logger für technische Fehler */
|
|
||||||
private LoggingService $logger;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
|
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php
|
||||||
*/
|
*/
|
||||||
@ -47,9 +43,6 @@ class UserManagementController
|
|||||||
|
|
||||||
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
|
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
|
||||||
$this->directoryService = new LdapDirectoryService($ldapConfig);
|
$this->directoryService = new LdapDirectoryService($ldapConfig);
|
||||||
|
|
||||||
// Logging-Service initialisieren.
|
|
||||||
$this->logger = new LoggingService($config['logging'] ?? []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,18 +63,9 @@ class UserManagementController
|
|||||||
$users = $this->directoryService->getUsers();
|
$users = $this->directoryService->getUsers();
|
||||||
$groups = $this->directoryService->getGroups();
|
$groups = $this->directoryService->getGroups();
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
// Technische Details ins Log, für den Benutzer eine allgemeine Meldung.
|
// Sämtliche technischen Fehler (z. B. Verbindungs- oder Konfigurationsprobleme)
|
||||||
$this->logger->logException(
|
// werden hier in eine für den Benutzer lesbare Fehlermeldung übersetzt.
|
||||||
'Fehler beim Laden von Benutzern/Gruppen.',
|
$error = 'Fehler beim Laden von Benutzern/Gruppen: ' . $exception->getMessage();
|
||||||
$exception,
|
|
||||||
[
|
|
||||||
'route' => 'users',
|
|
||||||
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$error = 'Fehler beim Laden von Benutzern/Gruppen. '
|
|
||||||
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pfad zur eigentlichen View-Datei bestimmen.
|
// Pfad zur eigentlichen View-Datei bestimmen.
|
||||||
|
|||||||
@ -58,8 +58,7 @@ class LdapConnectionHelper
|
|||||||
|
|
||||||
// Verbindung zum LDAP/AD-Server herstellen.
|
// Verbindung zum LDAP/AD-Server herstellen.
|
||||||
// ldap_connect liefert entweder ein Verbindungs-Handle (Resource) oder false.
|
// ldap_connect liefert entweder ein Verbindungs-Handle (Resource) oder false.
|
||||||
$uri = "ldap://".$server . ':' . $port;
|
$connection = ldap_connect($server, $port);
|
||||||
$connection = ldap_connect($uri);
|
|
||||||
|
|
||||||
// Wenn keine Verbindung aufgebaut werden konnte, Exception werfen.
|
// Wenn keine Verbindung aufgebaut werden konnte, Exception werfen.
|
||||||
if ($connection === false) {
|
if ($connection === false) {
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Logging;
|
|
||||||
|
|
||||||
use DateTimeImmutable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Einfacher File-Logger für die AdminTool-Anwendung.
|
|
||||||
*
|
|
||||||
* Ziele:
|
|
||||||
* - Technische Details werden in eine Log-Datei unter public/logs/ geschrieben.
|
|
||||||
* - In der Weboberfläche erscheinen nur verständliche, fachliche Fehlermeldungen.
|
|
||||||
*/
|
|
||||||
class LoggingService
|
|
||||||
{
|
|
||||||
/** @var string Vollständiger Pfad zum Log-Verzeichnis */
|
|
||||||
private string $logDir;
|
|
||||||
|
|
||||||
/** @var string Dateiname der Log-Datei */
|
|
||||||
private string $logFile;
|
|
||||||
|
|
||||||
/** @var int Minimale Log-Stufe, ab der geschrieben wird. */
|
|
||||||
private int $minLevel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zuordnung der Log-Level zu numerischen Werten zur Filterung.
|
|
||||||
*
|
|
||||||
* @var array<string, int>
|
|
||||||
*/
|
|
||||||
private const LEVEL_MAP = [
|
|
||||||
'debug' => 100,
|
|
||||||
'info' => 200,
|
|
||||||
'warning' => 300,
|
|
||||||
'error' => 400,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $config Teilkonfiguration "logging" aus config.php
|
|
||||||
*/
|
|
||||||
public function __construct(array $config)
|
|
||||||
{
|
|
||||||
// Standard: public/logs relativ zum Projektroot
|
|
||||||
$baseDir = $config['log_dir'] ?? (__DIR__ . '/../../../public/logs');
|
|
||||||
$fileName = $config['log_file'] ?? 'app.log';
|
|
||||||
$level = strtolower((string)($config['min_level'] ?? 'info'));
|
|
||||||
|
|
||||||
$this->logDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
|
|
||||||
$this->logFile = $fileName;
|
|
||||||
$this->minLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['info'];
|
|
||||||
|
|
||||||
$this->ensureLogDirectoryExists();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stellt sicher, dass das Log-Verzeichnis existiert.
|
|
||||||
*/
|
|
||||||
private function ensureLogDirectoryExists(): void
|
|
||||||
{
|
|
||||||
if (is_dir($this->logDir) === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@mkdir($this->logDir, 0775, true) === false && is_dir($this->logDir) === false) {
|
|
||||||
// Wenn das Anlegen fehlschlägt, wenigstens einen Eintrag im PHP-Error-Log hinterlassen.
|
|
||||||
error_log(sprintf('LoggingService: Konnte Log-Verzeichnis "%s" nicht anlegen.', $this->logDir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allgemeiner Log-Eintrag.
|
|
||||||
*
|
|
||||||
* @param string $level Log-Level (debug|info|warning|error)
|
|
||||||
* @param string $message Nachrichtentext
|
|
||||||
* @param array<string, mixed> $context Zusätzliche Kontextinformationen
|
|
||||||
*/
|
|
||||||
public function log(string $level, string $message, array $context = []): void
|
|
||||||
{
|
|
||||||
$level = strtolower($level);
|
|
||||||
$numericLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['error'];
|
|
||||||
|
|
||||||
// Alles unterhalb der minimalen Stufe ignorieren.
|
|
||||||
if ($numericLevel < $this->minLevel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$timestamp = (new DateTimeImmutable())->format('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
$contextJson = $context === []
|
|
||||||
? '{}'
|
|
||||||
: (string)json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
||||||
|
|
||||||
$line = sprintf(
|
|
||||||
"[%s] %-7s %s %s%s",
|
|
||||||
$timestamp,
|
|
||||||
strtoupper($level),
|
|
||||||
$message,
|
|
||||||
$contextJson,
|
|
||||||
PHP_EOL
|
|
||||||
);
|
|
||||||
|
|
||||||
$filePath = $this->logDir . DIRECTORY_SEPARATOR . $this->logFile;
|
|
||||||
|
|
||||||
if (@file_put_contents($filePath, $line, FILE_APPEND | LOCK_EX) === false) {
|
|
||||||
// Fallback, damit Fehler beim Logging selbst nicht die App zerschießen.
|
|
||||||
error_log(sprintf('LoggingService: Konnte in Log-Datei "%s" nicht schreiben.', $filePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Komfortmethode, um Exceptions strukturiert zu loggen.
|
|
||||||
*
|
|
||||||
* @param string $message Kurzer Kontexttext zur Exception
|
|
||||||
* @param \Throwable $exception Die geworfene Exception
|
|
||||||
* @param array<string, mixed> $context Zusätzlicher Kontext (Route, Benutzername, Remote-IP, ...)
|
|
||||||
*/
|
|
||||||
public function logException(string $message, \Throwable $exception, array $context = []): void
|
|
||||||
{
|
|
||||||
$exceptionContext = [
|
|
||||||
'exception_class' => get_class($exception),
|
|
||||||
'exception_message' => $exception->getMessage(),
|
|
||||||
'file' => $exception->getFile(),
|
|
||||||
'line' => $exception->getLine(),
|
|
||||||
'trace' => $exception->getTraceAsString(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$mergedContext = array_merge($context, $exceptionContext);
|
|
||||||
|
|
||||||
$this->log('error', $message, $mergedContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\Snmp;
|
namespace App\Services\Snmp;
|
||||||
@ -7,20 +8,15 @@ namespace App\Services\Snmp;
|
|||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service zur Ermittlung des Serverstatus per SNMP.
|
* Service zur Ermittlung des Serverstatus.
|
||||||
*
|
*
|
||||||
* Features:
|
* In dieser ersten Version werden noch statische Demo-Daten zurückgegeben.
|
||||||
* - Robuste Fehlerbehandlung mit aussagekräftigen Exceptions
|
* Später können hier echte SNMP-Abfragen eingebaut werden, ohne dass sich
|
||||||
* - Intelligente Fallback-Logik bei fehlenden oder unerwarteten OID-Beschreibungen
|
* der DashboardController oder die Views ändern müssen.
|
||||||
* - Unterstützung für Windows (C:\) und Linux (/) Systeme
|
|
||||||
* - Detailliertes Logging über Exceptions
|
|
||||||
*
|
|
||||||
* Wird vom DashboardController beim initialen Laden aufgerufen.
|
|
||||||
* Das Live-Polling erfolgt über das API-Endpunkt (public/api/snmp_status.php).
|
|
||||||
*/
|
*/
|
||||||
class SnmpServerStatusService
|
class SnmpServerStatusService
|
||||||
{
|
{
|
||||||
/** @var array<string, mixed> SNMP-Konfiguration (Host, Community, Timeout, OIDs, etc.) */
|
/** @var array<string, mixed> SNMP-spezifische Konfiguration (Host, Community, Timeout, OIDs, etc.) */
|
||||||
private array $config;
|
private array $config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,9 +26,9 @@ class SnmpServerStatusService
|
|||||||
*/
|
*/
|
||||||
public function __construct(array $snmpConfig)
|
public function __construct(array $snmpConfig)
|
||||||
{
|
{
|
||||||
|
// SNMP-Konfiguration in der Instanz speichern.
|
||||||
$this->config = $snmpConfig;
|
$this->config = $snmpConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liefert den aktuellen Serverstatus zurück.
|
* Liefert den aktuellen Serverstatus zurück.
|
||||||
*
|
*
|
||||||
@ -60,166 +56,106 @@ class SnmpServerStatusService
|
|||||||
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (oids fehlen).');
|
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (oids fehlen).');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!function_exists('snmpget')) {
|
// Helper-Funktion zum Bereinigen von SNMP-Antworten (z.B. "INTEGER: 123" -> 123)
|
||||||
throw new RuntimeException('PHP-SNMP-Erweiterung ist nicht installiert.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsfunktion: SNMP-Werte bereinigen (z.B. "INTEGER: 123" -> 123)
|
|
||||||
$cleanSnmpValue = fn($v) => (int)filter_var($v, FILTER_SANITIZE_NUMBER_INT);
|
$cleanSnmpValue = fn($v) => (int)filter_var($v, FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
|
||||||
// --- 2. Uptime abfragen ---
|
// --- 2. Uptime abfragen (war vorher nicht implementiert) ---
|
||||||
$uptimeOid = $oids['uptime'] ?? '1.3.6.1.2.1.1.3.0';
|
$uptimeResult = snmpget($host, $community, $oids['uptime'], $timeout, $retries);
|
||||||
$uptimeResult = @snmpget($host, $community, $uptimeOid, $timeout, $retries);
|
|
||||||
if ($uptimeResult === false) {
|
if ($uptimeResult === false) {
|
||||||
throw new RuntimeException("SNMP Uptime GET fehlgeschlagen.");
|
throw new RuntimeException("SNMP Uptime GET fehlgeschlagen.");
|
||||||
}
|
}
|
||||||
|
// Uptime (timeticks) in ein lesbares Format umwandeln (optional, hier als String)
|
||||||
// Uptime aus TimeTicks (Hundertstel-Sekunden) in lesbar konvertieren
|
// Format ist oft "Timeticks: (12345678) 1 day, 10:17:36.78"
|
||||||
|
// Wir extrahieren den Teil in Klammern (Hundertstelsekunden)
|
||||||
preg_match('/\((.*?)\)/', $uptimeResult, $matches);
|
preg_match('/\((.*?)\)/', $uptimeResult, $matches);
|
||||||
$uptimeTicks = (int)($matches[1] ?? 0);
|
$uptimeTicks = (int)($matches[1] ?? 0);
|
||||||
$uptimeSeconds = $uptimeTicks / 100;
|
$uptimeSeconds = $uptimeTicks / 100;
|
||||||
$uptimeFormatted = sprintf(
|
$uptimeFormatted = sprintf(
|
||||||
'%d Tage, %02d:%02d:%02d',
|
'%d Tage, %02d:%02d:%02d',
|
||||||
(int)floor($uptimeSeconds / 86400),
|
floor($uptimeSeconds / 86400),
|
||||||
(int)floor(($uptimeSeconds % 86400) / 3600),
|
floor(($uptimeSeconds % 86400) / 3600),
|
||||||
(int)floor(($uptimeSeconds % 3600) / 60),
|
floor(($uptimeSeconds % 3600) / 60),
|
||||||
(int)($uptimeSeconds % 60)
|
$uptimeSeconds % 60
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- 3. CPU (Durchschnitt über alle Kerne) ---
|
|
||||||
$cpuTable = $oids['cpu_table'] ?? '1.3.6.1.2.1.25.3.3.1.2';
|
// --- 3. CPU ---
|
||||||
$cpuValues = @snmpwalk($host, $community, $cpuTable, $timeout, $retries);
|
$cpuValues = snmpwalk($host, $community, $oids['cpu_table'], $timeout, $retries);
|
||||||
|
|
||||||
if (!is_array($cpuValues) || empty($cpuValues)) {
|
if (!is_array($cpuValues) || empty($cpuValues)) {
|
||||||
throw new RuntimeException("SNMP CPU WALK fehlgeschlagen.");
|
throw new RuntimeException("SNMP CPU WALK fehlgeschlagen.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$cpuValues = array_map($cleanSnmpValue, $cpuValues);
|
$cpuValues = array_map($cleanSnmpValue, $cpuValues);
|
||||||
$cpuAvg = (int)round(array_sum($cpuValues) / count($cpuValues));
|
$cpuAvg = array_sum($cpuValues) / count($cpuValues);
|
||||||
|
|
||||||
// --- 4. Storage-Tabellen (RAM + Disks) ---
|
|
||||||
$descrOid = $oids['storage_descr'] ?? '1.3.6.1.2.1.25.2.3.1.3';
|
|
||||||
$unitsOid = $oids['storage_units'] ?? '1.3.6.1.2.1.25.2.3.1.4';
|
|
||||||
$sizeOid = $oids['storage_size'] ?? '1.3.6.1.2.1.25.2.3.1.5';
|
|
||||||
$usedOid = $oids['storage_used'] ?? '1.3.6.1.2.1.25.2.3.1.6';
|
|
||||||
|
|
||||||
$descr = @snmpwalk($host, $community, $descrOid, $timeout, $retries);
|
// --- 4. Memory ---
|
||||||
$units = @snmpwalk($host, $community, $unitsOid, $timeout, $retries);
|
$memTotalResult = snmpget($host, $community, $oids['mem_size'], $timeout, $retries);
|
||||||
$size = @snmpwalk($host, $community, $sizeOid, $timeout, $retries);
|
if($memTotalResult === false) {
|
||||||
$used = @snmpwalk($host, $community, $usedOid, $timeout, $retries);
|
throw new RuntimeException("SNMP MemTotal GET fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!is_array($descr) || !is_array($units) || !is_array($size) || !is_array($used)) {
|
// memTotal in Bytes berechnen (korrigierte Reihenfolge von filter/cast)
|
||||||
|
$memTotal = $cleanSnmpValue($memTotalResult) * 1024; // KB -> Bytes
|
||||||
|
|
||||||
|
// Storage-Tabelle (RAM + Disks)
|
||||||
|
$descr = snmpwalk($host, $community, $oids['storage_descr'], $timeout, $retries);
|
||||||
|
$units = snmpwalk($host, $community, $oids['storage_units'], $timeout, $retries);
|
||||||
|
$size = snmpwalk($host, $community, $oids['storage_size'], $timeout, $retries);
|
||||||
|
$used = snmpwalk($host, $community, $oids['storage_used'], $timeout, $retries);
|
||||||
|
|
||||||
|
if ($descr === false || $units === false || $size === false || $used === false) {
|
||||||
throw new RuntimeException("SNMP Storage WALK fehlgeschlagen.");
|
throw new RuntimeException("SNMP Storage WALK fehlgeschlagen.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Werte bereinigen
|
// Werte bereinigen
|
||||||
|
// Die SNMP-Antwort enthält "STRING: " und Anführungszeichen, die wir entfernen müssen.
|
||||||
$descr = array_map(fn($v) => trim(str_ireplace('STRING:', '', $v), ' "'), $descr);
|
$descr = array_map(fn($v) => trim(str_ireplace('STRING:', '', $v), ' "'), $descr);
|
||||||
$units = array_map($cleanSnmpValue, $units);
|
|
||||||
$size = array_map($cleanSnmpValue, $size);
|
|
||||||
$used = array_map($cleanSnmpValue, $used);
|
|
||||||
|
|
||||||
// --- 5. RAM mit Fallback-Logik ---
|
$units = array_map($cleanSnmpValue, $units); // Ints
|
||||||
$ramPercent = null;
|
$size = array_map($cleanSnmpValue, $size); // Ints
|
||||||
$memTotalBytes = null;
|
$used = array_map($cleanSnmpValue, $used); // Ints
|
||||||
|
|
||||||
// Heuristik 1: Suche nach "Physical Memory"
|
|
||||||
|
|
||||||
|
// RAM
|
||||||
$ramIndex = array_search("Physical Memory", $descr);
|
$ramIndex = array_search("Physical Memory", $descr);
|
||||||
if ($ramIndex !== false) {
|
if ($ramIndex === false) {
|
||||||
$memTotalBytes = $units[$ramIndex] * $size[$ramIndex];
|
|
||||||
$ramUsedBytes = $units[$ramIndex] * $used[$ramIndex];
|
|
||||||
$ramPercent = ($memTotalBytes > 0) ? ($ramUsedBytes / $memTotalBytes) * 100 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 1: Wenn nicht gefunden, suche nach ähnlichen Labels
|
|
||||||
if ($ramPercent === null) {
|
|
||||||
foreach ($descr as $index => $description) {
|
|
||||||
$lower = strtolower($description);
|
|
||||||
if (strpos($lower, 'physical') !== false || strpos($lower, 'memory') !== false || strpos($lower, 'ram') !== false) {
|
|
||||||
$memTotalBytes = $units[$index] * $size[$index];
|
|
||||||
$ramUsedBytes = $units[$index] * $used[$index];
|
|
||||||
$ramPercent = ($memTotalBytes > 0) ? ($ramUsedBytes / $memTotalBytes) * 100 : 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 2: Wenn immer noch nicht gefunden, nimm den kleinsten Eintrag (i.d.R. RAM)
|
|
||||||
if ($ramPercent === null && count($descr) > 0) {
|
|
||||||
$minIndex = 0;
|
|
||||||
$minSize = PHP_INT_MAX;
|
|
||||||
foreach ($size as $index => $s) {
|
|
||||||
if ($s > 0 && $s < $minSize) {
|
|
||||||
$minSize = $s;
|
|
||||||
$minIndex = $index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$memTotalBytes = $units[$minIndex] * $size[$minIndex];
|
|
||||||
$ramUsedBytes = $units[$minIndex] * $used[$minIndex];
|
|
||||||
$ramPercent = ($memTotalBytes > 0) ? ($ramUsedBytes / $memTotalBytes) * 100 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 3: Wenn gar nichts geht, Exception
|
|
||||||
if ($ramPercent === null) {
|
|
||||||
throw new RuntimeException("Konnte 'Physical Memory' in der SNMP Storage-Tabelle nicht finden.");
|
throw new RuntimeException("Konnte 'Physical Memory' in der SNMP Storage-Tabelle nicht finden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 6. Disk C: / Root mit Fallback-Logik ---
|
$ramUsedBytes = $units[$ramIndex] * $used[$ramIndex];
|
||||||
$diskCPercent = null;
|
$ramPercent = ($memTotal > 0) ? ($ramUsedBytes / $memTotal) * 100 : 0;
|
||||||
|
|
||||||
// Heuristik 1: Suche nach C:\
|
|
||||||
|
// --- 5. Disk C: ---
|
||||||
|
$cIndex = false;
|
||||||
foreach ($descr as $index => $description) {
|
foreach ($descr as $index => $description) {
|
||||||
|
// str_starts_with prüft, ob der String mit C:\ beginnt
|
||||||
if (str_starts_with($description, 'C:\\')) {
|
if (str_starts_with($description, 'C:\\')) {
|
||||||
$cTotal = $units[$index] * $size[$index];
|
$cIndex = $index;
|
||||||
$cUsed = $units[$index] * $used[$index];
|
|
||||||
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback 1: Suche nach "C:" oder "root" oder "/"
|
if ($cIndex === false) {
|
||||||
if ($diskCPercent === null) {
|
throw new RuntimeException("Konnte Laufwerk 'C:\' in der SNMP Storage-Tabelle nicht finden.");
|
||||||
foreach ($descr as $index => $description) {
|
}
|
||||||
$lower = strtolower($description);
|
|
||||||
if (strpos($lower, 'c:') !== false || $lower === '/' || strpos($lower, 'root') !== false) {
|
$cUsed = $units[$cIndex] * $used[$cIndex];
|
||||||
$cTotal = $units[$index] * $size[$index];
|
$cTotal = $units[$cIndex] * $size[$cIndex];
|
||||||
$cUsed = $units[$index] * $used[$index];
|
|
||||||
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 2: Nimm den größten Eintrag > 1GB (wahrscheinlich der Hauptdatenträger)
|
// --- 6. Status-Array zusammenbauen ---
|
||||||
if ($diskCPercent === null) {
|
|
||||||
$maxIndex = 0;
|
|
||||||
$maxSize = 0;
|
|
||||||
foreach ($size as $index => $s) {
|
|
||||||
$sizeGB = ($s * $units[$index]) / (1024 ** 3);
|
|
||||||
if ($sizeGB > 1 && $s > $maxSize) {
|
|
||||||
$maxSize = $s;
|
|
||||||
$maxIndex = $index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($maxSize > 0) {
|
|
||||||
$cTotal = $units[$maxIndex] * $size[$maxIndex];
|
|
||||||
$cUsed = $units[$maxIndex] * $used[$maxIndex];
|
|
||||||
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 3: Wenn immer noch nichts, Exception
|
|
||||||
if ($diskCPercent === null) {
|
|
||||||
throw new RuntimeException("Konnte Laufwerk 'C:\\' oder Root-Partition in der SNMP Storage-Tabelle nicht finden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 7. Status-Array zusammenbauen ---
|
|
||||||
$status = [
|
$status = [
|
||||||
'hostname' => $host,
|
'hostname' => $host,
|
||||||
'os' => 'Windows Server', // TODO: OS dynamisch per SNMP abfragen (OID 1.3.6.1.2.1.1.1.0)
|
'os' => 'Windows Server 2025 Datacenter', // TODO: OS dynamisch abfragen
|
||||||
'uptime' => $uptimeFormatted,
|
'uptime' => $uptimeFormatted, // Fehlende Variable hinzugefügt
|
||||||
'cpu_usage' => $cpuAvg,
|
'cpu_usage' => round($cpuAvg),
|
||||||
'memory_usage' => (int)round($ramPercent),
|
'memory_usage' => round($ramPercent),
|
||||||
'disk_usage_c' => (int)round($diskCPercent),
|
'disk_usage_c' => round($diskCPercent),
|
||||||
'last_update' => date('d.m.Y H:i:s'),
|
'last_update' => date('d.m.Y H:i:s'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -49,14 +49,4 @@ return [
|
|||||||
'storage_used' => '1.3.6.1.2.1.25.2.3.1.6',
|
'storage_used' => '1.3.6.1.2.1.25.2.3.1.6',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
// Logging-Konfiguration
|
|
||||||
'logging' => [
|
|
||||||
// Standard: public/logs relativ zum Projekt-Root
|
|
||||||
'log_dir' => __DIR__ . '/../public/logs',
|
|
||||||
// Name der Logdatei
|
|
||||||
'log_file' => 'app.log',
|
|
||||||
// Minimale Stufe: debug, info, warning, error
|
|
||||||
'min_level' => 'info',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,292 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SNMP-Status-API für das Dashboard.
|
|
||||||
*
|
|
||||||
* Nur authentifizierte Admins dürfen auf diesen Endpunkt zugreifen.
|
|
||||||
* Wird alle 5s vom JavaScript im Dashboard aufgerufen.
|
|
||||||
*
|
|
||||||
* Sicherheit: Session-Validierung + Admin-Rollenprüfung.
|
|
||||||
* Performance: Datei-basiertes Caching (10 Sekunden) um SNMP-Last zu reduzieren.
|
|
||||||
* Logging: Alle Fehler und wichtigen Daten werden in `public/logs/snmp_api.log` geschrieben.
|
|
||||||
*/
|
|
||||||
|
|
||||||
session_start();
|
|
||||||
|
|
||||||
// === Authentifizierung + Autorisierung ===
|
|
||||||
$sessionKeyUser = 'admin_user'; // aus config
|
|
||||||
if (!isset($_SESSION[$sessionKeyUser])) {
|
|
||||||
http_response_code(401);
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
echo json_encode(['error' => 'unauthorized']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
|
|
||||||
// === Logging-Setup ===
|
|
||||||
$logDir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'logs';
|
|
||||||
if (!is_dir($logDir)) {
|
|
||||||
@mkdir($logDir, 0755, true);
|
|
||||||
}
|
|
||||||
$logFile = $logDir . DIRECTORY_SEPARATOR . 'snmp_api.log';
|
|
||||||
|
|
||||||
function log_msg(string $msg): void {
|
|
||||||
global $logFile;
|
|
||||||
$timestamp = date('Y-m-d H:i:s');
|
|
||||||
@file_put_contents($logFile, "[$timestamp] $msg\n", FILE_APPEND);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotate_log_if_needed(): void {
|
|
||||||
global $logFile;
|
|
||||||
$maxSize = 500 * 1024; // 500 KB (anpassbar)
|
|
||||||
|
|
||||||
if (file_exists($logFile) && filesize($logFile) > $maxSize) {
|
|
||||||
$backupFile = $logFile . '.old';
|
|
||||||
@rename($logFile, $backupFile);
|
|
||||||
log_msg('=== Log rotiert (alte Datei: .old) ===');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$configPath = __DIR__ . '/../../config/config.php';
|
|
||||||
if (!is_readable($configPath)) {
|
|
||||||
log_msg('ERROR: config.php nicht lesbar');
|
|
||||||
echo json_encode(['error' => 'config_not_found']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = require $configPath;
|
|
||||||
$snmp = $config['snmp'] ?? [];
|
|
||||||
|
|
||||||
rotate_log_if_needed();
|
|
||||||
|
|
||||||
log_msg('--- SNMP-Abfrage gestartet ---');
|
|
||||||
|
|
||||||
// === Cache-Logik (Datei) ===
|
|
||||||
// Cache-Datei wird nach 10 Sekunden als ungültig betrachtet
|
|
||||||
$cacheDir = sys_get_temp_dir();
|
|
||||||
$cacheFile = $cacheDir . DIRECTORY_SEPARATOR . 'snmp_status_cache.json';
|
|
||||||
$cacheTTL = 10; // Sekunden
|
|
||||||
|
|
||||||
if (file_exists($cacheFile)) {
|
|
||||||
$cacheAge = time() - filemtime($cacheFile);
|
|
||||||
if ($cacheAge < $cacheTTL) {
|
|
||||||
// Cache ist noch gültig
|
|
||||||
$cached = file_get_contents($cacheFile);
|
|
||||||
if ($cached !== false) {
|
|
||||||
log_msg("Cache HIT (Alter: {$cacheAge}s)");
|
|
||||||
echo $cached;
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log_msg("Cache abgelaufen (Alter: {$cacheAge}s), neue Abfrage");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$host = $snmp['host'] ?? '127.0.0.1';
|
|
||||||
$community = $snmp['community'] ?? 'public';
|
|
||||||
$timeout = (int)($snmp['timeout'] ?? 2);
|
|
||||||
$retries = (int)($snmp['retries'] ?? 1);
|
|
||||||
|
|
||||||
log_msg("SNMP-Host: $host, Community: $community, Timeout: {$timeout}s");
|
|
||||||
|
|
||||||
if (!function_exists('snmpget')) {
|
|
||||||
log_msg('ERROR: SNMP-Erweiterung nicht installiert');
|
|
||||||
echo json_encode(['error' => 'snmp_extension_missing']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
snmp_set_quick_print(true);
|
|
||||||
snmp_set_oid_output_format(SNMP_OID_OUTPUT_NUMERIC);
|
|
||||||
|
|
||||||
// Hilfsfunktion: sichere snmpget-Rückgabe (numeric oder null)
|
|
||||||
function sget(string $host, string $community, string $oid, int $timeout, int $retries)
|
|
||||||
{
|
|
||||||
$v = @snmpget($host, $community, $oid, $timeout, $retries);
|
|
||||||
if ($v === false || $v === null) return null;
|
|
||||||
return is_string($v) ? trim($v) : $v;
|
|
||||||
}
|
|
||||||
|
|
||||||
// System-Uptime (TimeTicks)
|
|
||||||
$uptimeOid = $snmp['oids']['uptime'] ?? '1.3.6.1.2.1.1.3.0';
|
|
||||||
$upticksRaw = @sget($host, $community, $uptimeOid, $timeout, $retries);
|
|
||||||
$upticks = $upticksRaw !== null ? (int)preg_replace('/[^0-9]/', '', (string)$upticksRaw) : null;
|
|
||||||
|
|
||||||
log_msg("Uptime OID: $uptimeOid, Raw: " . ($upticksRaw ?? 'null'));
|
|
||||||
|
|
||||||
function format_uptime(?int $ticks): ?string
|
|
||||||
{
|
|
||||||
if ($ticks === null) return null;
|
|
||||||
// ticks sind Hundertstel-Sekunden
|
|
||||||
$seconds = (int)floor($ticks / 100);
|
|
||||||
$days = intdiv($seconds, 86400);
|
|
||||||
$seconds -= $days * 86400;
|
|
||||||
$hours = intdiv($seconds, 3600);
|
|
||||||
$seconds -= $hours * 3600;
|
|
||||||
$minutes = intdiv($seconds, 60);
|
|
||||||
$seconds -= $minutes * 60;
|
|
||||||
$parts = [];
|
|
||||||
if ($days) $parts[] = $days . ' Tage';
|
|
||||||
if ($hours) $parts[] = $hours . ' Std';
|
|
||||||
if ($minutes) $parts[] = $minutes . ' Min';
|
|
||||||
$parts[] = $seconds . ' Sek';
|
|
||||||
return implode(' ', $parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
$uptimeStr = format_uptime($upticks);
|
|
||||||
|
|
||||||
// CPU: ersten Eintrag aus der CPU-Tabelle lesen
|
|
||||||
$cpuTable = $snmp['oids']['cpu_table'] ?? '1.3.6.1.2.1.25.3.3.1.2';
|
|
||||||
$cpuOid = $cpuTable . '.1';
|
|
||||||
$cpuRaw = @sget($host, $community, $cpuOid, $timeout, $retries);
|
|
||||||
$cpu = $cpuRaw !== null ? (float)preg_replace('/[^0-9.]/', '', (string)$cpuRaw) : null;
|
|
||||||
|
|
||||||
log_msg("CPU OID: $cpuOid, Raw: " . ($cpuRaw ?? 'null') . ", Parsed: " . ($cpu ?? 'null'));
|
|
||||||
|
|
||||||
// Storage-Tabellen: Versuche, C: (Windows) oder Root ("/") zu finden
|
|
||||||
$descrOid = $snmp['oids']['storage_descr'] ?? '1.3.6.1.2.1.25.2.3.1.3';
|
|
||||||
$unitsOid = $snmp['oids']['storage_units'] ?? '1.3.6.1.2.1.25.2.3.1.4';
|
|
||||||
$sizeOid = $snmp['oids']['storage_size'] ?? '1.3.6.1.2.1.25.2.3.1.5';
|
|
||||||
$usedOid = $snmp['oids']['storage_used'] ?? '1.3.6.1.2.1.25.2.3.1.6';
|
|
||||||
|
|
||||||
log_msg("Starte Storage-Walk - Descr: $descrOid, Units: $unitsOid, Size: $sizeOid, Used: $usedOid");
|
|
||||||
|
|
||||||
$descrWalk = @snmprealwalk($host, $community, $descrOid);
|
|
||||||
$unitsWalk = @snmprealwalk($host, $community, $unitsOid);
|
|
||||||
$sizeWalk = @snmprealwalk($host, $community, $sizeOid);
|
|
||||||
$usedWalk = @snmprealwalk($host, $community, $usedOid);
|
|
||||||
|
|
||||||
if (!is_array($descrWalk)) {
|
|
||||||
log_msg('WARNING: snmprealwalk für Beschreibungen fehlgeschlagen');
|
|
||||||
$descrWalk = [];
|
|
||||||
}
|
|
||||||
if (!is_array($unitsWalk)) {
|
|
||||||
log_msg('WARNING: snmprealwalk für Units fehlgeschlagen');
|
|
||||||
$unitsWalk = [];
|
|
||||||
}
|
|
||||||
if (!is_array($sizeWalk)) {
|
|
||||||
log_msg('WARNING: snmprealwalk für Größe fehlgeschlagen');
|
|
||||||
$sizeWalk = [];
|
|
||||||
}
|
|
||||||
if (!is_array($usedWalk)) {
|
|
||||||
log_msg('WARNING: snmprealwalk für Used fehlgeschlagen');
|
|
||||||
$usedWalk = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
log_msg('Storage-Einträge gefunden: ' . count($descrWalk));
|
|
||||||
|
|
||||||
$diskPercent = null;
|
|
||||||
$memPercent = null;
|
|
||||||
$storageEntries = [];
|
|
||||||
|
|
||||||
// Sammeln aller Storage-Einträge für Debug-Logging
|
|
||||||
if (is_array($descrWalk) && count($descrWalk) > 0) {
|
|
||||||
foreach ($descrWalk as $descrOidFull => $descrRaw) {
|
|
||||||
// Index extrahieren
|
|
||||||
if (!preg_match('/\.(\d+)$/', $descrOidFull, $m)) continue;
|
|
||||||
$idx = $m[1];
|
|
||||||
$descr = trim((string)$descrRaw, ' "');
|
|
||||||
|
|
||||||
// Suche nach passenden Einträgen in anderen Walks mit gleichem Index
|
|
||||||
$units = null;
|
|
||||||
$size = null;
|
|
||||||
$used = null;
|
|
||||||
|
|
||||||
foreach ($unitsWalk as $oid => $val) {
|
|
||||||
if (preg_match('/\.(\d+)$/', $oid, $m2) && $m2[1] === $idx) {
|
|
||||||
$units = (int)preg_replace('/[^0-9]/', '', (string)$val);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($sizeWalk as $oid => $val) {
|
|
||||||
if (preg_match('/\.(\d+)$/', $oid, $m2) && $m2[1] === $idx) {
|
|
||||||
$size = (int)preg_replace('/[^0-9]/', '', (string)$val);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($usedWalk as $oid => $val) {
|
|
||||||
if (preg_match('/\.(\d+)$/', $oid, $m2) && $m2[1] === $idx) {
|
|
||||||
$used = (int)preg_replace('/[^0-9]/', '', (string)$val);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log_msg("Storage[$idx]: Desc='$descr', Size=$size, Used=$used, Units=$units");
|
|
||||||
|
|
||||||
if ($size === null || $units === null || $used === null || $size === 0 || $units === 0) {
|
|
||||||
log_msg("Storage[$idx]: SKIP (fehlende oder ungültige Daten)");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalBytes = $size * $units;
|
|
||||||
$usedBytes = $used * $units;
|
|
||||||
$percent = $totalBytes > 0 ? ($usedBytes / $totalBytes) * 100 : 0;
|
|
||||||
|
|
||||||
$storageEntries[] = [
|
|
||||||
'idx' => $idx,
|
|
||||||
'descr' => $descr,
|
|
||||||
'percent' => $percent,
|
|
||||||
'totalGB' => $totalBytes / (1024 ** 3),
|
|
||||||
'usedGB' => $usedBytes / (1024 ** 3),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Heuristik 1: Suche nach C: oder Root
|
|
||||||
$lower = strtolower($descr);
|
|
||||||
if ($diskPercent === null && (strpos($lower, 'c:') !== false || strpos($lower, 'c:\\') !== false || strpos($lower, 'c:/') !== false || $lower === '/' || strpos($lower, 'root') !== false)) {
|
|
||||||
$diskPercent = $percent;
|
|
||||||
log_msg("Datenträger erkannt (Index $idx): $descr → $percent%");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heuristik 2: Suche nach Physical Memory
|
|
||||||
if ($memPercent === null && ((strpos($lower, 'physical') !== false && strpos($lower, 'memory') !== false) || strpos($lower, 'ram') !== false || strpos($lower, 'physical memory') !== false)) {
|
|
||||||
$memPercent = $percent;
|
|
||||||
log_msg("Speicher erkannt (Index $idx): $descr → $percent%");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 1: Wenn keine Disk gefunden, nimm den größten Storage-Eintrag > 1GB
|
|
||||||
if ($diskPercent === null && count($storageEntries) > 0) {
|
|
||||||
log_msg('Fallback: Suche Disk (größter Eintrag > 1GB)');
|
|
||||||
usort($storageEntries, fn($a, $b) => $b['totalGB'] <=> $a['totalGB']);
|
|
||||||
foreach ($storageEntries as $entry) {
|
|
||||||
if ($entry['totalGB'] > 1) {
|
|
||||||
$diskPercent = $entry['percent'];
|
|
||||||
log_msg('Fallback Disk gefunden (Index ' . $entry['idx'] . '): ' . $entry['descr'] . ' → ' . $diskPercent . '%');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 2: Wenn kein Speicher gefunden, nimm den kleinsten Eintrag (meist Physical Memory)
|
|
||||||
if ($memPercent === null && count($storageEntries) > 0) {
|
|
||||||
log_msg('Fallback: Suche Memory (kleinster Eintrag)');
|
|
||||||
usort($storageEntries, fn($a, $b) => $a['totalGB'] <=> $b['totalGB']);
|
|
||||||
$memPercent = $storageEntries[0]['percent'];
|
|
||||||
log_msg('Fallback Memory gefunden (Index ' . $storageEntries[0]['idx'] . '): ' . $storageEntries[0]['descr'] . ' → ' . $memPercent . '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = [
|
|
||||||
'hostname' => @sget($host, $community, '1.3.6.1.2.1.1.5.0', $timeout, $retries) ?? null,
|
|
||||||
'uptime' => $uptimeStr,
|
|
||||||
'upticks' => $upticks,
|
|
||||||
'cpu_usage' => $cpu,
|
|
||||||
'memory_usage' => $memPercent !== null ? round($memPercent, 2) : null,
|
|
||||||
'disk_usage_c' => $diskPercent !== null ? round($diskPercent, 2) : null,
|
|
||||||
'last_update' => time(),
|
|
||||||
];
|
|
||||||
|
|
||||||
log_msg('RESULT: CPU=' . $result['cpu_usage'] . ', Mem=' . $result['memory_usage'] . ', Disk=' . $result['disk_usage_c']);
|
|
||||||
|
|
||||||
$resultJson = json_encode($result);
|
|
||||||
|
|
||||||
// === Cache schreiben ===
|
|
||||||
@file_put_contents($cacheFile, $resultJson);
|
|
||||||
log_msg('Cache geschrieben, TTL: ' . $cacheTTL . 's');
|
|
||||||
|
|
||||||
echo $resultJson;
|
|
||||||
exit;
|
|
||||||
@ -19,14 +19,9 @@ declare(strict_types=1);
|
|||||||
* - Alle neuen Routen sollten über den Switch-Block am Ende ergänzt werden.
|
* - Alle neuen Routen sollten über den Switch-Block am Ende ergänzt werden.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt
|
|
||||||
// oder eine bestehende wird fortgesetzt.
|
|
||||||
session_start();
|
|
||||||
|
|
||||||
// PHP-Fehler erfassen, aber veraltete Hinweise (E_DEPRECATED) ignorieren,
|
// Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt oder eine bestehende wird fortgesetzt.
|
||||||
// weil sie sonst im Zusammenspiel mit IIS/fastcgi zu 500-Fehlern führen können.
|
session_start();
|
||||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
|
|
||||||
ini_set('display_errors', '0');
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\".
|
* Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\".
|
||||||
@ -53,11 +48,11 @@ spl_autoload_register(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Layout-Funktion einbinden (renderLayout)
|
|
||||||
require __DIR__ . '/views/layout.php';
|
require __DIR__ . '/views/layout.php';
|
||||||
|
|
||||||
// Die Konfigurationsdatei liefert ein assoziatives Array mit den Teilbereichen
|
// Die Konfigurationsdatei liefert ein assoziatives Array mit den Teilbereichen
|
||||||
// "ldap", "snmp", "security" und "logging" (u. a. Session-Keys, Timeout-Einstellungen, OIDs, Log-Pfade).
|
// "ldap", "snmp" und "security" (u. a. Session-Keys, Timeout-Einstellungen, OIDs).
|
||||||
|
|
||||||
$configPath = __DIR__ . '/../config/config.php';
|
$configPath = __DIR__ . '/../config/config.php';
|
||||||
if (file_exists($configPath) === false) {
|
if (file_exists($configPath) === false) {
|
||||||
// Fail fast: ohne Konfiguration macht die App keinen Sinn
|
// Fail fast: ohne Konfiguration macht die App keinen Sinn
|
||||||
@ -73,61 +68,6 @@ $config = require $configPath;
|
|||||||
use App\Controllers\AuthController;
|
use App\Controllers\AuthController;
|
||||||
use App\Controllers\DashboardController;
|
use App\Controllers\DashboardController;
|
||||||
use App\Controllers\UserManagementController;
|
use App\Controllers\UserManagementController;
|
||||||
use App\Services\Logging\LoggingService;
|
|
||||||
|
|
||||||
// Globalen Logger initialisieren, damit auch Fehler außerhalb der Controller
|
|
||||||
// (z. B. in index.php selbst) sauber protokolliert werden.
|
|
||||||
$globalLogger = new LoggingService($config['logging'] ?? []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Globale Fehlerbehandlung:
|
|
||||||
* - PHP-Fehler (Warnings, Notices, ...) werden in den Logger geschrieben.
|
|
||||||
* - Unbehandelte Exceptions werden ebenfalls geloggt und führen zu einer generischen 500er-Meldung.
|
|
||||||
*/
|
|
||||||
set_error_handler(
|
|
||||||
static function (
|
|
||||||
int $severity,
|
|
||||||
string $message,
|
|
||||||
string $file = '',
|
|
||||||
int $line = 0
|
|
||||||
) use ($globalLogger): bool {
|
|
||||||
// Fehler nur loggen, wenn sie durch error_reporting() nicht unterdrückt sind.
|
|
||||||
if ((error_reporting() & $severity) === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$globalLogger->log(
|
|
||||||
'error',
|
|
||||||
'PHP-Fehler: ' . $message,
|
|
||||||
[
|
|
||||||
'severity' => $severity,
|
|
||||||
'file' => $file,
|
|
||||||
'line' => $line,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// false zurückgeben = PHP darf seinen Standard-Handler zusätzlich verwenden
|
|
||||||
// (der Browser sieht wegen display_errors=0 trotzdem nichts).
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
set_exception_handler(
|
|
||||||
static function (\Throwable $exception) use ($globalLogger): void {
|
|
||||||
$globalLogger->logException(
|
|
||||||
'Unbehandelte Exception in der Anwendung.',
|
|
||||||
$exception,
|
|
||||||
[
|
|
||||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
|
|
||||||
'route' => $_GET['route'] ?? null,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
http_response_code(500);
|
|
||||||
echo 'Es ist ein unerwarteter Fehler aufgetreten. '
|
|
||||||
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist.
|
* Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist.
|
||||||
@ -159,22 +99,18 @@ function handleResult(?array $result): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect-Result
|
|
||||||
if (isset($result['redirect']) === true) {
|
if (isset($result['redirect']) === true) {
|
||||||
header('Location: ' . (string)$result['redirect']);
|
header('Location: ' . (string)$result['redirect']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// View-Result
|
|
||||||
$contentView = (string)($result['view'] ?? '');
|
$contentView = (string)($result['view'] ?? '');
|
||||||
$viewData = (array)($result['data'] ?? []);
|
$viewData = (array)($result['data'] ?? []);
|
||||||
|
|
||||||
// Standard: Wir gehen davon aus, dass es KEINE Loginseite ist,
|
// Standard: Wir gehen davon aus, dass es KEINE Loginseite ist,
|
||||||
// außer der Controller sagt explizit etwas anderes.
|
// außer der Controller sagt explizit etwas anderes.
|
||||||
if (array_key_exists('loginPage', $viewData) === false) {
|
if (!array_key_exists('loginPage', $viewData)) {
|
||||||
$viewData['loginPage'] = false;
|
$viewData['loginPage'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pageTitle = (string)($result['pageTitle'] ?? '');
|
$pageTitle = (string)($result['pageTitle'] ?? '');
|
||||||
$activeMenu = $result['activeMenu'] ?? null;
|
$activeMenu = $result['activeMenu'] ?? null;
|
||||||
|
|
||||||
@ -185,27 +121,24 @@ function handleResult(?array $result): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hier rufen wir jetzt die Layout-Funktion aus layout.php auf
|
// Hier rufen wir jetzt die Layout-Funktion aus layout.php auf
|
||||||
renderLayout(
|
renderLayout($contentView, $viewData, $pageTitle, is_string($activeMenu) ? $activeMenu : null);
|
||||||
$contentView,
|
|
||||||
$viewData,
|
|
||||||
$pageTitle,
|
|
||||||
is_string($activeMenu) ? $activeMenu : null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zentrale Controller der Anwendung initialisieren und ihnen die vollständige Konfiguration übergeben.
|
// Zentrale Controller der Anwendung initialisieren und ihnen die vollständige Konfiguration übergeben.
|
||||||
// Die Controller holen sich daraus bei Bedarf ihre spezifischen Teilkonfigurationen (z. B. "ldap" oder "snmp").
|
// Die Controller holen sich daraus bei Bedarf ihre spezifischen Teilkonfigurationen (z. B. "ldap" oder "snmp").
|
||||||
// Jeder Controller erzeugt intern seinen eigenen LoggingService aus $config['logging'].
|
|
||||||
$authController = new AuthController($config);
|
$authController = new AuthController($config);
|
||||||
$dashboardController = new DashboardController($config);
|
$dashboardController = new DashboardController($config);
|
||||||
$userManagementController = new UserManagementController($config);
|
$userManagementController = new UserManagementController($config);
|
||||||
|
|
||||||
// Route aus dem Query-Parameter lesen. Standardroute ist "login",
|
// Route aus dem Query-Parameter lesen. Standardroute ist "login",
|
||||||
// sodass nicht angemeldete Benutzer automatisch auf die Login-Seite geführt werden.
|
// sodass nicht angemeldete Benutzer automatisch auf die Login-Seite geführt werden.
|
||||||
|
|
||||||
$route = $_GET['route'] ?? 'login';
|
$route = $_GET['route'] ?? 'login';
|
||||||
|
|
||||||
// Einfache Router-Logik: Jede Route ruft eine Controller-Methode auf und
|
// Einfache Router-Logik: Jede Route ruft eine Controller-Methode auf und
|
||||||
// übergibt deren View-Result an handleResult(). Neue Seiten werden hier ergänzt.
|
// übergibt deren View-Result an handleResult(). Neue Seiten werden hier ergänzt.
|
||||||
|
|
||||||
switch ($route) {
|
switch ($route) {
|
||||||
case 'login':
|
case 'login':
|
||||||
// Login-Formular anzeigen (ggf. mit Fehlermeldung)
|
// Login-Formular anzeigen (ggf. mit Fehlermeldung)
|
||||||
@ -245,3 +178,5 @@ switch ($route) {
|
|||||||
echo 'Route nicht gefunden.';
|
echo 'Route nicht gefunden.';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server-Dashboard (Ansicht).
|
* View-Template für das Server-Dashboard.
|
||||||
*
|
*
|
||||||
* Zeigt Server-Kennzahlen an (Hostname, Uptime, CPU, RAM, Datenträger).
|
* Aufgaben:
|
||||||
* Erwartetes Array: `$serverStatus` mit Schlüsseln: hostname, uptime, cpu_usage,
|
* - Visualisiert den vom SnmpServerStatusService gelieferten Serverstatus.
|
||||||
* memory_usage, disk_usage_c, last_update.
|
* - Zeigt Kennzahlen wie Hostname, Uptime, CPU-Auslastung, RAM-Auslastung
|
||||||
|
* und Belegung der Systempartition "C:" an.
|
||||||
|
*
|
||||||
|
* Erwartete View-Daten:
|
||||||
|
* - array<string, mixed> $serverStatus Assoziatives Array mit Statuswerten (hostname, uptime, cpu_usage, memory_usage, disk_usage_c, last_update).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @var array<string, mixed> $serverStatus */
|
/** @var array<string, mixed> $serverStatus */
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Content Row -->
|
|
||||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||||
<h1 class="h3 mb-0 text-gray-800">Server-Dashboard</h1>
|
<h1 class="h3 mb-0 text-gray-800">Server-Dashboard</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<!-- Hostname -->
|
<!-- Hostname -->
|
||||||
<div class="col-xl-3 col-md-6 mb-4">
|
<div class="col-xl-3 col-md-6 mb-4">
|
||||||
<div class="card border-left-primary shadow h-100 py-2">
|
<div class="card border-left-primary shadow h-100 py-2">
|
||||||
@ -70,7 +75,7 @@ declare(strict_types=1);
|
|||||||
CPU-Auslastung
|
CPU-Auslastung
|
||||||
</div>
|
</div>
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
<span id="cpu_card_value"><?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?></span> %
|
<?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?> %
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@ -91,7 +96,7 @@ declare(strict_types=1);
|
|||||||
RAM-Auslastung
|
RAM-Auslastung
|
||||||
</div>
|
</div>
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
<span id="mem_card_value"><?php echo (int)($serverStatus['memory_usage'] ?? 0); ?></span> %
|
<?php echo (int)($serverStatus['memory_usage'] ?? 0); ?> %
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@ -101,85 +106,6 @@ declare(strict_types=1);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Hier kann man du später Charts, weitere Karten usw. anhängen -->
|
||||||
<div class="row">
|
|
||||||
<!-- Disk C: / Root -->
|
|
||||||
<div class="col-xl-3 col-md-6 mb-4">
|
|
||||||
<div class="card border-left-secondary shadow h-100 py-2">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row no-gutters align-items-center">
|
|
||||||
<div class="col mr-2">
|
|
||||||
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
|
|
||||||
Datenträger C: / Root
|
|
||||||
</div>
|
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
||||||
<span id="disk_usage_text"><?php echo isset($serverStatus['disk_usage_c']) ? (int)$serverStatus['disk_usage_c'] : 'n/a'; ?></span> %
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<i class="fas fa-hdd fa-2x text-gray-300"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Uptime -->
|
|
||||||
<div class="col-xl-3 col-md-6 mb-4">
|
|
||||||
<div class="card border-left-dark shadow h-100 py-2">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row no-gutters align-items-center">
|
|
||||||
<div class="col mr-2">
|
|
||||||
<div class="text-xs font-weight-bold text-dark text-uppercase mb-1">
|
|
||||||
System Uptime
|
|
||||||
</div>
|
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
||||||
<span id="uptime_text"><?php echo htmlspecialchars((string)($serverStatus['uptime'] ?? 'n/a'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<i class="fas fa-clock fa-2x text-gray-300"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 mb-2">
|
|
||||||
<div class="small text-gray-600">Letztes Update: <span id="snmp_last_update"></span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function(){
|
|
||||||
const diskEl = document.getElementById('disk_usage_text');
|
|
||||||
const uptimeEl = document.getElementById('uptime_text');
|
|
||||||
const cpuEl = document.getElementById('cpu_card_value');
|
|
||||||
const memEl = document.getElementById('mem_card_value');
|
|
||||||
const lastEl = document.getElementById('snmp_last_update');
|
|
||||||
|
|
||||||
function updateUI(data){
|
|
||||||
if(!data) return;
|
|
||||||
if(diskEl) diskEl.textContent = (data.disk_usage_c !== null && data.disk_usage_c !== undefined) ? Math.round(data.disk_usage_c) : 'n/a';
|
|
||||||
if(uptimeEl) uptimeEl.textContent = data.uptime || 'n/a';
|
|
||||||
if(cpuEl) cpuEl.textContent = (data.cpu_usage !== null && data.cpu_usage !== undefined) ? Math.round(data.cpu_usage) : 0;
|
|
||||||
if(memEl) memEl.textContent = (data.memory_usage !== null && data.memory_usage !== undefined) ? Math.round(data.memory_usage) : 0;
|
|
||||||
if(lastEl) lastEl.textContent = data.last_update ? new Date(data.last_update * 1000).toLocaleTimeString() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchStatus(){
|
|
||||||
try{
|
|
||||||
const res = await fetch('api/snmp_status.php');
|
|
||||||
if(!res.ok) throw new Error('Network response not ok');
|
|
||||||
const json = await res.json();
|
|
||||||
updateUI(json);
|
|
||||||
} catch(e){
|
|
||||||
console.error('Abruf SNMP fehlgeschlagen', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchStatus();
|
|
||||||
setInterval(fetchStatus, 5000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ declare(strict_types=1);
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered" id="usersTable">
|
<table class="table table-bordered" id="usersTable" width="100%" cellspacing="0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><input type="checkbox" name="selectAllUsers"></th>
|
<th><input type="checkbox" name="selectAllUsers"></th>
|
||||||
@ -83,7 +83,7 @@ declare(strict_types=1);
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered" id="groupsTable">
|
<table class="table table-bordered" id="groupsTable" width="100%" cellspacing="0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Gruppenname (sAMAccountName)</th>
|
<th>Gruppenname (sAMAccountName)</th>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user