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(), ]); } } }