Updated Snmp and Interface

Ich habe das UI Testweise nochmal erweitert und die Art wie wir die SNMP Daten abrufen und anzeigen nochmal überarbeitet. Es gibt jetzt eine kleine API die über Javascript in Intervallen die Daten aktualisiert. In der Datei Changelog sind nochmal die genauen Änderungen erklärt.
This commit is contained in:
Tom 2025-12-03 17:06:10 +01:00
parent f9f9b1f99b
commit c03a99d07a
5 changed files with 823 additions and 119 deletions

297
CHANGELOG.md Normal file
View File

@ -0,0 +1,297 @@
# 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)

View File

@ -1,6 +1,4 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Controllers;
@ -9,63 +7,56 @@ use App\Services\Snmp\SnmpServerStatusService;
/**
* Controller für das Dashboard.
* Zuständig für:
* - Abrufen des Serverstatus (über SnmpServerStatusService)
* - Auswählen und Rendern der Dashboard-View
*
* NEU:
* - Gibt ein View-Result zurück, das von index.php + Layout gerendert wird.
* Zeigt Serverstatus-Metriken über SNMP an:
* - Initial Load: Server-seitiger Service-Aufruf (sofortige Daten)
* - Live-Updates: Client-seitiges JavaScript-Polling alle 5s
*/
class DashboardController
{
/** @var array<string, mixed> Vollständige Anwendungskonfiguration (aus config.php) */
private array $config;
/** @var SnmpServerStatusService Service, der den Serverstatus (später per SNMP) liefert */
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)
{
// Komplette Config lokal speichern (falls später weitere Werte benötigt werden).
$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'] ?? [];
// SNMP-Service initialisieren, der den Serverstatus liefert.
$this->snmpService = new SnmpServerStatusService($snmpConfig);
}
/**
* Zeigt das Dashboard an.
* Holt die Serverstatus-Daten aus dem SnmpServerStatusService und übergibt sie an die View.
*
* @return array<string, mixed> View-Result für das zentrale Layout
* Beim initialen Laden wird der Service aufgerufen, um sofort Daten anzuzeigen.
* Live-Updates erfolgen anschließend via JavaScript-Polling (api/snmp_status.php alle 5s).
*/
public function show(): array
{
// Serverstatus über den SNMP-Service ermitteln.
// In der aktuellen Version liefert der Service noch Demo-Daten.
$serverStatus = $this->snmpService->getServerStatus();
$serverStatus = [
'hostname' => 'n/a',
'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();
} 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';
return [
'view' => $viewPath,
'data' => [
// Die View erwartet aktuell $serverStatus.
'serverStatus' => $serverStatus,
'loginPage' => false,
],
'pageTitle' => 'Dashboard',
// In der Sidebar soll der Dashboard-Menüpunkt aktiv sein.
'activeMenu' => 'dashboard',
];
}

View File

