Files
Virtnet.bond/extensions/Servers/Proxmox/Http/Controllers/LiveTrafficController.php
Joseph.Rawlings 5f2e591c6e feat: add LXC container support to Proxmox extension
- Add is_lxc boolean column to ext_proxmox_nodes (default false, existing nodes unaffected)
- Add LXC checkbox to admin node creation form (disabled on edit)
- Dynamic API path selection via Node::vmType() (returns 'lxc' or 'qemu')
- LXC deployment: syncLxcSettings() for cores, memory, swap, hostname, rootfs, net config
- LXC network uses name=eth0 format with applyLxcPoolSettings() for VLAN/firewall/MTU
- All lifecycle operations LXC-aware: install, suspend, unsuspend, terminate, upgrade, reinstall
- VNC/xterm console uses correct type (lxc vs kvm)
- All 20+ Livewire components use vmType() for Proxmox API paths
- LXC user interface shows basic-only view (no advanced toggle, no KVM-specific tabs)
- Basic tabs for LXC: Overview, Network Configuration, Backups, Console
2026-03-02 07:11:25 -05:00

371 lines
17 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 . '/' . $server->node->vmType() . '/' . $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 . '/' . $server->node->vmType() . '/' . $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 . '/' . $server->node->vmType() . '/' . $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 . '/' . $server->node->vmType() . '/' . $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 . '/' . $server->node->vmType() . '/' . $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' => 'An internal error occurred.',
], 500);
}
}
}