From c03a99d07a55f16cfefa60a5ef900bb52d1b8106 Mon Sep 17 00:00:00 2001 From: Tom <165781231+GraegelTh@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:06:10 +0100 Subject: [PATCH 1/2] Updated Snmp and Interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 297 ++++++++++++++++++ app/Controllers/DashboardController.php | 69 ++-- app/Services/Snmp/SnmpServerStatusService.php | 196 ++++++++---- public/api/snmp_status.php | 278 ++++++++++++++++ public/views/dashboard.php | 102 +++++- 5 files changed, 823 insertions(+), 119 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 public/api/snmp_status.php diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f83df96 --- /dev/null +++ b/CHANGELOG.md @@ -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) diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index c5fe63a..64913db 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -1,6 +1,4 @@ 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 $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 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' => $viewPath, + 'data' => [ + 'serverStatus' => $serverStatus, + 'loginPage' => false, + ], + 'pageTitle' => 'Dashboard', + 'activeMenu' => 'dashboard', ]; } } diff --git a/app/Services/Snmp/SnmpServerStatusService.php b/app/Services/Snmp/SnmpServerStatusService.php index 56ae54a..b75907e 100644 --- a/app/Services/Snmp/SnmpServerStatusService.php +++ b/app/Services/Snmp/SnmpServerStatusService.php @@ -1,6 +1,5 @@ SNMP-spezifische Konfiguration (Host, Community, Timeout, OIDs, etc.) */ + /** @var array 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. * @@ -55,108 +59,168 @@ class SnmpServerStatusService if (empty($oids)) { 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."); - } - - // memTotal in Bytes berechnen (korrigierte Reihenfolge von filter/cast) - $memTotal = $cleanSnmpValue($memTotalResult) * 1024; // KB -> Bytes + $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); - // 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); // Ints - $size = array_map($cleanSnmpValue, $size); // Ints - $used = array_map($cleanSnmpValue, $used); // Ints + $units = array_map($cleanSnmpValue, $units); + $size = array_map($cleanSnmpValue, $size); + $used = array_map($cleanSnmpValue, $used); - + // --- 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; + } + } - $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."); + } - // --- 6. Status-Array zusammenbauen --- + // --- 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), - 'last_update' => date('d.m.Y H:i:s'), + 'hostname' => $host, + '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'), ]; return $status; diff --git a/public/api/snmp_status.php b/public/api/snmp_status.php new file mode 100644 index 0000000..ea85338 --- /dev/null +++ b/public/api/snmp_status.php @@ -0,0 +1,278 @@ + '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; diff --git a/public/views/dashboard.php b/public/views/dashboard.php index ea5180d..d2c2026 100644 --- a/public/views/dashboard.php +++ b/public/views/dashboard.php @@ -1,28 +1,23 @@ $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 $serverStatus */ ?> +

Server-Dashboard

-
@@ -75,7 +70,7 @@ declare(strict_types=1); CPU-Auslastung
- % + %
@@ -96,7 +91,7 @@ declare(strict_types=1); RAM-Auslastung
- % + %
@@ -106,6 +101,85 @@ declare(strict_types=1);
- - + +
+ +
+
+
+
+
+
+ Datenträger C: / Root +
+
+ % +
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ System Uptime +
+
+ +
+
+
+ +
+
+
+
+
+ +
+
Letztes Update:
+
+
+ + -- 2.45.2 From b5146cde6547b485025fdf59544891d96edf3f9a Mon Sep 17 00:00:00 2001 From: Tom <165781231+GraegelTh@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:54:12 +0100 Subject: [PATCH 2/2] Update snmp_status.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funktion Hinzugefügt damit log files nicht unendlich wachsen jeweils ein 500kb backup und ein 500kb aktueller file --- public/api/snmp_status.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/public/api/snmp_status.php b/public/api/snmp_status.php index ea85338..4d26b23 100644 --- a/public/api/snmp_status.php +++ b/public/api/snmp_status.php @@ -38,6 +38,18 @@ function log_msg(string $msg): void { @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'); @@ -48,6 +60,8 @@ if (!is_readable($configPath)) { $config = require $configPath; $snmp = $config['snmp'] ?? []; +rotate_log_if_needed(); + log_msg('--- SNMP-Abfrage gestartet ---'); // === Cache-Logik (Datei) === -- 2.45.2