@ -1,6 +1,5 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Services\Snmp;
@ -8,15 +7,20 @@ namespace App\Services\Snmp;
use RuntimeException;
/**
* Service zur Ermittlung des Serverstatus.
* Service zur Ermittlung des Serverstatus per SNMP.
*
* In dieser ersten Version werden noch statische Demo-Daten zurückgegeben.
* Später können hier echte SNMP-Abfragen eingebaut werden, ohne dass sich
* der DashboardController oder die Views ändern müssen.
* Features:
* - Robuste Fehlerbehandlung mit aussagekräftigen Exceptions
* - Intelligente Fallback-Logik bei fehlenden oder unerwarteten OID-Beschreibungen
* - 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
{
/** @var array<string, mixed> SNMP-spezifische Konfiguration (Host, Community, Timeout, OIDs, etc.) */
/** @var array<string, mixed> SNMP-Konfiguration (Host, Community, Timeout, OIDs, etc.) */
private array $config;
/**
@ -26,9 +30,9 @@ class SnmpServerStatusService
*/
public function __construct(array $snmpConfig)
{
// SNMP-Konfiguration in der Instanz speichern.
$this->config = $snmpConfig;
}
/**
* Liefert den aktuellen Serverstatus zurück.
*
@ -56,106 +60,166 @@ class SnmpServerStatusService
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (oids fehlen).');
}
// Helper-Funktion zum Bereinigen von SNMP-Antworten (z.B. "INTEGER: 123" -> 123)
if (!function_exists('snmpget')) {
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);
// --- 2. Uptime abfragen (war vorher nicht implementiert) ---
$uptimeResult = snmpget($host, $community, $oids['uptime'], $timeout, $retries);
// --- 2. Uptime abfragen ---
$uptimeOid = $oids['uptime'] ?? '1.3.6.1.2.1.1.3.0';
$uptimeResult = @snmpget($host, $community, $uptimeOid, $timeout, $retries);
if ($uptimeResult === false) {
throw new RuntimeException("SNMP Uptime GET fehlgeschlagen.");
}
// Uptime (timeticks) in ein lesbares Format umwandeln (optional, hier als String)
// Format ist oft "Timeticks: (12345678) 1 day, 10:17:36.78"
// Wir extrahieren den Teil in Klammern (Hundertstelsekunden)
// Uptime aus TimeTicks (Hundertstel-Sekunden) in lesbar konvertieren
preg_match('/\((.*?)\)/', $uptimeResult, $matches);
$uptimeTicks = (int)($matches[1] ?? 0);
$uptimeSeconds = $uptimeTicks / 100;
$uptimeFormatted = sprintf(
'%d Tage, %02d:%02d:%02d',
floor($uptimeSeconds / 86400),
floor(($uptimeSeconds % 86400) / 3600),
floor(($uptimeSeconds % 3600) / 60),
$uptimeSeconds % 60
(int)floor($uptimeSeconds / 86400),
(int)floor(($uptimeSeconds % 86400) / 3600),
(int)floor(($uptimeSeconds % 3600) / 60),
(int)($uptimeSeconds % 60)
);
// --- 3. CPU ---
$cpuValues = snmpwalk($host, $community, $oids['cpu_table'], $timeout, $retries);
// --- 3. CPU (Durchschnitt über alle Kerne) ---
$cpuTable = $oids['cpu_table'] ?? '1.3.6.1.2.1.25.3.3.1.2';
$cpuValues = @snmpwalk($host, $community, $cpuTable, $timeout, $retries);
if (!is_array($cpuValues) || empty($cpuValues)) {
throw new RuntimeException("SNMP CPU WALK fehlgeschlagen.");
}
$cpuValues = array_map($cleanSnmpValue, $cpuValues);
$cpuAvg = array_sum($cpuValues) / count($cpuValues);
$cpuAvg = (int)round(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';
// --- 4. Memory ---
$memTotalResult = snmpget($host, $community, $oids['mem_size'], $timeout, $retries);
if($memTotalResult === false) {
throw new RuntimeException("SNMP MemTotal GET fehlgeschlagen.");
}
$descr = @snmpwalk($host, $community, $descrOid, $timeout, $retries);
$units = @snmpwalk($host, $community, $unitsOid, $timeout, $retries);
$size = @snmpwalk($host, $community, $sizeOid, $timeout, $retries);
$used = @snmpwalk($host, $community, $usedOid, $timeout, $retries);
// 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) {
if (!is_array($descr) || !is_array($units) || !is_array($size) || !is_array($used)) {
throw new RuntimeException("SNMP Storage WALK fehlgeschlagen.");
}
// 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);
$units = array_map($cleanSnmpValue, $units);
$size = array_map($cleanSnmpValue, $size);
$used = array_map($cleanSnmpValue, $used);
$units = array_map($cleanSnmpValue, $units); // Ints
$size = array_map($cleanSnmpValue, $size); // Ints
$used = array_map($cleanSnmpValue, $used); // Ints
// --- 5. RAM mit Fallback-Logik ---
$ramPercent = null;
$memTotalBytes = null;
// RAM
// Heuristik 1: Suche nach "Physical Memory"
$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.");
}
$ramUsedBytes = $units[$ramIndex] * $used[$ramIndex];
$ramPercent = ($memTotal > 0) ? ($ramUsedBytes / $memTotal) * 100 : 0;
// --- 6. Disk C: / Root mit Fallback-Logik ---
$diskCPercent = null;
// --- 5. Disk C: ---
$cIndex = false;
// Heuristik 1: Suche nach C:\
foreach ($descr as $index => $description) {
// str_starts_with prüft, ob der String mit C:\ beginnt
if (str_starts_with($description, 'C:\\')) {
$cIndex = $index;
$cTotal = $units[$index] * $size[$index];
$cUsed = $units[$index] * $used[$index];
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
break;
}
}
if ($cIndex === false) {
throw new RuntimeException("Konnte Laufwerk 'C:\' in der SNMP Storage-Tabelle nicht finden.");
// Fallback 1: Suche nach "C:" oder "root" oder "/"
if ($diskCPercent === null) {
foreach ($descr as $index => $description) {
$lower = strtolower($description);
if (strpos($lower, 'c:') !== false || $lower === '/' || strpos($lower, 'root') !== false) {
$cTotal = $units[$index] * $size[$index];
$cUsed = $units[$index] * $used[$index];
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
break;
}
}
}
$cUsed = $units[$cIndex] * $used[$cIndex];
$cTotal = $units[$cIndex] * $size[$cIndex];
// Fallback 2: Nimm den größten Eintrag > 1GB (wahrscheinlich der Hauptdatenträger)
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;
}
}
// --- 6. Status-Array zusammenbauen ---
// 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 = [
'hostname' => $host,
'os' => 'Windows Server 2025 Datacenter', // TODO: OS dynamisch abfragen
'uptime' => $uptimeFormatted, // Fehlende Variable hinzugefügt
'cpu_usage' => round($cpuAvg),
'memory_usage' => round($ramPercent),
'disk_usage_c' => round($diskCPercent),
'os' => 'Windows Server', // TODO: OS dynamisch per SNMP abfragen (OID 1.3.6.1.2.1.1.1.0)
'uptime' => $uptimeFormatted,
'cpu_usage' => $cpuAvg,
'memory_usage' => (int)round($ramPercent),
'disk_usage_c' => (int)round($diskCPercent),
'last_update' => date('d.m.Y H:i:s'),
];

278
public/api/snmp_status.php Normal file
View File

@ -0,0 +1,278 @@
<?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);
}
$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'] ?? [];
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;

View File

@ -1,28 +1,23 @@
<?php
declare(strict_types=1);
/**
* View-Template für das Server-Dashboard.
* Server-Dashboard (Ansicht).
*
* Aufgaben:
* - Visualisiert den vom SnmpServerStatusService gelieferten Serverstatus.
* - 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).
* Zeigt Server-Kennzahlen an (Hostname, Uptime, CPU, RAM, Datenträger).
* Erwartetes Array: `$serverStatus` mit Schlüsseln: hostname, uptime, cpu_usage,
* memory_usage, disk_usage_c, last_update.
*/
/** @var array<string, mixed> $serverStatus */
?>
<!-- Content Row -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Server-Dashboard</h1>
</div>
<div class="row">
<!-- Hostname -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
@ -75,7 +70,7 @@ declare(strict_types=1);
CPU-Auslastung
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?> %
<span id="cpu_card_value"><?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?></span> %
</div>
</div>
<div class="col-auto">
@ -96,7 +91,7 @@ declare(strict_types=1);
RAM-Auslastung
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?php echo (int)($serverStatus['memory_usage'] ?? 0); ?> %
<span id="mem_card_value"><?php echo (int)($serverStatus['memory_usage'] ?? 0); ?></span> %
</div>
</div>
<div class="col-auto">
@ -106,6 +101,85 @@ declare(strict_types=1);
</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>