snmp_update2 (#14)
Co-authored-by: Tom <165781231+GraegelTh@users.noreply.github.com> Reviewed-on: https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/pulls/14
This commit is contained in:
parent
f9f9b1f99b
commit
e82c2c041a
297
CHANGELOG.md
Normal file
297
CHANGELOG.md
Normal 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)
|
||||||
@ -1,6 +1,4 @@
|
|||||||
<?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;
|
||||||
@ -9,64 +7,57 @@ use App\Services\Snmp\SnmpServerStatusService;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller für das Dashboard.
|
* Controller für das Dashboard.
|
||||||
* Zuständig für:
|
*
|
||||||
* - Abrufen des Serverstatus (über SnmpServerStatusService)
|
* Zeigt Serverstatus-Metriken über SNMP an:
|
||||||
* - Auswählen und Rendern der Dashboard-View
|
* - Initial Load: Server-seitiger Service-Aufruf (sofortige Daten)
|
||||||
*
|
* - Live-Updates: Client-seitiges JavaScript-Polling alle 5s
|
||||||
* NEU:
|
|
||||||
* - Gibt ein View-Result zurück, das von index.php + Layout gerendert wird.
|
|
||||||
*/
|
*/
|
||||||
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 über den SNMP-Service ermitteln.
|
$serverStatus = [
|
||||||
// In der aktuellen Version liefert der Service noch Demo-Daten.
|
'hostname' => 'n/a',
|
||||||
$serverStatus = $this->snmpService->getServerStatus();
|
'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';
|
$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',
|
'activeMenu' => 'dashboard',
|
||||||
// In der Sidebar soll der Dashboard-Menüpunkt aktiv sein.
|
|
||||||
'activeMenu' => 'dashboard',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<?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;
|
||||||
@ -8,15 +7,20 @@ namespace App\Services\Snmp;
|
|||||||
use RuntimeException;
|
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.
|
* Features:
|
||||||
* Später können hier echte SNMP-Abfragen eingebaut werden, ohne dass sich
|
* - Robuste Fehlerbehandlung mit aussagekräftigen Exceptions
|
||||||
* der DashboardController oder die Views ändern müssen.
|
* - 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
|
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;
|
private array $config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,9 +30,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.
|
||||||
*
|
*
|
||||||
@ -55,108 +59,168 @@ class SnmpServerStatusService
|
|||||||
if (empty($oids)) {
|
if (empty($oids)) {
|
||||||
throw new RuntimeException('SNMP-Konfiguration ist unvollständig (oids fehlen).');
|
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);
|
$cleanSnmpValue = fn($v) => (int)filter_var($v, FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
|
||||||
// --- 2. Uptime abfragen (war vorher nicht implementiert) ---
|
// --- 2. Uptime abfragen ---
|
||||||
$uptimeResult = snmpget($host, $community, $oids['uptime'], $timeout, $retries);
|
$uptimeOid = $oids['uptime'] ?? '1.3.6.1.2.1.1.3.0';
|
||||||
|
$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)
|
|
||||||
// Format ist oft "Timeticks: (12345678) 1 day, 10:17:36.78"
|
// Uptime aus TimeTicks (Hundertstel-Sekunden) in lesbar konvertieren
|
||||||
// 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',
|
||||||
floor($uptimeSeconds / 86400),
|
(int)floor($uptimeSeconds / 86400),
|
||||||
floor(($uptimeSeconds % 86400) / 3600),
|
(int)floor(($uptimeSeconds % 86400) / 3600),
|
||||||
floor(($uptimeSeconds % 3600) / 60),
|
(int)floor(($uptimeSeconds % 3600) / 60),
|
||||||
$uptimeSeconds % 60
|
(int)($uptimeSeconds % 60)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- 3. CPU (Durchschnitt über alle Kerne) ---
|
||||||
// --- 3. CPU ---
|
$cpuTable = $oids['cpu_table'] ?? '1.3.6.1.2.1.25.3.3.1.2';
|
||||||
$cpuValues = snmpwalk($host, $community, $oids['cpu_table'], $timeout, $retries);
|
$cpuValues = @snmpwalk($host, $community, $cpuTable, $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 = 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 ---
|
$descr = @snmpwalk($host, $community, $descrOid, $timeout, $retries);
|
||||||
$memTotalResult = snmpget($host, $community, $oids['mem_size'], $timeout, $retries);
|
$units = @snmpwalk($host, $community, $unitsOid, $timeout, $retries);
|
||||||
if($memTotalResult === false) {
|
$size = @snmpwalk($host, $community, $sizeOid, $timeout, $retries);
|
||||||
throw new RuntimeException("SNMP MemTotal GET fehlgeschlagen.");
|
$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)
|
if (!is_array($descr) || !is_array($units) || !is_array($size) || !is_array($used)) {
|
||||||
$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);
|
||||||
$units = array_map($cleanSnmpValue, $units); // Ints
|
$size = array_map($cleanSnmpValue, $size);
|
||||||
$size = array_map($cleanSnmpValue, $size); // Ints
|
$used = array_map($cleanSnmpValue, $used);
|
||||||
$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);
|
$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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$ramUsedBytes = $units[$ramIndex] * $used[$ramIndex];
|
|
||||||
$ramPercent = ($memTotal > 0) ? ($ramUsedBytes / $memTotal) * 100 : 0;
|
|
||||||
|
|
||||||
|
// --- 6. Disk C: / Root mit Fallback-Logik ---
|
||||||
|
$diskCPercent = null;
|
||||||
|
|
||||||
// --- 5. Disk C: ---
|
// Heuristik 1: Suche nach 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:\\')) {
|
||||||
$cIndex = $index;
|
$cTotal = $units[$index] * $size[$index];
|
||||||
|
$cUsed = $units[$index] * $used[$index];
|
||||||
|
$diskCPercent = ($cTotal > 0) ? ($cUsed / $cTotal) * 100 : 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($cIndex === false) {
|
// Fallback 1: Suche nach "C:" oder "root" oder "/"
|
||||||
throw new RuntimeException("Konnte Laufwerk 'C:\' in der SNMP Storage-Tabelle nicht finden.");
|
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];
|
// Fallback 2: Nimm den größten Eintrag > 1GB (wahrscheinlich der Hauptdatenträger)
|
||||||
$cTotal = $units[$cIndex] * $size[$cIndex];
|
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 = [
|
$status = [
|
||||||
'hostname' => $host,
|
'hostname' => $host,
|
||||||
'os' => 'Windows Server 2025 Datacenter', // TODO: OS dynamisch abfragen
|
'os' => 'Windows Server', // TODO: OS dynamisch per SNMP abfragen (OID 1.3.6.1.2.1.1.1.0)
|
||||||
'uptime' => $uptimeFormatted, // Fehlende Variable hinzugefügt
|
'uptime' => $uptimeFormatted,
|
||||||
'cpu_usage' => round($cpuAvg),
|
'cpu_usage' => $cpuAvg,
|
||||||
'memory_usage' => round($ramPercent),
|
'memory_usage' => (int)round($ramPercent),
|
||||||
'disk_usage_c' => round($diskCPercent),
|
'disk_usage_c' => (int)round($diskCPercent),
|
||||||
'last_update' => date('d.m.Y H:i:s'),
|
'last_update' => date('d.m.Y H:i:s'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return $status;
|
return $status;
|
||||||
|
|||||||
292
public/api/snmp_status.php
Normal file
292
public/api/snmp_status.php
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
<?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;
|
||||||
@ -1,28 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View-Template für das Server-Dashboard.
|
* Server-Dashboard (Ansicht).
|
||||||
*
|
*
|
||||||
* Aufgaben:
|
* Zeigt Server-Kennzahlen an (Hostname, Uptime, CPU, RAM, Datenträger).
|
||||||
* - Visualisiert den vom SnmpServerStatusService gelieferten Serverstatus.
|
* Erwartetes Array: `$serverStatus` mit Schlüsseln: hostname, uptime, cpu_usage,
|
||||||
* - Zeigt Kennzahlen wie Hostname, Uptime, CPU-Auslastung, RAM-Auslastung
|
* memory_usage, disk_usage_c, last_update.
|
||||||
* 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">
|
||||||
@ -75,7 +70,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">
|
||||||
<?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?> %
|
<span id="cpu_card_value"><?php echo (int)($serverStatus['cpu_usage'] ?? 0); ?></span> %
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@ -96,7 +91,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">
|
||||||
<?php echo (int)($serverStatus['memory_usage'] ?? 0); ?> %
|
<span id="mem_card_value"><?php echo (int)($serverStatus['memory_usage'] ?? 0); ?></span> %
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@ -106,6 +101,85 @@ 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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user