From 2329897941131f7f6c5e5fb37896c65910f481d0 Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Wed, 3 Dec 2025 03:45:54 -0500 Subject: [PATCH] refactor: enhance Proxmox Firewall log handling with improved pagination, batch fetching, and error handling for log retrieval --- .../Servers/Proxmox/Livewire/Firewall.php | 169 +++++++++++++----- .../Servers/Proxmox/views/firewall.blade.php | 99 +++++++++- 2 files changed, 222 insertions(+), 46 deletions(-) diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index b9d2a1be5..585c79539 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -21,7 +21,7 @@ class Firewall extends Component public array $networkAdapters = []; public array $adapterFirewallStates = []; public array $firewallLogs = []; - public int $logLimit = 100; + public int $logLimit = 1000; public array $availableIps = []; public bool $showDeleteModal = false; public ?int $deletingRulePos = null; @@ -108,7 +108,14 @@ class Firewall extends Component return $request->asForm()->$method($fullUrl, $data)->throw(); } - return $request->$method($fullUrl, $data)->throw(); + // For GET requests, if data is provided, it should be query parameters + if (strtolower($method) === 'get' && !empty($data)) { + $queryString = http_build_query($data); + $fullUrl .= (str_contains($fullUrl, '?') ? '&' : '?') . $queryString; + return $request->$method($fullUrl)->throw(); + } + + return $request->$method($fullUrl)->throw(); } public function loadFirewallOptions(): void @@ -799,54 +806,119 @@ class Firewall extends Component // Proxmox API: GET /api2/json/nodes/{node}/qemu/{vmid}/firewall/log // Documentation: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/firewall/log // Returns: text/plain format with log entries, one per line - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', - 'get', - [ - 'limit' => $this->logLimit, - 'start' => 0 - ] - ); + // Fetch logs in batches and collect all of them + $allRawLogs = []; + $start = 0; + $batchSize = 500; + $maxLogs = $this->logLimit; + $fetchedCount = 0; + + // Keep fetching until we have all logs or reach the limit + do { + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', + 'get', + [ + 'limit' => $batchSize, + 'start' => $start + ] + ); - if (! $response->successful()) { - $errorData = []; - try { - $errorData = $response->json(); - } catch (\Exception $e) { - // Response might not be JSON + if (! $response->successful()) { + $errorData = []; + try { + $errorData = $response->json(); + } catch (\Exception $e) { + // Response might not be JSON + } + $errorMsg = $errorData['message'] ?? 'HTTP ' . $response->status(); + $this->logError = 'Failed to fetch logs: ' . $errorMsg; + \Log::warning('Firewall log API request failed', [ + 'status' => $response->status(), + 'body' => substr($response->body(), 0, 500), + 'server_id' => $this->server->id, + 'vm_id' => $this->server->vm_id, + ]); + $this->firewallLogs = []; + return; } - $errorMsg = $errorData['message'] ?? 'HTTP ' . $response->status(); - $this->logError = 'Failed to fetch logs: ' . $errorMsg; - \Log::warning('Firewall log API request failed', [ - 'status' => $response->status(), - 'body' => substr($response->body(), 0, 500), - 'server_id' => $this->server->id, - 'vm_id' => $this->server->vm_id, - ]); - $this->firewallLogs = []; - return; - } - // Proxmox firewall log endpoint returns text/plain format - // Each log entry is on a new line - $body = $response->body(); - $rawLogs = []; - - if (! empty($body)) { - // Split by newlines (handle both \n and \r\n) - $lines = preg_split('/\r\n|\r|\n/', $body); - $rawLogs = array_filter($lines, static function ($line) { - return ! empty(trim($line)); - }); + // Proxmox firewall log endpoint can return either text/plain or JSON format + // Try to parse as JSON first, then fall back to text/plain + $body = $response->body(); + $batchRawLogs = []; + + // Check if response is JSON + $contentType = $response->header('Content-Type'); + $isJson = str_contains($contentType ?? '', 'json'); + + if ($isJson) { + try { + $jsonData = $response->json(); + // Proxmox may return logs in different formats: + // 1. { "data": [ {line}, {line}, ... ] } + // 2. { "data": "line1\nline2\n..." } + // 3. [ {line}, {line}, ... ] + + if (isset($jsonData['data'])) { + if (is_array($jsonData['data'])) { + $batchRawLogs = $jsonData['data']; + } elseif (is_string($jsonData['data'])) { + $lines = preg_split('/\r\n|\r|\n/', $jsonData['data']); + $batchRawLogs = array_filter($lines, static function ($line) { + return ! empty(trim($line)); + }); + } + } elseif (is_array($jsonData)) { + $batchRawLogs = $jsonData; + } + } catch (\Exception $e) { + // Fall through to text parsing + \Log::warning('Failed to parse JSON response, falling back to text', [ + 'error' => $e->getMessage() + ]); + } + } + + // If no JSON data or JSON parsing failed, try text/plain format + if (empty($batchRawLogs) && ! empty($body)) { + // Split by newlines (handle both \n and \r\n) + $lines = preg_split('/\r\n|\r|\n/', $body); + $batchRawLogs = array_filter($lines, static function ($line) { + return ! empty(trim($line)); + }); + } + // Re-index array after filtering - $rawLogs = array_values($rawLogs); + $batchRawLogs = array_values($batchRawLogs); + + // Add this batch to all logs + $allRawLogs = array_merge($allRawLogs, $batchRawLogs); + $fetchedCount = count($batchRawLogs); + + // Move to next batch + $start += $batchSize; + + // Break if we got fewer logs than requested (meaning we've reached the end) + // or if we've reached our limit + if ($fetchedCount < $batchSize || count($allRawLogs) >= $maxLogs) { + break; + } + } while (true); + + // Limit to maxLogs + if (count($allRawLogs) > $maxLogs) { + $allRawLogs = array_slice($allRawLogs, 0, $maxLogs); } + + $rawLogs = $allRawLogs; \Log::info('Firewall logs fetched from Proxmox API', [ 'raw_count' => count($rawLogs), 'body_length' => strlen($body), - 'content_type' => $response->header('Content-Type'), - 'sample' => ! empty($rawLogs) ? substr($rawLogs[array_key_first($rawLogs)], 0, 200) : null, + 'content_type' => $contentType, + 'is_json' => $isJson, + 'sample' => ! empty($rawLogs) ? substr(is_array($rawLogs[0]) ? json_encode($rawLogs[0]) : $rawLogs[0], 0, 200) : null, 'first_5_lines' => array_slice($rawLogs, 0, 5), ]); @@ -858,6 +930,7 @@ class Firewall extends Component if (is_string($logEntry)) { $logText = trim($logEntry); } elseif (is_array($logEntry)) { + // Handle various possible array formats from Proxmox API $logText = trim( $logEntry['raw'] ?? $logEntry['n'] @@ -870,11 +943,21 @@ class Firewall extends Component ); } - if ($logText === '') { + if ($logText === '' || strlen($logText) < 10) { continue; } - $this->firewallLogs[] = $this->parseFirewallLog($logText); + try { + $parsed = $this->parseFirewallLog($logText); + if ($parsed !== null) { + $this->firewallLogs[] = $parsed; + } + } catch (\Exception $e) { + \Log::warning('Failed to parse firewall log entry', [ + 'entry' => substr($logText, 0, 200), + 'error' => $e->getMessage() + ]); + } } \Log::info('Firewall logs final count', [ diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php b/extensions/Servers/Proxmox/views/firewall.blade.php index 2c7dcc1a7..81cccbab6 100644 --- a/extensions/Servers/Proxmox/views/firewall.blade.php +++ b/extensions/Servers/Proxmox/views/firewall.blade.php @@ -328,6 +328,8 @@ searchTerm: '', sortField: 'timestamp_iso', sortDirection: 'desc', + currentPage: 1, + perPage: 10, initialize(initialLogs) { console.log('Initializing logs, received:', initialLogs ? initialLogs.length : 0, 'entries'); this.logs = (initialLogs || []).map((log, index) => ({ @@ -343,6 +345,7 @@ this.sortField = field; this.sortDirection = 'asc'; } + this.currentPage = 1; // Reset to first page when sorting }, get filteredLogs() { const term = this.searchTerm.trim().toLowerCase(); @@ -379,8 +382,59 @@ return String(aVal).localeCompare(String(bVal)) * dir; }); }, + get paginatedLogs() { + const filtered = this.filteredLogs; + const start = (this.currentPage - 1) * this.perPage; + const end = start + this.perPage; + return filtered.slice(start, end); + }, + get totalPages() { + return Math.ceil(this.filteredLogs.length / this.perPage); + }, + get paginationRange() { + const total = this.totalPages; + const current = this.currentPage; + const delta = 2; + const range = []; + + for (let i = Math.max(2, current - delta); i <= Math.min(total - 1, current + delta); i++) { + range.push(i); + } + + if (current - delta > 2) { + range.unshift('...'); + } + if (current + delta < total - 1) { + range.push('...'); + } + + if (total > 0) { + range.unshift(1); + } + if (total > 1) { + range.push(total); + } + + return range; + }, + goToPage(page) { + if (page >= 1 && page <= this.totalPages) { + this.currentPage = page; + } + }, + nextPage() { + if (this.currentPage < this.totalPages) { + this.currentPage++; + } + }, + prevPage() { + if (this.currentPage > 1) { + this.currentPage--; + } + } }" x-init="initialize(@js($firewallLogs))" + x-effect="if (currentPage > totalPages && totalPages > 0) currentPage = totalPages" class="space-y-4" >
@@ -389,12 +443,18 @@
-
- +
+
+ +
+
+ +
@@ -453,7 +513,7 @@ -