371 lines
16 KiB
PHP
371 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Servers\Proxmox\Http\Controllers;
|
|
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Routing\Controller;
|
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
|
|
|
class LiveTrafficController extends Controller
|
|
{
|
|
private const RATE_SCALE = 100 * 1024 * 1024; // 100 MiB/s default scale
|
|
|
|
public function __invoke(Server $server): JsonResponse
|
|
{
|
|
abort_if(auth()->guest() || (int) $server->user_id !== (int) auth()->id(), 403);
|
|
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
|
|
// First, get the VM configuration to identify network interfaces
|
|
$configResponse = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/config'
|
|
);
|
|
$config = $configResponse->json()['data'] ?? [];
|
|
|
|
// Extract network interfaces from config - only show net0
|
|
$interfaces = [];
|
|
// Only include net0 interface
|
|
$key = "net0";
|
|
if (isset($config[$key])) {
|
|
// Parse the network config string
|
|
$netConfig = $config[$key];
|
|
preg_match('/bridge=([^,]+)/', $netConfig, $bridgeMatch);
|
|
preg_match('/^([^=]+)/', $netConfig, $modelMatch);
|
|
|
|
$interfaces[] = [
|
|
'id' => $key,
|
|
'name' => $key,
|
|
'bridge' => $bridgeMatch[1] ?? 'unknown',
|
|
'model' => $modelMatch[1] ?? 'virtio',
|
|
];
|
|
}
|
|
|
|
// Try to get guest agent network data for per-interface statistics
|
|
// This provides actual per-interface data when RRD doesn't have it
|
|
$guestAgentInterfaces = [];
|
|
try {
|
|
$guestResponse = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/agent/network-get-interfaces'
|
|
);
|
|
$guestJson = $guestResponse->json();
|
|
$guestAgentInterfaces = $guestJson['data']['result'] ?? [];
|
|
} catch (\Throwable $e) {
|
|
// Guest agent not available - continue without it
|
|
$guestAgentInterfaces = [];
|
|
}
|
|
|
|
// Map guest agent interfaces by name for easier lookup
|
|
// Guest agent interfaces are typically named eth0, eth1, ens3, etc.
|
|
$guestInterfaceMap = [];
|
|
foreach ($guestAgentInterfaces as $guestIf) {
|
|
$name = $guestIf['name'] ?? '';
|
|
if ($name) {
|
|
$guestInterfaceMap[$name] = $guestIf;
|
|
}
|
|
}
|
|
|
|
// Get RRD data once for all interfaces (more efficient)
|
|
$rrdResponse = null;
|
|
$rrdRows = null;
|
|
|
|
if (count($interfaces) > 0) {
|
|
try {
|
|
$rrdResponse = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/rrddata',
|
|
'get',
|
|
[
|
|
'timeframe' => 'hour',
|
|
'cf' => 'AVERAGE',
|
|
]
|
|
);
|
|
|
|
$rrdJson = $rrdResponse->json();
|
|
$rrdRows = $rrdJson['data'] ?? [];
|
|
} catch (\Throwable $e) {
|
|
// If RRD request fails, we'll handle it per interface
|
|
$rrdRows = [];
|
|
}
|
|
}
|
|
|
|
// Get RRD data for each interface
|
|
$interfaceData = [];
|
|
$totalInRate = 0;
|
|
$totalOutRate = 0;
|
|
|
|
foreach ($interfaces as $interface) {
|
|
try {
|
|
// RRD data uses netin0, netout0, netin1, netout1, etc.
|
|
$ifNum = str_replace('net', '', $interface['id']);
|
|
$dsNames = "netin{$ifNum},netout{$ifNum}";
|
|
|
|
$rows = $rrdRows;
|
|
|
|
if (!is_array($rows) || count($rows) === 0) {
|
|
// If no RRD data available, still create interface entry with zero rates
|
|
$interfaceData[] = [
|
|
'id' => $interface['id'],
|
|
'name' => 'Network Statistics',
|
|
'bridge' => $interface['bridge'],
|
|
'model' => $interface['model'],
|
|
'downloadRate' => 0,
|
|
'uploadRate' => 0,
|
|
'totalRate' => 0,
|
|
'totalIn' => 0,
|
|
'totalOut' => 0,
|
|
'history' => [
|
|
'download' => array_fill(0, 20, 0),
|
|
'upload' => array_fill(0, 20, 0),
|
|
],
|
|
];
|
|
continue;
|
|
}
|
|
|
|
// Get the latest row with valid data
|
|
$currentRow = null;
|
|
$netinKey = "netin{$ifNum}";
|
|
$netoutKey = "netout{$ifNum}";
|
|
|
|
// Try to find interface-specific data first
|
|
foreach (array_reverse($rows) as $row) {
|
|
if (isset($row[$netinKey], $row[$netoutKey])) {
|
|
$currentRow = $row;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If interface-specific data not found, use aggregate data as fallback for net0 only
|
|
// For other interfaces, use zeros to ensure each interface has unique data
|
|
if (!$currentRow) {
|
|
if ($ifNum === '0') {
|
|
// For net0, use aggregate data as fallback (since aggregate typically = net0)
|
|
foreach (array_reverse($rows) as $row) {
|
|
if (isset($row['netin'], $row['netout'])) {
|
|
$currentRow = $row;
|
|
$currentRow[$netinKey] = $row['netin'];
|
|
$currentRow[$netoutKey] = $row['netout'];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// For other interfaces or if net0 fallback failed, use zeros
|
|
if (!$currentRow) {
|
|
$currentRow = [$netinKey => 0, $netoutKey => 0];
|
|
}
|
|
}
|
|
|
|
$inRate = (float) ($currentRow[$netinKey] ?? 0);
|
|
$outRate = (float) ($currentRow[$netoutKey] ?? 0);
|
|
|
|
$totalInRate += $inRate;
|
|
$totalOutRate += $outRate;
|
|
|
|
// Calculate history for sparkline (last 20 samples)
|
|
$history = [
|
|
'download' => [],
|
|
'upload' => []
|
|
];
|
|
|
|
$sampleCount = min(20, count($rows));
|
|
$samples = array_slice($rows, -$sampleCount);
|
|
|
|
// Check if interface-specific keys exist in ANY sample
|
|
// This is more lenient - if the keys exist in some samples, we'll use them
|
|
$hasInterfaceSpecificData = false;
|
|
foreach ($samples as $sample) {
|
|
if (isset($sample[$netinKey]) || isset($sample[$netoutKey])) {
|
|
$hasInterfaceSpecificData = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Use interface-specific data if available, otherwise use zeros or aggregate for net0 only
|
|
// This ensures each interface has its own unique graph data (not duplicated)
|
|
foreach ($samples as $sample) {
|
|
if ($hasInterfaceSpecificData && isset($sample[$netinKey], $sample[$netoutKey])) {
|
|
// Use interface-specific data when available
|
|
$history['download'][] = (float) $sample[$netinKey];
|
|
$history['upload'][] = (float) $sample[$netoutKey];
|
|
} else {
|
|
// For net0, use aggregate data as fallback (since aggregate typically = net0)
|
|
// For other interfaces, use zeros to ensure unique graphs per interface
|
|
if ($ifNum === '0') {
|
|
$history['download'][] = (float) ($sample['netin'] ?? 0);
|
|
$history['upload'][] = (float) ($sample['netout'] ?? 0);
|
|
} else {
|
|
// Use zeros for other interfaces without specific data
|
|
// This ensures each interface has its own unique graph (even if zero)
|
|
$history['download'][] = 0.0;
|
|
$history['upload'][] = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get aggregate totals for net0 fallback (will be set later from status endpoint)
|
|
// For now, set per-interface totals
|
|
$totalIn = 0;
|
|
$totalOut = 0;
|
|
|
|
// For net0, we'll use aggregate totals (set later)
|
|
// For other interfaces, use zeros to ensure uniqueness
|
|
if ($ifNum !== '0') {
|
|
$totalIn = 0;
|
|
$totalOut = 0;
|
|
}
|
|
// Note: net0 totals will be set from aggregate data after status endpoint call
|
|
|
|
$interfaceData[] = [
|
|
'id' => $interface['id'],
|
|
'name' => 'Network Statistics',
|
|
'bridge' => $interface['bridge'],
|
|
'model' => $interface['model'],
|
|
'downloadRate' => $inRate,
|
|
'uploadRate' => $outRate,
|
|
'totalRate' => $inRate + $outRate,
|
|
'totalIn' => $totalIn,
|
|
'totalOut' => $totalOut,
|
|
'history' => $history,
|
|
];
|
|
|
|
} catch (\Throwable $e) {
|
|
// Skip this interface if there's an error
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If no interface-specific data, fall back to general RRD data
|
|
if (empty($interfaceData)) {
|
|
$response = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/rrddata',
|
|
'get',
|
|
[
|
|
'timeframe' => 'hour',
|
|
'cf' => 'AVERAGE',
|
|
]
|
|
);
|
|
|
|
$json = $response->json();
|
|
$rows = $json['data'] ?? [];
|
|
|
|
if (is_array($rows) && count($rows) > 0) {
|
|
$currentRow = null;
|
|
foreach (array_reverse($rows) as $row) {
|
|
if (isset($row['netin'], $row['netout'])) {
|
|
$currentRow = $row;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($currentRow) {
|
|
$inRate = (float) ($currentRow['netin'] ?? 0);
|
|
$outRate = (float) ($currentRow['netout'] ?? 0);
|
|
|
|
// Calculate history
|
|
$history = [
|
|
'download' => [],
|
|
'upload' => []
|
|
];
|
|
|
|
$sampleCount = min(20, count($rows));
|
|
$samples = array_slice($rows, -$sampleCount);
|
|
|
|
foreach ($samples as $sample) {
|
|
$history['download'][] = (float) ($sample['netin'] ?? 0);
|
|
$history['upload'][] = (float) ($sample['netout'] ?? 0);
|
|
}
|
|
|
|
$interfaceData[] = [
|
|
'id' => 'net0',
|
|
'name' => 'Network Statistics',
|
|
'bridge' => 'default',
|
|
'model' => 'virtio',
|
|
'downloadRate' => $inRate,
|
|
'uploadRate' => $outRate,
|
|
'totalRate' => $inRate + $outRate,
|
|
'totalIn' => 0, // Will be set from aggregate data later
|
|
'totalOut' => 0, // Will be set from aggregate data later
|
|
'history' => $history,
|
|
];
|
|
|
|
$totalInRate = $inRate;
|
|
$totalOutRate = $outRate;
|
|
}
|
|
}
|
|
}
|
|
|
|
$timestamp = now();
|
|
|
|
// Calculate dynamic scale based on max rate
|
|
$maxRate = max($totalInRate, $totalOutRate);
|
|
$scale = self::RATE_SCALE;
|
|
|
|
if ($maxRate > 0) {
|
|
// Round up to nice numbers
|
|
if ($maxRate > 1000 * 1024 * 1024) { // > 1 GiB/s
|
|
$scale = ceil($maxRate / (100 * 1024 * 1024)) * (100 * 1024 * 1024);
|
|
} elseif ($maxRate > 100 * 1024 * 1024) { // > 100 MiB/s
|
|
$scale = ceil($maxRate / (50 * 1024 * 1024)) * (50 * 1024 * 1024);
|
|
} elseif ($maxRate > 10 * 1024 * 1024) { // > 10 MiB/s
|
|
$scale = ceil($maxRate / (10 * 1024 * 1024)) * (10 * 1024 * 1024);
|
|
} else {
|
|
$scale = max(10 * 1024 * 1024, ceil($maxRate / (1024 * 1024)) * (1024 * 1024));
|
|
}
|
|
}
|
|
|
|
// Get total statistics from status endpoint
|
|
$statusResponse = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/status/current'
|
|
);
|
|
$status = $statusResponse->json()['data'] ?? [];
|
|
|
|
$totalIn = (float) ($status['netin'] ?? 0);
|
|
$totalOut = (float) ($status['netout'] ?? 0);
|
|
|
|
// Update net0 totals with aggregate data (since aggregate = net0 typically)
|
|
// Other interfaces keep zeros to ensure uniqueness
|
|
foreach ($interfaceData as &$interface) {
|
|
if ($interface['id'] === 'net0') {
|
|
$interface['totalIn'] = $totalIn;
|
|
$interface['totalOut'] = $totalOut;
|
|
}
|
|
}
|
|
unset($interface); // Break reference
|
|
|
|
return response()->json([
|
|
'interfaces' => $interfaceData,
|
|
'total' => [
|
|
'downloadRate' => $totalInRate,
|
|
'uploadRate' => $totalOutRate,
|
|
'combinedRate' => $totalInRate + $totalOutRate,
|
|
'downloadTotal' => $totalIn,
|
|
'uploadTotal' => $totalOut,
|
|
'combinedTotal' => $totalIn + $totalOut,
|
|
],
|
|
'scale' => $scale,
|
|
'updatedAt' => $timestamp->toIso8601String(),
|
|
'timestamp' => $timestamp->timestamp,
|
|
]);
|
|
|
|
} catch (\Throwable $e) {
|
|
// Fallback: return empty data
|
|
return response()->json([
|
|
'interfaces' => [],
|
|
'total' => [
|
|
'downloadRate' => 0,
|
|
'uploadRate' => 0,
|
|
'combinedRate' => 0,
|
|
'downloadTotal' => 0,
|
|
'uploadTotal' => 0,
|
|
'combinedTotal' => 0,
|
|
],
|
|
'scale' => self::RATE_SCALE,
|
|
'updatedAt' => now()->toIso8601String(),
|
|
'timestamp' => now()->timestamp,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
} |