diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index e8e2e856e..9af206d68 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -24,6 +24,7 @@ class Firewall extends Component public array $availableIps = []; public bool $showDeleteModal = false; public ?int $deletingRulePos = null; + public string $logError = ''; // Rule creation/editing public ?int $editingRulePos = null; @@ -89,9 +90,14 @@ class Firewall extends Component // Proxmox API token format: PVEAPIToken=USER@REALM!TOKENID=SECRET $authHeader = 'PVEAPIToken=' . $user . '!' . $tokenId . '=' . $secret; + // For firewall log endpoint, accept text/plain as well + $acceptHeader = str_contains($url, '/firewall/log') + ? 'application/json, text/plain, */*' + : 'application/json'; + $request = Http::withHeaders([ 'Authorization' => $authHeader, - 'Accept' => 'application/json', + 'Accept' => $acceptHeader, ])->withOptions([ 'verify' => true, ]); @@ -787,7 +793,9 @@ class Firewall extends Component public function loadFirewallLogs(): void { + $this->logError = ''; try { + // Try the firewall log endpoint - Proxmox returns logs as text/plain, not JSON $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', 'get', @@ -795,35 +803,52 @@ class Firewall extends Component ); if (!$response->successful()) { + $errorData = $response->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' => $response->body(), + 'server_id' => $this->server->id, + 'vm_id' => $this->server->vm_id + ]); $this->firewallLogs = []; return; } - $responseData = $response->json(); + // Proxmox firewall log endpoint returns plain text, not JSON + $body = $response->body(); $rawLogs = []; - if (isset($responseData['data']) && is_array($responseData['data'])) { - $rawLogs = $responseData['data']; - } elseif (is_array($responseData) && !isset($responseData['data'])) { - $rawLogs = $responseData; + if (!empty($body)) { + // Split by newlines and filter empty lines + $lines = explode("\n", trim($body)); + $rawLogs = array_filter($lines, function($line) { + return !empty(trim($line)); + }); } - // If still empty, try to get logs from the response body as text + // If we got JSON instead, try to parse it if (empty($rawLogs)) { - $body = $response->body(); - if (!empty($body) && is_string($body)) { - $jsonData = json_decode($body, true); - if (json_last_error() === JSON_ERROR_NONE && is_array($jsonData)) { - $rawLogs = $jsonData['data'] ?? $jsonData; - } else { - $lines = explode("\n", trim($body)); - $rawLogs = array_filter($lines, function($line) { - return !empty(trim($line)); - }); + try { + $responseData = $response->json(); + if (isset($responseData['data']) && is_array($responseData['data'])) { + $rawLogs = $responseData['data']; + } elseif (is_array($responseData) && !isset($responseData['data'])) { + $rawLogs = $responseData; } + } catch (\Exception $jsonError) { + // Not JSON, that's fine - we already tried plain text } } + \Log::debug('Firewall logs fetched', [ + 'raw_count' => count($rawLogs), + 'body_length' => strlen($body), + 'content_type' => $response->header('Content-Type'), + 'sample' => !empty($rawLogs) ? (is_array($rawLogs[0]) ? json_encode($rawLogs[0]) : substr($rawLogs[0], 0, 200)) : null + ]); + $this->firewallLogs = []; foreach ($rawLogs as $logEntry) { @@ -832,10 +857,12 @@ class Firewall extends Component if (is_string($logEntry)) { $logText = trim($logEntry); } elseif (is_array($logEntry)) { + // Try common field names $logText = $logEntry['n'] ?? $logEntry['t'] ?? $logEntry['text'] ?? $logEntry['log'] ?? $logEntry['message'] ?? $logEntry['line'] ?? ''; + // If still empty, try to get the first string value if (empty($logText)) { - foreach ($logEntry as $value) { + foreach ($logEntry as $key => $value) { if (is_string($value) && !empty(trim($value))) { $logText = trim($value); break; @@ -853,7 +880,31 @@ class Firewall extends Component $this->firewallLogs[] = $parsed; } } + + if (empty($this->firewallLogs) && !empty($rawLogs)) { + $this->logError = 'Logs received but could not be parsed. Please check that firewall logging is enabled in Proxmox.'; + } elseif (empty($this->firewallLogs)) { + $this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).'; + } + + \Log::debug('Firewall logs parsed', [ + 'parsed_count' => count($this->firewallLogs) + ]); } catch (\Exception $e) { + $errorMessage = $e->getMessage(); + if (method_exists($e, 'response') && $e->response()) { + $errorData = $e->response()->json(); + if (isset($errorData['message'])) { + $errorMessage = $errorData['message']; + } + } + $this->logError = 'Error loading logs: ' . $errorMessage; + \Log::error('Failed to load firewall logs', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'server_id' => $this->server->id, + 'vm_id' => $this->server->vm_id + ]); $this->firewallLogs = []; } } diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php b/extensions/Servers/Proxmox/views/firewall.blade.php index 0b321f8e1..fdd371dc0 100644 --- a/extensions/Servers/Proxmox/views/firewall.blade.php +++ b/extensions/Servers/Proxmox/views/firewall.blade.php @@ -307,6 +307,17 @@ + @if(!empty($logError)) +
+
+ +
+

{{ $logError }}

+
+
+
+ @endif + @if(count($firewallLogs) > 0)
@@ -347,10 +358,11 @@
- @else + @elseif(empty($logError))

No firewall logs available

+

Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options)

@endif