feat: implement available IPs loading in Proxmox Firewall component to streamline IP selection for firewall rules and enhance user experience

This commit is contained in:
2025-12-01 14:25:08 -05:00
parent 3b170e88ba
commit d320a5cdce
2 changed files with 21 additions and 68 deletions

View File

@@ -49,12 +49,26 @@ class Firewall extends Component
{
parent::mount($server);
$this->server->loadMissing('ipAddresses');
$this->loadAvailableIps();
$this->loadFirewallOptions();
$this->loadFirewallRules();
$this->loadAliases();
$this->loadIpsets();
$this->loadNetworkAdapters();
$this->loadFirewallLogs();
// Don't load logs on mount - only load when user clicks refresh
}
protected function loadAvailableIps(): void
{
$allIps = collect();
if ($this->server->primaryIpv4) {
$allIps->push(['ip' => $this->server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary']);
}
if ($this->server->primaryIpv6) {
$allIps->push(['ip' => $this->server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary']);
}
foreach ($this->server->ipAddresses as $ipAddress) {
$allIps->push(['ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Assigned']);
}
$this->availableIps = $allIps->unique('ip')->values()->toArray();
}
/**
@@ -197,19 +211,8 @@ class Firewall extends Component
$this->resetRuleForm();
// Set default destination to first IP address
$allIps = collect();
if ($this->server->primaryIpv4) {
$allIps->push($this->server->primaryIpv4->ip);
}
if ($this->server->primaryIpv6) {
$allIps->push($this->server->primaryIpv6->ip);
}
foreach ($this->server->ipAddresses as $ipAddress) {
$allIps->push($ipAddress->ip);
}
$uniqueIps = $allIps->unique();
if ($uniqueIps->isNotEmpty()) {
$this->ruleDest = $uniqueIps->first();
if (!empty($this->availableIps)) {
$this->ruleDest = $this->availableIps[0]['ip'];
}
// Set default interface to first network adapter
@@ -655,7 +658,6 @@ class Firewall extends Component
$segments = array_filter(array_map('trim', explode(',', $configString)));
$model = 'virtio';
$mac = null;
$bridge = null;
$firewallEnabled = false;
foreach ($segments as $segment) {
@@ -669,18 +671,15 @@ class Firewall extends Component
if (in_array($segmentKeyLower, ['virtio', 'e1000', 'e1000e', 'rtl8139', 'vmxnet3', 'pcnet'], true)) {
$model = $segmentKeyLower;
$mac = $segmentValue;
} elseif ($segmentKeyLower === 'bridge') {
$bridge = $segmentValue;
} elseif ($segmentKeyLower === 'firewall') {
$firewallEnabled = ($segmentValue == '1' || $segmentValue === '1');
}
}
$this->networkAdapters[$key] = [
$this->networkAdapters[] = [
'name' => $key,
'model' => $model,
'mac' => $mac,
'bridge' => $bridge,
];
$this->adapterFirewallStates[$key] = $firewallEnabled;
}
@@ -766,33 +765,23 @@ class Firewall extends Component
public function loadFirewallLogs(): void
{
try {
// Try the firewall log endpoint
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
'get',
['limit' => $this->logLimit]
);
// Check if response is successful
if (!$response->successful()) {
\Log::warning('Firewall log API returned non-success status', [
'status' => $response->status(),
'body' => $response->body()
]);
$this->firewallLogs = [];
return;
}
$responseData = $response->json();
// Proxmox firewall log API can return data in different formats
// Try multiple possible response structures
$rawLogs = [];
if (isset($responseData['data']) && is_array($responseData['data'])) {
$rawLogs = $responseData['data'];
} elseif (is_array($responseData) && !isset($responseData['data'])) {
// Response might be the array directly
$rawLogs = $responseData;
}
@@ -800,12 +789,10 @@ class Firewall extends Component
if (empty($rawLogs)) {
$body = $response->body();
if (!empty($body) && is_string($body)) {
// Try to parse as JSON first
$jsonData = json_decode($body, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($jsonData)) {
$rawLogs = $jsonData['data'] ?? $jsonData;
} else {
// If not JSON, try to split by newlines (plain text format)
$lines = explode("\n", trim($body));
$rawLogs = array_filter($lines, function($line) {
return !empty(trim($line));
@@ -814,25 +801,16 @@ class Firewall extends Component
}
}
\Log::debug('Firewall logs raw response', [
'raw_logs_count' => count($rawLogs),
'sample_entry' => !empty($rawLogs) ? (is_array($rawLogs[0]) ? $rawLogs[0] : substr($rawLogs[0], 0, 100)) : null
]);
$this->firewallLogs = [];
foreach ($rawLogs as $logEntry) {
$logText = null;
// Try different possible field names for the log text
if (is_string($logEntry)) {
// If the entry is a string directly, use it
$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) {
if (is_string($value) && !empty(trim($value))) {
@@ -847,24 +825,12 @@ class Firewall extends Component
continue;
}
// Parse the log entry to extract simplified information
$parsed = $this->parseFirewallLog($logText);
if ($parsed) {
$this->firewallLogs[] = $parsed;
}
}
\Log::debug('Firewall logs parsed', [
'parsed_count' => count($this->firewallLogs)
]);
} catch (\Exception $e) {
// Log the error for debugging
\Log::error('Failed to load firewall logs: ' . $e->getMessage(), [
'server_id' => $this->server->id,
'vm_id' => $this->server->vm_id,
'node' => $this->server->node->name ?? 'unknown',
'trace' => $e->getTraceAsString()
]);
$this->firewallLogs = [];
}
}

View File

@@ -261,20 +261,7 @@
<div>
<label class="block text-sm font-medium mb-2">Destination</label>
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
@php
$allIps = collect();
if ($server->primaryIpv4) {
$allIps->push(['ip' => $server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary']);
}
if ($server->primaryIpv6) {
$allIps->push(['ip' => $server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary']);
}
foreach ($server->ipAddresses as $ipAddress) {
$allIps->push(['ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Assigned']);
}
$uniqueIps = $allIps->unique('ip');
@endphp
@foreach($uniqueIps as $ipData)
@foreach($availableIps as $ipData)
<option value="{{ $ipData['ip'] }}">{{ $ipData['ip'] }} ({{ $ipData['type'] }} - {{ $ipData['label'] }})</option>
@endforeach
</select>