diff --git a/extensions/Servers/Proxmox/Http/Controllers/LiveTrafficController.php b/extensions/Servers/Proxmox/Http/Controllers/LiveTrafficController.php index 6e04b54da..ef7b5cdec 100644 --- a/extensions/Servers/Proxmox/Http/Controllers/LiveTrafficController.php +++ b/extensions/Servers/Proxmox/Http/Controllers/LiveTrafficController.php @@ -3,6 +3,7 @@ namespace Paymenter\Extensions\Servers\Proxmox\Http\Controllers; use Carbon\Carbon; +use Illuminate\Http\Client\RequestException; use Illuminate\Http\JsonResponse; use Illuminate\Routing\Controller; use Paymenter\Extensions\Servers\Proxmox\Models\Server; @@ -25,16 +26,17 @@ class LiveTrafficController extends Controller ); $config = $configResponse->json()['data'] ?? []; - // Extract network interfaces from config - only show net0 + // Extract network interfaces from config $interfaces = []; - // Only include net0 interface - $key = "net0"; - if (isset($config[$key])) { - // Parse the network config string - $netConfig = $config[$key]; + foreach ($config as $key => $value) { + if (!preg_match('/^net\d+$/', (string) $key)) { + continue; + } + + $netConfig = (string) $value; preg_match('/bridge=([^,]+)/', $netConfig, $bridgeMatch); preg_match('/^([^=]+)/', $netConfig, $modelMatch); - + $interfaces[] = [ 'id' => $key, 'name' => $key, @@ -43,30 +45,6 @@ class LiveTrafficController extends Controller ]; } - // 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; @@ -90,6 +68,9 @@ class LiveTrafficController extends Controller } } + usort($interfaces, static fn (array $a, array $b) => strcmp($a['id'], $b['id'])); + $primaryInterfaceId = $interfaces[0]['id'] ?? 'net0'; + // Get RRD data for each interface $interfaceData = []; $totalInRate = 0; @@ -139,7 +120,7 @@ class LiveTrafficController extends Controller // 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') { + if ($interface['id'] === $primaryInterfaceId) { // For net0, use aggregate data as fallback (since aggregate typically = net0) foreach (array_reverse($rows) as $row) { if (isset($row['netin'], $row['netout'])) { @@ -192,7 +173,7 @@ class LiveTrafficController extends Controller } 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') { + if ($interface['id'] === $primaryInterfaceId) { $history['download'][] = (float) ($sample['netin'] ?? 0); $history['upload'][] = (float) ($sample['netout'] ?? 0); } else { @@ -211,7 +192,7 @@ class LiveTrafficController extends Controller // For net0, we'll use aggregate totals (set later) // For other interfaces, use zeros to ensure uniqueness - if ($ifNum !== '0') { + if ($interface['id'] !== $primaryInterfaceId) { $totalIn = 0; $totalOut = 0; } @@ -238,17 +219,7 @@ class LiveTrafficController extends Controller // 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'] ?? []; + $rows = is_array($rrdRows) ? $rrdRows : []; if (is_array($rows) && count($rows) > 0) { $currentRow = null; @@ -324,10 +295,10 @@ class LiveTrafficController extends Controller $totalIn = (float) ($status['netin'] ?? 0); $totalOut = (float) ($status['netout'] ?? 0); - // Update net0 totals with aggregate data (since aggregate = net0 typically) + // Update the first configured interface totals with aggregate data // Other interfaces keep zeros to ensure uniqueness foreach ($interfaceData as &$interface) { - if ($interface['id'] === 'net0') { + if ($interface['id'] === $primaryInterfaceId) { $interface['totalIn'] = $totalIn; $interface['totalOut'] = $totalOut; } @@ -349,8 +320,24 @@ class LiveTrafficController extends Controller 'timestamp' => $timestamp->timestamp, ]); + } catch (RequestException $e) { + 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' => 'Unable to load live traffic from hypervisor.', + 'code' => 'proxmox_request_failed', + ], 502); } catch (\Throwable $e) { - // Fallback: return empty data return response()->json([ 'interfaces' => [], 'total' => [ @@ -365,6 +352,7 @@ class LiveTrafficController extends Controller 'updatedAt' => now()->toIso8601String(), 'timestamp' => now()->timestamp, 'error' => 'An internal error occurred.', + 'code' => 'internal_error', ], 500); } } diff --git a/extensions/Servers/Proxmox/Livewire/Backups/Create.php b/extensions/Servers/Proxmox/Livewire/Backups/Create.php index dc80ced73..4d073c4be 100644 --- a/extensions/Servers/Proxmox/Livewire/Backups/Create.php +++ b/extensions/Servers/Proxmox/Livewire/Backups/Create.php @@ -9,6 +9,7 @@ use Paymenter\Extensions\Servers\Proxmox\Proxmox; class Create extends Component { private const BACKUP_RATE_LIMIT_MINUTES = 10; + private const BACKUP_COUNT_CACHE_SECONDS = 60; public $visible; public $name; @@ -37,14 +38,7 @@ class Create extends Component } try { - $proxmox = new Proxmox; - $currentBackups = $proxmox->request('/nodes/' . $this->server->node->name . '/storage/' . $this->server->node->backup_location . '/content', data: [ - 'content' => 'backup', - 'vmid' => $this->server->vm_id, - ])->json()['data'] ?? []; - - $currentBackupCount = count($currentBackups); - + $currentBackupCount = $this->getCurrentBackupCount(); return $currentBackupCount < $backupLimit; } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Failed to check backup limit: ' . $e->getMessage()); @@ -54,6 +48,12 @@ class Create extends Component public function backup() { + $lock = Cache::lock($this->getBackupRateLimitLockKey(), 15); + if (!$lock->get()) { + $this->notify('A backup request is already being processed. Please wait a moment.', 'error'); + return; + } + $this->validate([ 'name' => 'required|string|max:255', 'type' => 'required|in:snapshot,suspend,stop', @@ -76,62 +76,65 @@ class Create extends Component $backupSettings = $this->server->service->product->settings()->where('key', 'backups')->first(); $backupLimit = (int) ($backupSettings?->value ?? 0); - if ($backupLimit > 0 && !empty($this->server->node->backup_location)) { - try { - $proxmox = new Proxmox; - $currentBackups = $proxmox->request('/nodes/' . $this->server->node->name . '/storage/' . $this->server->node->backup_location . '/content', data: [ - 'content' => 'backup', - 'vmid' => $this->server->vm_id, - ])->json()['data'] ?? []; + try { + if ($backupLimit > 0 && !empty($this->server->node->backup_location)) { + try { + $currentBackupCount = $this->getCurrentBackupCount(); - $currentBackupCount = count($currentBackups); - - if ($currentBackupCount >= $backupLimit) { - $this->notify("Backup limit reached. You can only create {$backupLimit} backup(s). Please delete an existing backup before creating a new one.", 'error'); + if ($currentBackupCount >= $backupLimit) { + $this->notify("Backup limit reached. You can only create {$backupLimit} backup(s). Please delete an existing backup before creating a new one.", 'error'); + return; + } + } catch (\Exception $e) { + report($e); + $this->notify('Failed to verify backup limit. Please try again.', 'error'); return; } - } catch (\Exception $e) { - $this->notify('Failed to check backup limit: ' . $e->getMessage(), 'error'); + } + + if (empty($this->server->node->backup_location)) { + $this->notify('Backup storage location is not configured.', 'error'); return; } - } - if (empty($this->server->node->backup_location)) { - $this->notify('Backup storage location is not configured.', 'error'); - return; - } + $proxmox = new Proxmox; - $proxmox = new Proxmox; + try { + $proxmox->request('/nodes/' . $this->server->node->name . '/vzdump', 'post', data: [ + 'vmid' => $this->server->vm_id, + 'storage' => $this->server->node->backup_location, + 'mode' => $this->type, + 'compress' => $this->compression == 'none' ? (int) false : $this->compression, + 'notes-template' => $this->name, + ]); + } catch (\Throwable $e) { + report($e); + $this->notify('Failed to start backup. Please try again in a moment.', 'error'); + return; + } - try { - $proxmox->request('/nodes/' . $this->server->node->name . '/vzdump', 'post', data: [ - 'vmid' => $this->server->vm_id, - 'storage' => $this->server->node->backup_location, - 'mode' => $this->type, - 'compress' => $this->compression == 'none' ? (int) false : $this->compression, - 'notes-template' => $this->name, + Cache::put( + $this->getBackupRateLimitCacheKey(), + now()->timestamp, + now()->addMinutes(self::BACKUP_RATE_LIMIT_MINUTES + 1) + ); + + Cache::forget($this->getBackupCountCacheKey()); + + $this->notify('Backup started', 'success'); + $this->dispatch('backup-started'); + + $this->visible = false; + + $this->reset([ + 'name', + 'type', + 'compression', ]); - } catch (\Throwable $e) { - $this->notify('Failed to start backup: ' . $e->getMessage(), 'error'); - return; + } finally { + $lock->release(); } - Cache::put( - $this->getBackupRateLimitCacheKey(), - now()->timestamp, - now()->addMinutes(self::BACKUP_RATE_LIMIT_MINUTES + 1) - ); - - $this->notify('Backup started', 'success'); - - $this->visible = false; - - $this->reset([ - 'name', - 'type', - 'compression', - ]); - } public function render() @@ -180,6 +183,29 @@ class Create extends Component return 'proxmox-backup-last-create-' . $this->server->id; } + private function getBackupRateLimitLockKey(): string + { + return 'proxmox-backup-create-lock-' . $this->server->id; + } + + private function getBackupCountCacheKey(): string + { + return 'proxmox-backup-count-' . $this->server->id; + } + + private function getCurrentBackupCount(): int + { + return (int) Cache::remember($this->getBackupCountCacheKey(), now()->addSeconds(self::BACKUP_COUNT_CACHE_SECONDS), function () { + $proxmox = new Proxmox; + $currentBackups = $proxmox->request('/nodes/' . $this->server->node->name . '/storage/' . $this->server->node->backup_location . '/content', data: [ + 'content' => 'backup', + 'vmid' => $this->server->vm_id, + ])->json()['data'] ?? []; + + return count($currentBackups); + }); + } + private function formatDurationForHumans(int $seconds): string { if ($seconds <= 0) { diff --git a/extensions/Servers/Proxmox/Livewire/Backups/Index.php b/extensions/Servers/Proxmox/Livewire/Backups/Index.php index 0a68c95dd..582007ac8 100644 --- a/extensions/Servers/Proxmox/Livewire/Backups/Index.php +++ b/extensions/Servers/Proxmox/Livewire/Backups/Index.php @@ -22,11 +22,17 @@ class Index extends Component public $volid; + public function promptRestore(string $volid): void + { + $this->volid = $volid; + $this->confirmRestore = false; + $this->showRestore = true; + } + public function mount(Server $server): void { parent::mount($server); - $proxmox = new Proxmox; $settings = $this->server->service->product->settings()->where('key', 'backups')->first(); if ($settings?->value <= 0) { $this->redirect(route('proxmox.server.overview', $this->server->id), true); @@ -40,7 +46,24 @@ class Index extends Component return; } + $this->loadBackups(); + } + + #[\Livewire\Attributes\On('backup-started')] + public function handleBackupStarted(): void + { + $this->loadBackups(); + } + + public function loadBackups(): void + { + if (empty($this->server->node->backup_location)) { + $this->backups = []; + return; + } + try { + $proxmox = new Proxmox; $response = $proxmox->request('/nodes/' . $this->server->node->name . '/storage/' . $this->server->node->backup_location . '/content', data: [ 'content' => 'backup', 'vmid' => $this->server->vm_id, @@ -78,28 +101,41 @@ class Index extends Component // Stop the VM before restoring — Proxmox requires it try { - $proxmox->requestAndWait('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/status/stop', 'post', [ + $stopStatus = $proxmox->requestAndWait('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/status/stop', 'post', [ 'vmid' => $this->server->vm_id, ], $this->server->node->name); + if ($stopStatus !== 'stopped' && $stopStatus !== 'OK') { + $this->notify('Failed to stop VM before restore. Please try again.', 'error'); + + return; + } } catch (\Exception $e) { $msg = strtolower($e->getMessage()); // If VM is already stopped, continue with restore; otherwise abort if (!str_contains($msg, 'not running') && !str_contains($msg, 'already stopped') && !str_contains($msg, 'is not running')) { - $this->notify('Failed to stop VM before restore: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to stop VM before restore. Please try again.', 'error'); return; } } try { - $proxmox->request('/nodes/' . $this->server->node->name . '/qemu', 'post', data: [ + $restoreStatus = $proxmox->requestAndWait('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType(), 'post', data: [ 'vmid' => $this->server->vm_id, 'force' => true, 'archive' => $this->volid, - ]); + ], node: $this->server->node->name); + + if ($restoreStatus !== 'stopped' && $restoreStatus !== 'OK') { + $this->notify('Backup restore task failed to complete successfully.', 'error'); + + return; + } } catch (\Throwable $e) { - $this->notify('Backup restore failed: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Backup restore failed. Please try again in a moment.', 'error'); return; } @@ -148,7 +184,7 @@ class Index extends Component $response = $proxmox->request('/nodes/' . $this->server->node->name . '/storage/' . $this->server->node->backup_location . '/content/' . $volid, 'delete'); $this->notify('Backup deleted successfully', 'success'); - $this->backups = array_values(array_filter($this->backups, fn ($backup) => $backup['volid'] !== $volid)); + $this->loadBackups(); $this->showDelete = false; $this->confirmDelete = false; $this->volid = null; diff --git a/extensions/Servers/Proxmox/Livewire/BootOrder.php b/extensions/Servers/Proxmox/Livewire/BootOrder.php index f18c3b5c7..c2e65bbb1 100644 --- a/extensions/Servers/Proxmox/Livewire/BootOrder.php +++ b/extensions/Servers/Proxmox/Livewire/BootOrder.php @@ -37,6 +37,12 @@ class BootOrder extends Component { parent::mount($server); + if ($this->server->node?->is_lxc) { + $this->notify('Boot order settings are only available for virtual machines.', 'info'); + $this->redirect(route('proxmox.server.overview', ['server' => $this->server->id]), navigate: true); + return; + } + $this->loadBootConfiguration(false); } @@ -122,7 +128,8 @@ class BootOrder extends Component $this->currentOrder = $this->order; } catch (\Exception $e) { - $this->notify('Failed to update boot order: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to update boot order. Please try again.', 'error'); } } @@ -142,7 +149,8 @@ class BootOrder extends Component try { $currentConfig = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config')->json()['data'] ?? []; } catch (\Exception $e) { - $this->notify('Failed to fetch VM configuration: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to fetch VM configuration. Please refresh and try again.', 'error'); return; } @@ -274,7 +282,8 @@ class BootOrder extends Component $this->notify('CD/DVD-ROM drive removed from ' . strtoupper($drive) . '.', 'success'); $this->loadBootConfiguration(false); } catch (\Exception $e) { - $this->notify('Failed to remove CD/DVD-ROM drive: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to remove CD/DVD-ROM drive. Please try again.', 'error'); } } @@ -329,7 +338,8 @@ class BootOrder extends Component $this->loadBootConfiguration(false); } catch (\Exception $e) { - $this->notify('Failed to update ISO selection: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to update ISO selection. Please try again.', 'error'); } } @@ -419,7 +429,8 @@ class BootOrder extends Component $this->currentIso = null; $this->selectedIsos = []; $this->currentIsos = []; - $this->notify('Unable to load boot information: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Unable to load boot information. Please refresh and try again.', 'error'); } } @@ -734,7 +745,8 @@ class BootOrder extends Component 'label' => 'None', ], ]; - $this->notify('Unable to fetch ISO images from cephfs: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Unable to fetch ISO images. Please try again later.', 'error'); } } diff --git a/extensions/Servers/Proxmox/Livewire/CloudInit.php b/extensions/Servers/Proxmox/Livewire/CloudInit.php index 62f346391..1814422fa 100644 --- a/extensions/Servers/Proxmox/Livewire/CloudInit.php +++ b/extensions/Servers/Proxmox/Livewire/CloudInit.php @@ -23,14 +23,8 @@ class CloudInit extends Component 'l26', 'solaris', ]; - private const MANAGEMENT_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCBeSz45D52fvy4EKPUvI4Upq2t1Ae+MJxB4Lla17f7kHFmoDCJvHfXUCxNv+Tr7FtbCWCdZ2yj8jvS8TNuJ+dzUORbRMgJcokmMLj486RYT7luRHPi+kU5CCv+yIOTNl308wTNmY12rmj+9/81gJA+ipll6UlNTh4T//koSwtcu0mw8ElyV8vjz+s+2vTbPRP+TMPGsFATyJjcjvFvtISW/TT0MuUkbk/bLpv8iurMjFSORa2/lnnMfnznSEL3LQC7CrWvpiCV7mhPn4ZLR/MksvzNnjjKzJHw2u5b8NmiztCfrPUTE/LDNwXh6WtIgqXt7BcjRUT1lR+MMLkhb1lf rsa-key-20240918'; - public string $ciUser = ''; - public string $password = ''; - - public string $password_confirmation = ''; - public string $dnsDomain = ''; public array $dnsServers = []; @@ -128,8 +122,11 @@ class CloudInit extends Component $normalizedKeys = array_filter(array_map('trim', $this->sshKeys)); $encodedKeys = array_map('rawurlencode', $normalizedKeys); - $encodedManagementKey = rawurlencode(self::MANAGEMENT_KEY); - $allKeys = array_merge($encodedKeys, [$encodedManagementKey]); + $allKeys = $encodedKeys; + $managementKey = $this->managementSshKey(); + if ($managementKey !== '' && !in_array($managementKey, $normalizedKeys, true)) { + $allKeys[] = rawurlencode($managementKey); + } $payload['sshkeys'] = implode('%0A', $allKeys); foreach ($this->networks as $network) { @@ -190,7 +187,8 @@ class CloudInit extends Component $this->notify(implode(' ', $messageParts), $messageLevel); } catch (\Exception $e) { - $this->notify('Failed to save cloud-init settings: ' . $e->getMessage(), 'info'); + report($e); + $this->notify('Failed to save cloud-init settings. Please try again.', 'error'); } } @@ -235,7 +233,8 @@ class CloudInit extends Component ]; } } catch (\Exception $e) { - $this->notify('Unable to load cloud-init data: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Unable to load cloud-init data. Please refresh and try again.', 'error'); } } @@ -260,10 +259,25 @@ class CloudInit extends Component return array_values(array_filter($keys, function ($key) { $normalized = preg_replace('/\s+/', ' ', trim($key)); - return $normalized !== self::MANAGEMENT_KEY; + $managementKey = $this->managementSshKey(); + if ($managementKey === '') { + return true; + } + + return $normalized !== $managementKey; })); } + protected function managementSshKey(): string + { + $configured = config('services.proxmox.management_ssh_key'); + if (is_string($configured) && trim($configured) !== '') { + return trim($configured); + } + + return trim((string) env('PROXMOX_MANAGEMENT_SSH_KEY', '')); + } + protected function extractNetworkConfigs(array $config): array { $networks = []; @@ -380,16 +394,12 @@ class CloudInit extends Component public function startEditingPassword(): void { $this->isEditingPassword = true; - $this->password = ''; - $this->password_confirmation = ''; $this->passwordError = ''; } public function cancelEditingPassword(): void { $this->isEditingPassword = false; - $this->password = ''; - $this->password_confirmation = ''; $this->passwordError = ''; } @@ -438,12 +448,12 @@ class CloudInit extends Component $this->cancelEditingNetwork(); } - protected function validatePasswordFields(): bool + protected function validatePasswordFields(string $password, string $passwordConfirmation): bool { $validator = Validator::make( [ - 'password' => $this->password, - 'password_confirmation' => $this->password_confirmation, + 'password' => $password, + 'password_confirmation' => $passwordConfirmation, ], [ 'password' => 'required|string|min:8|confirmed', @@ -459,33 +469,40 @@ class CloudInit extends Component return true; } - public function updatePassword(): void + protected function closePasswordEditor(): void { - if (!$this->validatePasswordFields()) { + $this->isEditingPassword = false; + $this->passwordError = ''; + } + + public function updatePassword(string $password = '', string $passwordConfirmation = ''): void + { + if (!$this->validatePasswordFields($password, $passwordConfirmation)) { return; } try { - $this->server->password = $this->password; + $this->server->password = $password; $this->server->save(); $this->server->refresh(); $this->notify('Password saved to database successfully.', 'success'); - $this->cancelEditingPassword(); + $this->closePasswordEditor(); } catch (\Exception $e) { - $this->passwordError = 'Failed to save password: ' . $e->getMessage(); + report($e); + $this->passwordError = 'Failed to save password. Please try again.'; $this->notify('Failed to save password', 'error'); } } - public function applyPassword(): void + public function applyPassword(string $password = '', string $passwordConfirmation = ''): void { - if (!$this->validatePasswordFields()) { + if (!$this->validatePasswordFields($password, $passwordConfirmation)) { return; } try { - $this->server->password = $this->password; + $this->server->password = $password; $this->server->save(); $this->server->refresh(); @@ -494,14 +511,15 @@ class CloudInit extends Component '/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config', 'put', [ - 'cipassword' => $this->password, + 'cipassword' => $password, ] ); $this->notify('Password applied. Reboot the server for cloud-init to set the new password.', 'success'); - $this->cancelEditingPassword(); + $this->closePasswordEditor(); } catch (\Exception $e) { - $this->passwordError = 'Failed to apply password: ' . $e->getMessage(); + report($e); + $this->passwordError = 'Failed to apply password. Please try again.'; $this->notify('Failed to apply password to Proxmox', 'error'); } } diff --git a/extensions/Servers/Proxmox/Livewire/Component.php b/extensions/Servers/Proxmox/Livewire/Component.php index b0c67dbb3..935fdf3ba 100644 --- a/extensions/Servers/Proxmox/Livewire/Component.php +++ b/extensions/Servers/Proxmox/Livewire/Component.php @@ -24,6 +24,13 @@ class Component extends BaseComponent */ public bool $fastPolling = false; + /** + * Cached Proxmox storage content lookups keyed by storage name. + * + * @var array>> + */ + protected array $storageContentCache = []; + /** * Mount the component with the server. */ @@ -71,7 +78,8 @@ class Component extends BaseComponent $this->fastPolling = true; } } catch (\Throwable $e) { - $this->notify('Action failed: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Action failed. Please try again in a moment.', 'error'); } finally { $this->loadVmStatus(); } @@ -113,8 +121,8 @@ class Component extends BaseComponent return true; } catch (\Throwable $e) { - // If check fails, allow the action (fail open) - return true; + report($e); + return 'Unable to validate storage quota at this time. Please try again in a moment.'; } } @@ -139,6 +147,13 @@ class Component extends BaseComponent } $totalSize += $this->extractDiskSize($value); } + // LXC root filesystem and mount points + elseif ($key === 'rootfs' || preg_match('/^mp(\d+)$/', $key)) { + if ($this->isExcludedDisk($value)) { + continue; + } + $totalSize += $this->extractDiskSize($value); + } // Check for unused/detached drives elseif (preg_match('/^unused(\d+)$/', $key)) { if ($this->isExcludedDisk($value)) { @@ -215,9 +230,31 @@ class Component extends BaseComponent $volume = trim($value); // Extract storage name from volume (format: storage:volume-name) - $storageName = 'cephpool'; - if (str_contains($volume, ':')) { - [$storageName, ] = explode(':', $volume, 2); + $volumeParts = explode(':', $volume, 2); + $storageName = trim($volumeParts[0] ?? ''); + if ($storageName === '') { + return 0; + } + + $storageRows = $this->getStorageContentRows($storageName); + foreach ($storageRows as $item) { + if (isset($item['volid']) && $item['volid'] === $volume && isset($item['size'])) { + return round(((float) $item['size']) / (1024 * 1024 * 1024), 2); + } + } + + return 0; + } + + /** + * Fetch and memoize storage contents for the active VM. + * + * @return array> + */ + protected function getStorageContentRows(string $storageName): array + { + if (array_key_exists($storageName, $this->storageContentCache)) { + return $this->storageContentCache[$storageName]; } try { @@ -226,24 +263,13 @@ class Component extends BaseComponent '/nodes/' . $this->server->node->name . '/storage/' . $storageName . '/content', data: ['vmid' => $this->server->vm_id] ); - - $data = $storageContent->json()['data'] ?? []; - if (is_array($data)) { - foreach ($data as $item) { - if (isset($item['volid']) && $item['volid'] === $volume) { - // Size is in bytes - if (isset($item['size'])) { - return round($item['size'] / (1024 * 1024 * 1024), 2); - } - break; - } - } - } + $rows = $storageContent->json()['data'] ?? []; + $this->storageContentCache[$storageName] = is_array($rows) ? $rows : []; } catch (\Throwable $e) { - // If we can't fetch size, return 0 + $this->storageContentCache[$storageName] = []; } - - return 0; + + return $this->storageContentCache[$storageName]; } /** diff --git a/extensions/Servers/Proxmox/Livewire/Display.php b/extensions/Servers/Proxmox/Livewire/Display.php index f122815f2..4e6b6b07b 100644 --- a/extensions/Servers/Proxmox/Livewire/Display.php +++ b/extensions/Servers/Proxmox/Livewire/Display.php @@ -58,6 +58,14 @@ class Display extends Component { parent::mount($server); + if ($this->server->node?->is_lxc) { + $this->redirect( + route('proxmox.server.overview', ['server' => $this->server->id, 'notice' => 'display_unavailable']), + navigate: true + ); + return; + } + $this->loadDisplayConfiguration(false); } @@ -91,7 +99,8 @@ class Display extends Component } } catch (\Exception $e) { if ($notify) { - $this->notify('Failed to load display settings: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to load display settings. Please refresh and try again.', 'error'); } } } @@ -130,7 +139,8 @@ class Display extends Component $this->notify('Display configuration updated successfully. Reboot the VPS for changes to take effect.', 'success'); } catch (\Exception $e) { - $this->notify('Unable to update display settings: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Unable to update display settings. Please try again.', 'error'); } } diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index 93a40e98c..6f650f5dc 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -305,6 +305,12 @@ class Firewall extends Component public int $logLimit = 500; // Reduced to prevent 413 errors public string $logError = ''; public bool $logsLoaded = false; // Track if logs have been loaded + public bool $ipSetsLoaded = false; + public bool $savedConfigurationsLoaded = false; + public bool $networkAdaptersLoaded = false; + public bool $firewallOptionsLoaded = false; + public bool $firewallRulesLoaded = false; + public bool $aliasesLoaded = false; public string $activeLogPreset = 'all'; // Pagination for logs @@ -325,13 +331,42 @@ class Firewall extends Component $this->coreRouterConfigured = $this->coreRouterEnabled(); $this->loadAvailableIps(); - $this->loadNetworkAdapters(); - $this->loadFirewallOptions(); - $this->loadFirewallRules(); - $this->loadAliases(); - $this->loadIPSets(); - $this->loadSavedConfigurations(); - // Don't load logs on mount - load lazily when logs tab is viewed + // Defer firewall-heavy calls until the relevant tab is activated. + } + + public function activateTab(string $tab): void + { + if ($tab === 'rules') { + if (!$this->networkAdaptersLoaded) { + $this->loadNetworkAdapters(); + } + if (!$this->firewallOptionsLoaded) { + $this->loadFirewallOptions(); + } + if (!$this->firewallRulesLoaded) { + $this->loadFirewallRules(); + } + return; + } + + if ($tab === 'aliases' && !$this->aliasesLoaded) { + $this->loadAliases(); + return; + } + + if ($tab === 'logs' && !$this->logsLoaded) { + $this->loadFirewallLogs(false); + return; + } + + if ($tab === 'ipsets' && !$this->ipSetsLoaded) { + $this->loadIPSets(); + return; + } + + if ($tab === 'saved' && !$this->savedConfigurationsLoaded) { + $this->loadSavedConfigurations(); + } } // ==================== AVAILABLE IPS ==================== @@ -371,16 +406,19 @@ class Firewall extends Component protected function firewallRequest(string $url, string $method = 'get', array $data = []): \Illuminate\Http\Client\Response { - // Load config from the Server model directly (avoids debug_backtrace() class resolution issue) - $serverModel = \App\Models\Server::where('extension', 'Proxmox')->first(); - $config = $serverModel ? $serverModel->settings->pluck('value', 'key')->toArray() : []; - $proxmox = new \Paymenter\Extensions\Servers\Proxmox\Proxmox($config); + $proxmox = new Proxmox; + + // Reuse the centralized Proxmox client for all non-log requests + // to keep auth/timeouts/error behavior aligned across the extension. + if (!str_contains($url, '/firewall/log')) { + return $proxmox->request($url, $method, $data, timeoutSeconds: 30, connectTimeoutSeconds: 10); + } + $baseUrl = rtrim($proxmox->config('host'), '/'); $authToken = 'PVEAPIToken=' . $proxmox->config('user') . '=' . $proxmox->config('token'); $verifySsl = $proxmox->config('verify_ssl') ? true : false; - $fullUrl = $baseUrl . '/api2/json' . $url; - + $acceptHeader = str_contains($url, '/firewall/log') ? 'application/json, text/plain, */*' : 'application/json'; @@ -388,21 +426,23 @@ class Firewall extends Component $headers = [ 'Authorization' => $authToken, 'Accept' => $acceptHeader, + 'Content-Type' => 'application/json', ]; // Prevent caching for log requests - if (str_contains($url, '/firewall/log')) { - $headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'; - $headers['Pragma'] = 'no-cache'; - } + $headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'; + $headers['Pragma'] = 'no-cache'; - $request = Http::withHeaders($headers)->withOptions(['verify' => $verifySsl])->asForm(); + $request = Http::withHeaders($headers) + ->withOptions(['verify' => $verifySsl]) + ->timeout(20) + ->connectTimeout(8); $response = match($method) { 'post' => $request->post($fullUrl, $data), 'put' => $request->put($fullUrl, $data), 'delete' => $request->delete($fullUrl), - default => Http::withHeaders($headers)->withOptions(['verify' => $verifySsl])->get($fullUrl, $data), + default => $request->get($fullUrl, $data), }; return $response; @@ -451,6 +491,7 @@ class Firewall extends Component } $this->networkAdapters = $adapters; + $this->networkAdaptersLoaded = true; } } catch (\Exception $e) { Log::error('Failed to load network adapters: ' . $e->getMessage()); @@ -542,6 +583,7 @@ class Firewall extends Component $this->firewallEnabled = ($options['enable'] ?? 0) == 1; $this->policyIn = strtoupper($options['policy_in'] ?? 'DROP'); $this->policyOut = strtoupper($options['policy_out'] ?? 'ACCEPT'); + $this->firewallOptionsLoaded = true; } } catch (\Exception $e) { Log::error('Failed to load firewall options: ' . $e->getMessage()); @@ -689,6 +731,7 @@ class Firewall extends Component if ($response->successful()) { $this->rules = $response->json()['data'] ?? []; + $this->firewallRulesLoaded = true; } } catch (\Exception $e) { Log::error('Failed to load firewall rules: ' . $e->getMessage()); @@ -1030,6 +1073,7 @@ class Firewall extends Component if ($response->successful()) { $this->aliases = $response->json()['data'] ?? []; + $this->aliasesLoaded = true; } } catch (\Exception $e) { Log::error('Failed to load aliases: ' . $e->getMessage()); @@ -1191,6 +1235,7 @@ class Firewall extends Component if ($response->successful()) { $this->ipsets = $response->json()['data'] ?? []; + $this->ipSetsLoaded = true; } } catch (\Exception $e) { Log::error('Failed to load IPSets: ' . $e->getMessage()); @@ -1475,7 +1520,7 @@ class Firewall extends Component // ==================== FIREWALL LOGS ==================== - public function loadFirewallLogs(): void + public function loadFirewallLogs(bool $notify = true): void { $this->logError = ''; @@ -1571,8 +1616,10 @@ class Firewall extends Component // Set last refresh timestamp $this->lastLogRefresh = now()->format('Y-m-d H:i:s'); - // Notify user of successful refresh - $this->notify('Loaded ' . count($this->firewallLogs) . ' firewall log entries', 'success'); + // Notify user only for explicit/manual refresh actions + if ($notify) { + $this->notify('Loaded ' . count($this->firewallLogs) . ' firewall log entries', 'success'); + } Log::info('Firewall log parsing summary', [ 'total_raw_logs' => count($allRawLogs), @@ -1583,8 +1630,8 @@ class Firewall extends Component ]); } catch (\Exception $e) { - $this->logError = 'Failed to load logs: ' . $e->getMessage(); - Log::error('Firewall logs error: ' . $e->getMessage()); + $this->logError = 'Failed to load firewall logs. Please try again.'; + Log::error('Firewall logs error', ['error' => $e->getMessage()]); $this->firewallLogs = []; } } @@ -2129,6 +2176,7 @@ class Firewall extends Component ->orderBy('created_at', 'desc') ->get() ->toArray(); + $this->savedConfigurationsLoaded = true; } /** @@ -2732,18 +2780,17 @@ class Firewall extends Component /** * Get all other servers owned by the current user. */ - public function getUserOtherServers(): array + public function getUserOtherServers(): \Illuminate\Support\Collection { $userId = Auth::id(); if (!$userId) { - return []; + return collect(); } return Server::where('user_id', $userId) ->where('id', '!=', $this->server->id) - ->with(['node', 'primaryIpv4', 'primaryIpv6']) - ->get() - ->toArray(); + ->with(['node', 'primaryIpv4', 'primaryIpv6', 'ipAddresses']) + ->get(); } /** @@ -2815,18 +2862,28 @@ class Firewall extends Component $results = []; try { - foreach ($otherServers as $serverData) { - $server = Server::with('node')->find($serverData['id']); + $sourceVmType = $this->server->node?->vmType(); + foreach ($otherServers as $server) { if (!$server || !$server->node) { $results[] = [ - 'server_id' => $serverData['id'], - 'hostname' => $serverData['hostname'] ?? 'Unknown', + 'server_id' => $server->id ?? 0, + 'hostname' => $server->hostname ?? 'Unknown', 'success' => false, 'message' => 'Server or node not found', ]; continue; } + if ($sourceVmType && $server->node->vmType() !== $sourceVmType) { + $results[] = [ + 'server_id' => $server->id, + 'hostname' => $server->hostname ?? 'VPS ' . $server->vm_id, + 'success' => false, + 'message' => 'Skipped: server type mismatch (KVM/LXC)', + ]; + continue; + } + try { // Apply configuration to this server $this->applyConfigurationToServer($server, $matchingConfig); @@ -2843,7 +2900,7 @@ class Firewall extends Component 'server_id' => $server->id, 'hostname' => $server->hostname ?? 'VPS ' . $server->vm_id, 'success' => false, - 'message' => 'Failed: ' . $e->getMessage(), + 'message' => 'Failed to apply configuration to this server.', ]; } } @@ -2863,7 +2920,7 @@ class Firewall extends Component } } catch (\Exception $e) { Log::error('Failed to apply configuration to all servers: ' . $e->getMessage()); - $this->error = 'Failed to apply configuration: ' . $e->getMessage(); + $this->error = 'Failed to apply configuration. Please try again.'; $this->showErrorModal = true; } finally { $this->loading = false; @@ -2880,6 +2937,33 @@ class Firewall extends Component $outgoingPolicy = $config['outgoing_policy'] ?? 'ACCEPT'; $vmType = $server->node->vmType(); + $targetInterfaces = $this->getServerNetworkAdapterNames($server); + $targetIps = collect([ + $server->primaryIpv4?->ip, + $server->primaryIpv6?->ip, + ]) + ->merge($server->ipAddresses->pluck('ip')) + ->filter(fn ($ip) => is_string($ip) && trim($ip) !== '') + ->values() + ->all(); + + $referencedInterfaces = collect($savedRules) + ->pluck('iface') + ->filter(fn ($iface) => is_string($iface) && trim($iface) !== '') + ->map(fn ($iface) => trim((string) $iface)) + ->unique() + ->values() + ->all(); + + if (!empty($referencedInterfaces)) { + $missingInterfaces = array_values(array_diff($referencedInterfaces, $targetInterfaces)); + if (!empty($missingInterfaces)) { + throw new \Exception( + 'Skipped: missing interfaces on target server (' . implode(', ', $missingInterfaces) . ')' + ); + } + } + // Step 1: Update policies $response = $this->firewallRequest( '/nodes/' . $server->node->name . '/' . $vmType . '/' . $server->vm_id . '/firewall/options', @@ -2923,14 +3007,22 @@ class Firewall extends Component ]; if (!empty($savedRule['source'])) $rulePayload['source'] = $savedRule['source']; - if (!empty($savedRule['dest'])) $rulePayload['dest'] = $savedRule['dest']; + if (!empty($savedRule['dest'])) { + $destIp = (string) $savedRule['dest']; + $isDirectIp = filter_var($destIp, FILTER_VALIDATE_IP) !== false; + if (!$isDirectIp || in_array($destIp, $targetIps, true)) { + $rulePayload['dest'] = $destIp; + } + } if (!empty($savedRule['proto'])) $rulePayload['proto'] = $savedRule['proto']; if (!empty($savedRule['dport'])) $rulePayload['dport'] = $savedRule['dport']; if (!empty($savedRule['sport'])) $rulePayload['sport'] = $savedRule['sport']; if (!empty($savedRule['macro'])) $rulePayload['macro'] = $savedRule['macro']; if (!empty($savedRule['comment'])) $rulePayload['comment'] = $savedRule['comment']; if (!empty($savedRule['log']) && $savedRule['log'] !== 'nolog') $rulePayload['log'] = $savedRule['log']; - // Skip iface as it may not exist on other servers + if (!empty($savedRule['iface']) && in_array($savedRule['iface'], $targetInterfaces, true)) { + $rulePayload['iface'] = $savedRule['iface']; + } $this->firewallRequest( '/nodes/' . $server->node->name . '/' . $vmType . '/' . $server->vm_id . '/firewall/rules', @@ -2941,6 +3033,31 @@ class Firewall extends Component } } + protected function getServerNetworkAdapterNames(Server $server): array + { + try { + $response = $this->firewallRequest( + '/nodes/' . $server->node->name . '/' . $server->node->vmType() . '/' . $server->vm_id . '/config' + ); + + if (!$response->successful()) { + return []; + } + + $config = $response->json()['data'] ?? []; + $names = []; + foreach ($config as $key => $value) { + if (is_string($key) && preg_match('/^net\d+$/', $key)) { + $names[] = $key; + } + } + + return array_values(array_unique($names)); + } catch (\Throwable $e) { + return []; + } + } + public function render() { return view('proxmox::firewall')->layoutData([ diff --git a/extensions/Servers/Proxmox/Livewire/Index.php b/extensions/Servers/Proxmox/Livewire/Index.php index 6549c913e..5669ef8eb 100644 --- a/extensions/Servers/Proxmox/Livewire/Index.php +++ b/extensions/Servers/Proxmox/Livewire/Index.php @@ -13,6 +13,7 @@ class Index extends Component return view('proxmox::index', [ 'servers' => Server::where('user_id', Auth::id()) ->with(['primaryIpv4', 'primaryIpv6', 'service.product', 'node']) + ->orderBy('id') ->get(), ])->layoutData([ 'sidebar' => true, diff --git a/extensions/Servers/Proxmox/Livewire/Overview.php b/extensions/Servers/Proxmox/Livewire/Overview.php index 9c63f0c99..d5f042c34 100644 --- a/extensions/Servers/Proxmox/Livewire/Overview.php +++ b/extensions/Servers/Proxmox/Livewire/Overview.php @@ -1,9 +1,7 @@ loadVmStatus(); } - $this->hostname = $server->hostname ?? ($this->status->name ?? ''); + $this->hostname = $server->hostname ?? ($this->status?->name ?? ''); + + $notice = request()->query('notice'); + if ($notice === 'display_unavailable') { + $this->notify('Display settings are only available for virtual machines.', 'info'); + } // Load notes from Proxmox VM description try { @@ -68,136 +57,6 @@ class Overview extends Component } } - #[On('proxmox:stats')] - public function stats() - { - // Group graph types by their (timeRange, aggregation) to batch API calls. - // The Proxmox /rrddata endpoint returns ALL metrics in a single response, - // so we can serve multiple graph types from one API call when they share parameters. - $groups = []; - $graphConfigs = [ - 'cpu' => [$this->cpuTimeRange, $this->cpuAggregation], - 'memory' => [$this->memoryTimeRange, $this->memoryAggregation], - 'network' => [$this->networkTimeRange, $this->networkAggregation], - 'disk' => [$this->diskTimeRange, $this->diskAggregation], - ]; - - foreach ($graphConfigs as $graphType => [$timeRange, $aggregation]) { - $key = $timeRange . '|' . $aggregation; - $groups[$key]['timeRange'] = $timeRange; - $groups[$key]['aggregation'] = $aggregation; - $groups[$key]['types'][] = $graphType; - } - - foreach ($groups as $group) { - $this->fetchGraphDataBatch($group['types'], $group['timeRange'], $group['aggregation']); - } - } - - /** - * Fetch rrddata once and dispatch to multiple graph types that share the same parameters. - */ - protected function fetchGraphDataBatch(array $graphTypes, string $timeRange, string $aggregation): void - { - $allowedRanges = ['hour', 'day', 'week', 'month', 'year']; - $allowedAggs = ['average', 'maximum']; - if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) { - return; - } - - // Use a shared cache key for the raw API data (not per-graph-type) - $cacheKey = 'proxmox-stats-' . $this->server->id . '-rrd-' . $timeRange . '-' . $aggregation; - $data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) { - $proxmox = new Proxmox; - $vmType = $this->server->node->vmType(); - $stats = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/rrddata', data: [ - 'timeframe' => $timeRange, - 'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE', - ]); - - return $stats->json()['data'] ?? []; - }); - - foreach ($graphTypes as $graphType) { - // Also store per-graph-type cache keys for individual update/invalidation - $perGraphKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation; - Cache::put($perGraphKey, $data, 60); - - $this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data); - } - } - - protected function fetchGraphData(string $graphType, string $timeRange, string $aggregation) - { - $allowedRanges = ['hour', 'day', 'week', 'month', 'year']; - $allowedAggs = ['average', 'maximum']; - if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) { - return; - } - - $cacheKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation; - $data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) { - $proxmox = new Proxmox; - $vmType = $this->server->node->vmType(); - $stats = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/rrddata', data: [ - 'timeframe' => $timeRange, - 'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE', - ]); - - return $stats->json()['data'] ?? []; - }); - - $this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data); - } - - public function updatedCpuTimeRange() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation); - $this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation); - } - - public function updatedCpuAggregation() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation); - $this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation); - } - - public function updatedMemoryTimeRange() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation); - $this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation); - } - - public function updatedMemoryAggregation() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation); - $this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation); - } - - public function updatedNetworkTimeRange() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation); - $this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation); - } - - public function updatedNetworkAggregation() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation); - $this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation); - } - - public function updatedDiskTimeRange() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation); - $this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation); - } - - public function updatedDiskAggregation() - { - Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation); - $this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation); - } - protected function formatSeconds(?int $seconds): string { if (is_null($seconds) || $seconds < 0) { @@ -247,11 +106,11 @@ class Overview extends Component $tasks = $response->json()['data'] ?? []; foreach ($tasks as $task) { $type = strtolower($task['type'] ?? ''); - if (in_array($type, ['qmstart', 'qmresume'], true)) { + if (in_array($type, ['qmstart', 'qmresume', 'pctstart', 'pctresume'], true)) { $counts['starts']++; - } elseif (in_array($type, ['qmstop', 'qmshutdown'], true)) { + } elseif (in_array($type, ['qmstop', 'qmshutdown', 'pctstop', 'pctshutdown'], true)) { $counts['shutdowns']++; - } elseif (in_array($type, ['qmreboot', 'qmreset'], true)) { + } elseif (in_array($type, ['qmreboot', 'qmreset', 'pctreboot', 'pctreset'], true)) { $counts['reboots']++; } } @@ -334,14 +193,14 @@ class Overview extends Component public function startEditingHostname() { $this->isEditingHostname = true; - $this->hostname = $this->server->hostname ?? $this->status->name ?? ''; + $this->hostname = $this->server->hostname ?? $this->status?->name ?? ''; $this->hostnameError = ''; } public function cancelEditingHostname() { $this->isEditingHostname = false; - $this->hostname = $this->server->hostname ?? $this->status->name ?? ''; + $this->hostname = $this->server->hostname ?? $this->status?->name ?? ''; $this->hostnameError = ''; } @@ -423,26 +282,21 @@ class Overview extends Component public function startEditingPassword() { $this->isEditingPassword = true; - $this->password = ''; - $this->password_confirmation = ''; $this->passwordError = ''; } public function cancelEditingPassword() { $this->isEditingPassword = false; - $this->password = ''; - $this->password_confirmation = ''; $this->passwordError = ''; } - public function updatePassword() + protected function validatePasswordInput(string $password, string $passwordConfirmation): bool { - // Validate password using same rules as registration $validator = Validator::make( [ - 'password' => $this->password, - 'password_confirmation' => $this->password_confirmation, + 'password' => $password, + 'password_confirmation' => $passwordConfirmation, ], [ 'password' => 'required|string|min:8|confirmed', @@ -455,49 +309,49 @@ class Overview extends Component return false; } + return true; + } + + protected function closePasswordEditor(): void + { + $this->isEditingPassword = false; + $this->passwordError = ''; + } + + public function updatePassword(string $password = '', string $passwordConfirmation = '') + { + if (!$this->validatePasswordInput($password, $passwordConfirmation)) { + return false; + } + try { // Update password in database - $this->server->password = $this->password; + $this->server->password = $password; $this->server->save(); // Refresh the server model to get latest data $this->server->refresh(); $this->notify('Password saved to database successfully', 'success'); - $this->isEditingPassword = false; - $this->password = ''; - $this->password_confirmation = ''; - $this->passwordError = ''; + $this->closePasswordEditor(); return true; } catch (\Exception $e) { - $this->passwordError = 'Failed to save password: ' . $e->getMessage(); + report($e); + $this->passwordError = 'Failed to save password. Please try again.'; $this->notify('Failed to save password', 'error'); return false; } } - public function applyPassword() + public function applyPassword(string $password = '', string $passwordConfirmation = '') { - // Validate password first - $validator = Validator::make( - [ - 'password' => $this->password, - 'password_confirmation' => $this->password_confirmation, - ], - [ - 'password' => 'required|string|min:8|confirmed', - ] - ); - - if ($validator->fails()) { - $errors = $validator->errors()->all(); - $this->passwordError = implode(' ', $errors); + if (!$this->validatePasswordInput($password, $passwordConfirmation)) { return; } try { // Update password in database first - $this->server->password = $this->password; + $this->server->password = $password; $this->server->save(); // Refresh the server model to get latest data @@ -506,10 +360,7 @@ class Overview extends Component // LXC containers don't support password change via config API if ($this->server->node->is_lxc) { $this->notify('Password saved to database. Note: LXC container passwords cannot be changed via API - use console access to change root password if needed.', 'warning'); - $this->isEditingPassword = false; - $this->password = ''; - $this->password_confirmation = ''; - $this->passwordError = ''; + $this->closePasswordEditor(); return; } @@ -519,17 +370,15 @@ class Overview extends Component // For KVM: cipassword sets the password via cloud-init on next boot $response = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config', 'put', [ - 'cipassword' => $this->password, + 'cipassword' => $password, ]); // The request() method throws on error, so if we get here, it was successful $this->notify('Password applied to Proxmox successfully. The new password will be configured on the next boot via cloud-init. Please reboot your server for the changes to take effect.', 'success'); - $this->isEditingPassword = false; - $this->password = ''; - $this->password_confirmation = ''; - $this->passwordError = ''; + $this->closePasswordEditor(); } catch (\Exception $e) { - $this->passwordError = 'Failed to apply password: ' . $e->getMessage(); + report($e); + $this->passwordError = 'Failed to apply password. Please try again.'; $this->notify('Failed to apply password to Proxmox', 'error'); } } diff --git a/extensions/Servers/Proxmox/Livewire/Reinstall.php b/extensions/Servers/Proxmox/Livewire/Reinstall.php index e66b0cd69..24f3bb29a 100644 --- a/extensions/Servers/Proxmox/Livewire/Reinstall.php +++ b/extensions/Servers/Proxmox/Livewire/Reinstall.php @@ -15,12 +15,15 @@ class Reinstall extends Component public $confirmReinstall = false; public bool $isProtected = false; + public bool $protectionCheckFailed = false; + public $osList = []; public function mount(Server $server): void { parent::mount($server); $this->os = $server->os_id; $this->checkProtectedStatus(); + $this->loadOsList(); } protected function checkProtectedStatus(): void @@ -30,9 +33,11 @@ class Reinstall extends Component $config = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config')->json()['data'] ?? []; // Protected can be 1, '1', true, or missing/null $this->isProtected = isset($config['protection']) && ($config['protection'] == 1 || $config['protection'] === '1' || $config['protection'] === true); + $this->protectionCheckFailed = false; } catch (\Throwable $e) { - // If we can't check, assume not protected - $this->isProtected = false; + // Fail closed for destructive operations when protection status cannot be verified + $this->isProtected = true; + $this->protectionCheckFailed = true; } } @@ -42,7 +47,11 @@ class Reinstall extends Component $this->checkProtectedStatus(); if ($this->isProtected) { - $this->notify('Cannot reinstall: This server is protected in Proxmox', 'error'); + if ($this->protectionCheckFailed) { + $this->notify('Cannot reinstall: Unable to verify server protection status. Please try again.', 'error'); + } else { + $this->notify('Cannot reinstall: This server is protected in Proxmox', 'error'); + } return; } @@ -54,41 +63,41 @@ class Reinstall extends Component return; } - $this->server->os_id = $this->os; - $this->server->save(); - try { $proxmox = new Proxmox; $proxmox->reinstall($this->server); } catch (\Throwable $e) { - $this->notify('Reinstall failed: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Reinstall failed. Please try again in a moment.', 'error'); return; } + $this->server->os_id = $this->os; + $this->server->save(); + $this->redirect(route('proxmox.index')); } - public function render() + protected function loadOsList(): void { if (!$this->server->node) { - return view('proxmox::reinstall', [ - 'osList' => collect(), - 'isProtected' => $this->isProtected, - ]); + $this->osList = collect(); + return; } - // assuming Server has relation `node()` -> Node model $locationId = $this->server->node->location_id; - $isLxc = $this->server->node->is_lxc ?? false; + $isLxc = (bool) ($this->server->node->is_lxc ?? false); - // Filter OS templates by location AND type (LXC templates for LXC nodes, KVM for KVM) - $osList = \Paymenter\Extensions\Servers\Proxmox\Models\Location::findOrFail($locationId) - ->os() + $this->osList = OS::query() + ->where('location_id', $locationId) ->where('is_lxc', $isLxc) ->get(); + } + public function render() + { return view('proxmox::reinstall', [ - 'osList' => $osList, + 'osList' => $this->osList, 'isProtected' => $this->isProtected, ]); } diff --git a/extensions/Servers/Proxmox/Livewire/StatisticsRedirect.php b/extensions/Servers/Proxmox/Livewire/StatisticsRedirect.php index 8ced14662..1ea2d8e09 100644 --- a/extensions/Servers/Proxmox/Livewire/StatisticsRedirect.php +++ b/extensions/Servers/Proxmox/Livewire/StatisticsRedirect.php @@ -13,16 +13,12 @@ final class StatisticsRedirect extends Component { public function mount(Server $server): void { - $this->server = $server; - if ((int) $server->user_id !== (int) auth()->id()) { - abort(403); - } - - $this->redirect(route('proxmox.server.overview', ['server' => $server]), navigate: true); + parent::mount($server); + $this->redirect(route('proxmox.server.overview', ['server' => $this->server]), navigate: true); } - public function render(): \Illuminate\Http\RedirectResponse + public function render(): \Illuminate\View\View { - return redirect()->route('proxmox.server.overview', ['server' => $this->server]); + return view('proxmox::serverplaceholder'); } } diff --git a/extensions/Servers/Proxmox/Livewire/Storage.php b/extensions/Servers/Proxmox/Livewire/Storage.php index d08f3971f..27f87206c 100644 --- a/extensions/Servers/Proxmox/Livewire/Storage.php +++ b/extensions/Servers/Proxmox/Livewire/Storage.php @@ -134,13 +134,14 @@ class Storage extends Component public string $reattachTargetBus = 'scsi'; /** - * Storage name for cephpool. + * Active storage pool used for new drive allocations. */ protected string $storagePool = 'cephpool'; public function mount(Server $server): void { parent::mount($server); + $this->storagePool = trim((string) ($this->server->node->storage_location ?? '')) ?: 'cephpool'; $this->loadMaxAllowedStorage(); $this->loadStorageConfiguration(false); @@ -181,7 +182,8 @@ class Storage extends Component } } catch (\Exception $e) { if ($notify) { - $this->notify('Failed to load storage configuration: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to load storage configuration. Please refresh and try again.', 'error'); } } } @@ -193,11 +195,27 @@ class Storage extends Component { $drives = []; $totalSize = 0; + $isLxc = (bool) ($this->server->node->is_lxc ?? false); foreach ($config as $key => $value) { - // Skip non-disk entries - if (!preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $key, $matches)) { - continue; + if ($isLxc) { + if (preg_match('/^rootfs$/', (string) $key)) { + $busType = 'rootfs'; + $deviceIndex = 0; + } elseif (preg_match('/^mp(\d+)$/', (string) $key, $matches)) { + $busType = 'mp'; + $deviceIndex = (int) $matches[1]; + } else { + continue; + } + } else { + // Skip non-disk entries + if (!preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', (string) $key, $matches)) { + continue; + } + + $busType = $matches[1]; + $deviceIndex = (int) $matches[2]; } // Skip cloud-init, EFI, and TPM disks @@ -210,9 +228,6 @@ class Storage extends Component continue; } - $busType = $matches[1]; - $deviceIndex = (int) $matches[2]; - $diskInfo = $this->parseDiskString($value, $busType, $deviceIndex); if ($diskInfo) { $drives[$key] = $diskInfo; @@ -222,10 +237,17 @@ class Storage extends Component // Sort drives by bus type and index uksort($drives, function ($a, $b) { - preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $a, $matchA); - preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $b, $matchB); - - $busOrder = ['ide' => 0, 'sata' => 1, 'scsi' => 2, 'virtio' => 3]; + preg_match('/^(ide|sata|scsi|virtio|mp)(\d+)$/', (string) $a, $matchA); + preg_match('/^(ide|sata|scsi|virtio|mp)(\d+)$/', (string) $b, $matchB); + + if ((string) $a === 'rootfs') { + $matchA = ['rootfs', 'rootfs', '0']; + } + if ((string) $b === 'rootfs') { + $matchB = ['rootfs', 'rootfs', '0']; + } + + $busOrder = ['rootfs' => 0, 'ide' => 1, 'sata' => 2, 'scsi' => 3, 'virtio' => 4, 'mp' => 5]; $busCompare = ($busOrder[$matchA[1]] ?? 4) <=> ($busOrder[$matchB[1]] ?? 4); if ($busCompare !== 0) { @@ -281,7 +303,7 @@ class Storage extends Component $volume = trim($value); // Extract storage name from volume (format: storage:volume-name) - $storageName = 'cephpool'; + $storageName = ''; if (str_contains($volume, ':')) { [$storageName, ] = explode(':', $volume, 2); } @@ -289,35 +311,37 @@ class Storage extends Component // Try to get the volume size from the storage API $sizeGb = 0; $sizeFormatted = 'Unknown'; - try { - $proxmox = new Proxmox; - $storageContent = $proxmox->request( - '/nodes/' . $this->server->node->name . '/storage/' . $storageName . '/content', - data: ['vmid' => $this->server->vm_id] - ); + if ($storageName !== '') { + try { + $proxmox = new Proxmox; + $storageContent = $proxmox->request( + '/nodes/' . $this->server->node->name . '/storage/' . $storageName . '/content', + data: ['vmid' => $this->server->vm_id] + ); - $data = $storageContent->json()['data'] ?? []; - if (is_array($data)) { - foreach ($data as $item) { - if (isset($item['volid']) && $item['volid'] === $volume) { - // Size is in bytes - if (isset($item['size'])) { - $sizeGb = round($item['size'] / (1024 * 1024 * 1024), 2); - $sizeFormatted = $sizeGb . ' GB'; + $data = $storageContent->json()['data'] ?? []; + if (is_array($data)) { + foreach ($data as $item) { + if (isset($item['volid']) && $item['volid'] === $volume) { + // Size is in bytes + if (isset($item['size'])) { + $sizeGb = round($item['size'] / (1024 * 1024 * 1024), 2); + $sizeFormatted = $sizeGb . ' GB'; + } + break; } - break; } } + } catch (\Exception $e) { + // If we can't fetch size, leave as unknown } - } catch (\Exception $e) { - // If we can't fetch size, leave as unknown } return [ 'key' => 'unused' . $unusedIndex, 'index' => $unusedIndex, 'volume' => $volume, - 'storage' => $storageName, + 'storage' => $storageName !== '' ? $storageName : 'Unknown', 'size' => $sizeGb, 'size_formatted' => $sizeFormatted, 'label' => 'Detached Disk ' . $unusedIndex, @@ -445,6 +469,8 @@ class Storage extends Component protected function getDiskLabel(string $busType, int $index): string { $busLabels = [ + 'rootfs' => 'Root Filesystem', + 'mp' => 'Mount Point', 'ide' => 'IDE', 'sata' => 'SATA', 'scsi' => 'SCSI', @@ -584,7 +610,8 @@ class Storage extends Component $this->closeAddDriveModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { - $this->notify('Failed to add drive: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to add drive. Please try again.', 'error'); } } @@ -735,7 +762,8 @@ class Storage extends Component $this->closeEditDriveModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { - $this->notify('Failed to update drive settings: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to update drive settings. Please try again.', 'error'); } } @@ -853,7 +881,8 @@ class Storage extends Component // Dispatch browser event to refresh the page $this->dispatch('refresh-page'); } catch (\Exception $e) { - $this->notify('Failed to detach drive: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to detach drive. Please try again.', 'error'); } } diff --git a/extensions/Servers/Proxmox/Livewire/Subdomain.php b/extensions/Servers/Proxmox/Livewire/Subdomain.php index ed00ced8a..bfe4825e8 100644 --- a/extensions/Servers/Proxmox/Livewire/Subdomain.php +++ b/extensions/Servers/Proxmox/Livewire/Subdomain.php @@ -190,8 +190,9 @@ class Subdomain extends Component $dns = new TechnitiumDns; $this->serverZones = $dns->getServerZones($this->server->id); } catch (\Exception $e) { + report($e); $this->serverZones = []; - $this->notify('Failed to load subdomains: ' . $e->getMessage(), 'error'); + $this->notify('Failed to load subdomains. Please try again.', 'error'); } } @@ -218,7 +219,8 @@ class Subdomain extends Component $this->notify('Subdomain is available!', 'success'); } } catch (\Exception $e) { - $this->notify('Failed to check subdomain availability: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to check subdomain availability. Please try again.', 'error'); } } @@ -328,7 +330,8 @@ class Subdomain extends Component $this->notify($errorMessage, 'error'); } } catch (\Exception $e) { - $this->notify('Failed to create subdomain: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to create subdomain. Please try again.', 'error'); } } @@ -348,38 +351,12 @@ class Subdomain extends Component protected function adjectivePool(): array { - static $pool = null; - - if ($pool === null) { - $pool = $this->expandPool($this->baseAdjectives); - } - - return $pool; + return $this->baseAdjectives; } protected function animalPool(): array { - static $pool = null; - - if ($pool === null) { - $pool = $this->expandPool($this->baseAnimals); - } - - return $pool; - } - - protected function expandPool(array $base): array - { - if (count($base) >= 1000) { - return array_slice($base, 0, 1000); - } - - $pool = $base; - while (count($pool) < 1000) { - $pool = array_merge($pool, $base); - } - - return array_slice($pool, 0, 1000); + return $this->baseAnimals; } /** @@ -431,7 +408,8 @@ class Subdomain extends Component $this->notify('Failed to delete subdomain.', 'error'); } } catch (\Exception $e) { - $this->notify('Failed to delete subdomain: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to delete subdomain. Please try again.', 'error'); } } @@ -513,7 +491,8 @@ class Subdomain extends Component $this->notify('Failed to add DNS record.', 'error'); } } catch (\Exception $e) { - $this->notify('Failed to add DNS record: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to add DNS record. Please try again.', 'error'); } } @@ -545,7 +524,8 @@ class Subdomain extends Component $this->notify('Failed to delete DNS record.', 'error'); } } catch (\Exception $e) { - $this->notify('Failed to delete DNS record: ' . $e->getMessage(), 'error'); + report($e); + $this->notify('Failed to delete DNS record. Please try again.', 'error'); } } diff --git a/extensions/Servers/Proxmox/Livewire/System.php b/extensions/Servers/Proxmox/Livewire/System.php index 02954ee37..8921ec685 100644 --- a/extensions/Servers/Proxmox/Livewire/System.php +++ b/extensions/Servers/Proxmox/Livewire/System.php @@ -72,6 +72,12 @@ class System extends Component { parent::mount($server); + if ($this->server->node?->is_lxc) { + $this->notify('System hardware settings are only available for virtual machines.', 'info'); + $this->redirect(route('proxmox.server.overview', ['server' => $this->server->id]), navigate: true); + return; + } + $this->loadSystemConfiguration(false); $this->loadCapabilities(); } @@ -516,8 +522,8 @@ class System extends Component [$storage, $volume] = explode(':', $storageVolume, 2); - // Only delete from cephpool (RBD storage) for now - if ($storage !== 'cephpool') { + // Only delete known storage volume references + if (trim($storage) === '') { return; } @@ -589,6 +595,7 @@ class System extends Component $slot = 'efidisk0'; $volumeName = 'vm-' . $this->server->vm_id . '-efidisk0'; + $storagePool = $this->systemStoragePool(); try { $proxmox = new Proxmox; @@ -596,7 +603,7 @@ class System extends Component // For RBD storage, create the volume first try { $proxmox->request( - '/nodes/' . $this->server->node->name . '/storage/cephpool/content', + '/nodes/' . $this->server->node->name . '/storage/' . $storagePool . '/content', 'post', [ 'filename' => $volumeName, @@ -609,7 +616,7 @@ class System extends Component // Continue to add device to config } - $definition = 'cephpool:' . $volumeName . ',size=4M'; + $definition = $storagePool . ':' . $volumeName . ',size=4M'; if ($this->newEfiPreenroll) { $definition .= ',pre-enrolled-keys=1'; } @@ -652,6 +659,7 @@ class System extends Component } $volumeName = 'vm-' . $this->server->vm_id . '-tpm'; + $storagePool = $this->systemStoragePool(); try { $proxmox = new Proxmox; @@ -659,7 +667,7 @@ class System extends Component // For RBD storage, create the volume first try { $proxmox->request( - '/nodes/' . $this->server->node->name . '/storage/cephpool/content', + '/nodes/' . $this->server->node->name . '/storage/' . $storagePool . '/content', 'post', [ 'filename' => $volumeName, @@ -672,7 +680,7 @@ class System extends Component // Continue to add device to config } - $definition = 'cephpool:' . $volumeName . ',size=4M,version=' . $version; + $definition = $storagePool . ':' . $volumeName . ',size=4M,version=' . $version; $this->applyDeviceChange($slot, $definition, 'TPM device added.'); $this->newTpmVersion = 'v2.0'; @@ -698,6 +706,11 @@ class System extends Component } } + protected function systemStoragePool(): string + { + return trim((string) ($this->server->node->storage_location ?? '')) ?: 'cephpool'; + } + protected function storeOriginalValues(): void { $flags = array_values(array_unique($this->selectedCpuFlags ?? [])); diff --git a/extensions/Servers/Proxmox/Livewire/VNC.php b/extensions/Servers/Proxmox/Livewire/VNC.php index 1fb276967..7cbd28300 100644 --- a/extensions/Servers/Proxmox/Livewire/VNC.php +++ b/extensions/Servers/Proxmox/Livewire/VNC.php @@ -7,6 +7,10 @@ use Paymenter\Extensions\Servers\Proxmox\Proxmox; class VNC extends Component { + public ?string $vncUrl = null; + + public bool $vncLoadFailed = false; + public function mount(Server $server): void { parent::mount($server); @@ -15,26 +19,25 @@ class VNC extends Component if (!$this->server->node->vnc_enabled) { abort(403, 'VNC is not enabled on this server node.'); } + + try { + $proxmox = new Proxmox; + $this->vncUrl = $proxmox->vnc($this->server); + $this->vncLoadFailed = false; + } catch (\Throwable $e) { + $this->vncUrl = null; + $this->vncLoadFailed = true; + report($e); + $this->notify('Failed to connect to VNC console. Please try again.', 'error'); + } } public function render() { - try { - $proxmox = new Proxmox; - - return view('proxmox::vnc', [ - 'vnc' => $proxmox->vnc($this->server), - ])->layoutData([ - 'sidebar' => true, - ]); - } catch (\Throwable $e) { - $this->notify('Failed to connect to VNC console: ' . $e->getMessage(), 'error'); - - return view('proxmox::vnc', [ - 'vnc' => null, - ])->layoutData([ - 'sidebar' => true, - ]); - } + return view('proxmox::vnc', [ + 'vnc' => $this->vncUrl, + ])->layoutData([ + 'sidebar' => true, + ]); } } diff --git a/extensions/Servers/Proxmox/Livewire/XTerm.php b/extensions/Servers/Proxmox/Livewire/XTerm.php index 034dbf068..72a562d3b 100644 --- a/extensions/Servers/Proxmox/Livewire/XTerm.php +++ b/extensions/Servers/Proxmox/Livewire/XTerm.php @@ -7,6 +7,10 @@ use Paymenter\Extensions\Servers\Proxmox\Proxmox; class XTerm extends Component { + public ?string $xtermUrl = null; + + public bool $xtermLoadFailed = false; + public function mount(Server $server): void { parent::mount($server); @@ -15,27 +19,26 @@ class XTerm extends Component if (!$this->server->node->vnc_enabled) { abort(403, 'Console access is not enabled on this server node.'); } + + try { + $proxmox = new Proxmox; + $this->xtermUrl = $proxmox->xterm($this->server); + $this->xtermLoadFailed = false; + } catch (\Throwable $e) { + $this->xtermUrl = null; + $this->xtermLoadFailed = true; + report($e); + $this->notify('Failed to connect to console. Please try again.', 'error'); + } } public function render() { - try { - $proxmox = new Proxmox; - - return view('proxmox::xterm', [ - 'xterm' => $proxmox->xterm($this->server), - ])->layoutData([ - 'sidebar' => true, - ]); - } catch (\Throwable $e) { - $this->notify('Failed to connect to console: ' . $e->getMessage(), 'error'); - - return view('proxmox::xterm', [ - 'xterm' => null, - ])->layoutData([ - 'sidebar' => true, - ]); - } + return view('proxmox::xterm', [ + 'xterm' => $this->xtermUrl, + ])->layoutData([ + 'sidebar' => true, + ]); } } diff --git a/extensions/Servers/Proxmox/Models/Server.php b/extensions/Servers/Proxmox/Models/Server.php index 7d4d49dcf..312d6e1cd 100644 --- a/extensions/Servers/Proxmox/Models/Server.php +++ b/extensions/Servers/Proxmox/Models/Server.php @@ -3,7 +3,9 @@ namespace Paymenter\Extensions\Servers\Proxmox\Models; use App\Models\Service; +use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Crypt; class Server extends Model { @@ -31,6 +33,10 @@ class Server extends Model 'tag3color', ]; + protected $hidden = [ + 'password', + ]; + public function node() { return $this->belongsTo(Node::class); @@ -65,4 +71,37 @@ class Server extends Model { return $this->hasMany(ServerStatistics::class); } + + public function setPasswordAttribute(?string $value): void + { + if ($value === null || $value === '') { + $this->attributes['password'] = null; + return; + } + + $this->attributes['password'] = Crypt::encryptString($value); + } + + public function getPasswordAttribute(?string $value): ?string + { + if ($value === null || $value === '') { + return null; + } + + try { + return Crypt::decryptString($value); + } catch (DecryptException) { + // Refuse to expose undecipherable values to callers. + report(new \RuntimeException('Failed to decrypt stored Proxmox server password.')); + + return null; + } + } + + public function hasStoredPassword(): bool + { + $rawPassword = $this->getRawOriginal('password'); + + return is_string($rawPassword) && trim($rawPassword) !== ''; + } } diff --git a/extensions/Servers/Proxmox/Proxmox.php b/extensions/Servers/Proxmox/Proxmox.php index b31f9b0b8..e9103b955 100644 --- a/extensions/Servers/Proxmox/Proxmox.php +++ b/extensions/Servers/Proxmox/Proxmox.php @@ -7,6 +7,7 @@ use App\Models\Product; use App\Models\Service; use Exception; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\View; @@ -22,9 +23,17 @@ use Paymenter\Extensions\Servers\Proxmox\Models\Server; class Proxmox extends ClassServer { + private static bool $migrationsEnsured = false; + private const CONSOLE_CREDENTIAL_TTL_SECONDS = 1800; + public function enabled() { + if (self::$migrationsEnsured) { + return; + } + Artisan::call('migrate', ['--path' => 'extensions/Servers/Proxmox/database/migrations', '--force' => true]); + self::$migrationsEnsured = true; } public function boot() @@ -68,9 +77,11 @@ class Proxmox extends ClassServer }); } - public function request($url, $method = 'get', $data = []) + public function request($url, $method = 'get', $data = [], ?int $timeoutSeconds = null, ?int $connectTimeoutSeconds = null) { $url = rtrim($this->config('host'), '/') . '/api2/json' . $url; + $timeout = $timeoutSeconds ?? 15; + $connectTimeout = $connectTimeoutSeconds ?? 5; $response = Http::withHeaders([ 'Authorization' => 'PVEAPIToken=' . $this->config('user') . '=' . $this->config('token'), @@ -78,7 +89,7 @@ class Proxmox extends ClassServer 'Content-Type' => 'application/json', ])->withOptions([ 'verify' => $this->config('verify_ssl') ? true : false, - ])->timeout(15)->connectTimeout(5)->$method($url, $data)->throw(); + ])->timeout($timeout)->connectTimeout($connectTimeout)->$method($url, $data)->throw(); return $response; } @@ -140,13 +151,19 @@ class Proxmox extends ClassServer ]; } - $resourcePool = $this->request('/pools'); $poolList = []; - foreach ($resourcePool->json()['data'] ?? [] as $pool) { - $poolList[] = [ - 'label' => $pool['poolid'], - 'value' => $pool['poolid'], - ]; + try { + $resourcePool = $this->request('/pools'); + foreach ($resourcePool->json()['data'] ?? [] as $pool) { + $poolList[] = [ + 'label' => $pool['poolid'], + 'value' => $pool['poolid'], + ]; + } + } catch (\Throwable $e) { + \Log::warning('Unable to load Proxmox pools for product config', [ + 'error' => $e->getMessage(), + ]); } return [ @@ -406,12 +423,22 @@ class Proxmox extends ClassServer */ public function testConfig(): bool|string { - return true; + try { + $response = $this->request('/version'); + if ($response->successful()) { + return true; + } + + return 'Unable to connect to Proxmox API.'; + } catch (\Throwable $e) { + report($e); + return 'Unable to connect to Proxmox API.'; + } } - public function requestAndWait($url, $method = 'get', $data = [], $node = null) + public function requestAndWait($url, $method = 'get', $data = [], $node = null, int $timeoutSeconds = 300) { - $response = $this->request($url, $method, $data); + $response = $this->request($url, $method, $data, timeoutSeconds: 30, connectTimeoutSeconds: 10); // Check if the task is running $task = $response->json()['data'] ?? null; @@ -421,25 +448,44 @@ class Proxmox extends ClassServer return 'error'; } - return $this->waitForTask($task, $node); + return $this->waitForTask($task, $node, $timeoutSeconds); } - public function waitForTask($task, $node) + public function waitForTask($task, $node, int $timeoutSeconds = 300) { - // Check if the task is running (with max 300 iterations / 5 minute timeout) - $status = $this->request('/nodes/' . $node . '/tasks/' . $task . '/status')->json()['data']['status'] ?? null; - $iterations = 0; - while ($status == 'running' && $iterations < 300) { + if (!$node) { + \Log::warning('waitForTask called without node context', ['task' => $task]); + return 'error'; + } + + $deadline = now()->addSeconds(max(1, $timeoutSeconds))->timestamp; + $status = null; + $lastError = null; + + while (now()->timestamp <= $deadline) { + try { + $status = $this->request('/nodes/' . $node . '/tasks/' . $task . '/status', timeoutSeconds: 15, connectTimeoutSeconds: 5) + ->json()['data']['status'] ?? null; + } catch (\Throwable $e) { + $lastError = $e->getMessage(); + } + + if ($status !== 'running') { + return $status ?? 'error'; + } + sleep(1); - $status = $this->request('/nodes/' . $node . '/tasks/' . $task . '/status')->json()['data']['status'] ?? null; - $iterations++; } - if ($iterations >= 300) { - \Log::warning('Proxmox task timed out after 300 seconds', ['task' => $task, 'node' => $node]); - } + \Log::warning('Proxmox task timed out before completion', [ + 'task' => $task, + 'node' => $node, + 'timeout_seconds' => $timeoutSeconds, + 'last_status' => $status, + 'last_error' => $lastError, + ]); - return $status; + return 'timeout'; } private function generateMacAddress(): string @@ -839,23 +885,32 @@ class Proxmox extends ClassServer ]; } - public function recreateVncUser(Server $server) + public function recreateVncUser(Server $server, bool $forceRotate = false) { - // Check if user exists - try { - $this->request('/access/users/' . 'PAYMENTER_VNC_' . $server->id . '@pve', 'delete'); - } catch (Exception $e) { - // Handle exception + $cacheKey = $this->consoleCredentialCacheKey($server); + if (!$forceRotate) { + $cachedPassword = Cache::get($cacheKey); + if (is_string($cachedPassword) && trim($cachedPassword) !== '') { + return $cachedPassword; + } } + $userId = $this->consoleUserId($server); $password = Str::password(); - // Create user - $this->request('/access/users', 'post', [ - 'userid' => 'PAYMENTER_VNC_' . $server->id . '@pve', - 'enable' => 1, - 'password' => $password, - ]); + // Reuse existing user when possible, create if missing. + try { + $this->request('/access/users/' . $userId, 'put', [ + 'enable' => 1, + 'password' => $password, + ]); + } catch (Exception $e) { + $this->request('/access/users', 'post', [ + 'userid' => $userId, + 'enable' => 1, + 'password' => $password, + ]); + } // Ensure role exists with correct permissions try { @@ -875,22 +930,55 @@ class Proxmox extends ClassServer $this->request('/access/acl', 'PUT', [ 'path' => '/vms/' . $server->vm_id, 'roles' => 'PAYMENTER_VNC', - 'users' => 'PAYMENTER_VNC_' . $server->id . '@pve', + 'users' => $userId, ]); + Cache::put($cacheKey, $password, now()->addSeconds(self::CONSOLE_CREDENTIAL_TTL_SECONDS)); + return $password; } + private function consoleUserId(Server $server): string + { + return 'PAYMENTER_VNC_' . $server->id . '@pve'; + } + + private function consoleCredentialCacheKey(Server $server): string + { + return 'proxmox-console-user-password-' . $server->id; + } + + private function createConsoleTicket(Server $server): array + { + $attempts = [false, true]; + + foreach ($attempts as $forceRotate) { + try { + $password = $this->recreateVncUser($server, $forceRotate); + + $response = Http::withOptions([ + 'verify' => $this->config('verify_ssl') ? true : false, + ])->post(rtrim($this->config('host'), '/') . '/api2/json/access/ticket', [ + 'username' => $this->consoleUserId($server), + 'password' => $password, + ])->throw()->json()['data'] ?? []; + + if (!empty($response['ticket'])) { + return $response; + } + } catch (\Throwable $e) { + if ($forceRotate) { + throw $e; + } + } + } + + throw new \RuntimeException('Failed to obtain Proxmox authentication ticket.'); + } + public function vnc(Server $server) { - $password = $this->recreateVncUser($server); - // curl -k -d 'username=root@pam' --data-urlencode 'password=xxxxxxxxx' https://10.0.0.1:8006/api2/json/access/ticket - $response = Http::withOptions([ - 'verify' => $this->config('verify_ssl') ? true : false, - ])->post(rtrim($this->config('host'), '/') . '/api2/json/access/ticket', [ - 'username' => 'PAYMENTER_VNC_' . $server->id . '@pve', - 'password' => $password, - ])->throw()->json()['data'] ?? []; + $response = $this->createConsoleTicket($server); if (empty($response['ticket']) || empty($response['CSRFPreventionToken'])) { throw new \RuntimeException('Failed to obtain Proxmox authentication ticket.'); @@ -911,16 +999,7 @@ class Proxmox extends ClassServer 'websocket' => 1, ])->throw()->json()['data'] ?? []; - // Current - $paymenterDomain = rtrim(config('settings.app_url'), '/'); - // Remove port and http - $paymenterDomain = preg_replace('/^https?:\/\//', '', $paymenterDomain); - $paymenterDomain = preg_replace('/:\d+$/', '', $paymenterDomain); - $parts = explode('.', $paymenterDomain); - if (count($parts) >= 2) { - $paymenterDomain = $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1]; - } - setcookie('PVEAuthCookie', $response['ticket'], 0, '/', $paymenterDomain, true, true); + $this->setConsoleAuthCookie($response['ticket']); $consoleType = $server->node->is_lxc ? 'lxc' : 'kvm'; $vmType = $server->node->vmType(); @@ -936,14 +1015,7 @@ class Proxmox extends ClassServer public function xterm(Server $server) { - $password = $this->recreateVncUser($server); - // curl -k -d 'username=root@pam' --data-urlencode 'password=xxxxxxxxx' https://10.0.0.1:8006/api2/json/access/ticket - $response = Http::withOptions([ - 'verify' => $this->config('verify_ssl') ? true : false, - ])->post(rtrim($this->config('host'), '/') . '/api2/json/access/ticket', [ - 'username' => 'PAYMENTER_VNC_' . $server->id . '@pve', - 'password' => $password, - ])->throw()->json()['data'] ?? []; + $response = $this->createConsoleTicket($server); if (empty($response['ticket'])) { throw new \RuntimeException('Failed to obtain Proxmox authentication ticket.'); @@ -954,16 +1026,7 @@ class Proxmox extends ClassServer $domain = preg_replace('/^https?:\/\//', '', $domain); $domain = preg_replace('/:\d+$/', '', $domain); - // Current - $paymenterDomain = rtrim(config('settings.app_url'), '/'); - // Remove port and http - $paymenterDomain = preg_replace('/^https?:\/\//', '', $paymenterDomain); - $paymenterDomain = preg_replace('/:\d+$/', '', $paymenterDomain); - $parts = explode('.', $paymenterDomain); - if (count($parts) >= 2) { - $paymenterDomain = $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1]; - } - setcookie('PVEAuthCookie', $response['ticket'], 0, '/', $paymenterDomain, true, true); + $this->setConsoleAuthCookie($response['ticket']); $consoleType = $server->node->is_lxc ? 'lxc' : 'kvm'; @@ -974,6 +1037,35 @@ class Proxmox extends ClassServer '&vmid=' . $server->vm_id; } + private function setConsoleAuthCookie(string $ticket): void + { + $cookieDomain = $this->derivePaymenterCookieDomain(); + + setcookie('PVEAuthCookie', $ticket, 0, '/', $cookieDomain ?? '', true, true); + } + + private function derivePaymenterCookieDomain(): ?string + { + $appUrl = (string) config('settings.app_url'); + if (trim($appUrl) === '') { + return null; + } + + $host = parse_url($appUrl, PHP_URL_HOST); + if (!is_string($host) || trim($host) === '') { + return null; + } + + $host = strtolower(trim($host)); + + // Domain cookies are invalid for IPs/localhost/single-label hosts. + if (filter_var($host, FILTER_VALIDATE_IP) || !str_contains($host, '.')) { + return null; + } + + return $host; + } + /** * Create a server * diff --git a/extensions/Servers/Proxmox/Services/CoreRouterClient.php b/extensions/Servers/Proxmox/Services/CoreRouterClient.php index 1ee11d094..46dbd73e9 100644 --- a/extensions/Servers/Proxmox/Services/CoreRouterClient.php +++ b/extensions/Servers/Proxmox/Services/CoreRouterClient.php @@ -56,9 +56,12 @@ class CoreRouterClient $pageCount = 1; $hasNextLink = isset($links['next']) && !empty($links['next']); $gotFullLimit = count($allStates) >= $stateLimit; - $maxPages = 1000; // Safety limit to prevent infinite loops (should never reach this) + $maxPages = max(1, (int) config('corerouter.max_pages', 25)); + $maxStatesTotal = max($stateLimit, (int) config('corerouter.max_states_total', 5000)); + $fullLimitProbeCount = 0; + $maxFullLimitProbes = max(0, (int) config('corerouter.max_full_limit_probes', 1)); - while (($hasNextLink || $gotFullLimit) && $pageCount < $maxPages) { + while (($hasNextLink || ($gotFullLimit && $fullLimitProbeCount < $maxFullLimitProbes)) && $pageCount < $maxPages) { try { // Extract offset and limit from next URL or increment offset $offset = count($allStates); @@ -73,9 +76,21 @@ class CoreRouterClient if (is_array($nextData) && !empty($nextData)) { $allStates = array_merge($allStates, $nextData); + if (count($allStates) >= $maxStatesTotal) { + $allStates = array_slice($allStates, 0, $maxStatesTotal); + \Log::warning('Core Router API state collection capped to configured max', [ + 'max_states_total' => $maxStatesTotal, + 'pages_fetched' => $pageCount, + ]); + break; + } + $links = Arr::get($nextResponse, '_links', []); $hasNextLink = isset($links['next']) && !empty($links['next']); $gotFullLimit = count($nextData) >= $stateLimit; + if (!$hasNextLink && $gotFullLimit) { + $fullLimitProbeCount++; + } $pageCount++; // Log progress every 10 pages to track fetching @@ -104,6 +119,7 @@ class CoreRouterClient \Log::warning('Core Router API pagination reached safety limit', [ 'pages' => $pageCount, 'total_states' => count($allStates), + 'max_pages' => $maxPages, ]); } diff --git a/extensions/Servers/Proxmox/TechnitiumDns.php b/extensions/Servers/Proxmox/TechnitiumDns.php index 1a11907e0..ba1e42746 100644 --- a/extensions/Servers/Proxmox/TechnitiumDns.php +++ b/extensions/Servers/Proxmox/TechnitiumDns.php @@ -36,13 +36,25 @@ class TechnitiumDns private function request(string $endpoint, string $method = 'get', array $data = []): Response { $url = rtrim($this->baseUrl, '/') . '/api' . $endpoint; - - $params = array_merge(['token' => $this->token], $data); - + + $client = Http::timeout(10) + ->connectTimeout(5) + ->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + 'X-API-Token' => $this->token, + ]); + if ($method === 'get') { - return Http::timeout(10)->connectTimeout(5)->get($url, $params); + // Prefer token headers to keep secrets out of query strings and access logs. + $response = $client->get($url, $data); + if ($response->successful()) { + return $response; + } + + // Compatibility fallback for deployments that only accept token query params. + return $client->get($url, array_merge(['token' => $this->token], $data)); } else { - return Http::timeout(10)->connectTimeout(5)->asForm()->post($url, $params); + return $client->asForm()->post($url, array_merge(['token' => $this->token], $data)); } } diff --git a/extensions/Servers/Proxmox/views/backups.blade.php b/extensions/Servers/Proxmox/views/backups.blade.php index 78d3d0571..3a5fcbc57 100644 --- a/extensions/Servers/Proxmox/views/backups.blade.php +++ b/extensions/Servers/Proxmox/views/backups.blade.php @@ -1,67 +1,27 @@ - -
+ +

Backup Details

-
- - - - - + +
@@ -109,18 +69,19 @@
-
@@ -150,7 +111,7 @@
- Please ensure that the virtual private server is powered off before proceeding. If the server is not powered off, the restore will fail. + The panel will try to stop the virtual private server before restoring. If stop fails due to a non-recoverable state, the restore will be blocked.
diff --git a/extensions/Servers/Proxmox/views/backups/create.blade.php b/extensions/Servers/Proxmox/views/backups/create.blade.php index 381ede8b6..832733a39 100644 --- a/extensions/Servers/Proxmox/views/backups/create.blade.php +++ b/extensions/Servers/Proxmox/views/backups/create.blade.php @@ -1,4 +1,4 @@ -
+
+
Overview @@ -266,8 +266,8 @@ wire:loading.attr="disabled" class="text-xs font-semibold text-error hover:text-error/80"> - Remove - Removing... + Remove + Removing...
@@ -294,16 +294,16 @@ class="flex-1 sm:flex-initial" :disabled="($currentIsos[$drive] ?? 'none') === ($selectedIsos[$drive] ?? 'none')"> - Apply - Applying... + Apply + Applying...
- Eject - Ejecting... + Eject + Ejecting...
diff --git a/extensions/Servers/Proxmox/views/cloud-init.blade.php b/extensions/Servers/Proxmox/views/cloud-init.blade.php index 49209e81e..4eebdb50f 100644 --- a/extensions/Servers/Proxmox/views/cloud-init.blade.php +++ b/extensions/Servers/Proxmox/views/cloud-init.blade.php @@ -1,4 +1,4 @@ - + - -
- - {{-- Page Header --}} -
- - - -

- The Firewall Management interface provides comprehensive control over your virtual machine's network security, including rule-based filtering, IP aliases, and security policies. -

-
-

Firewall Rules: Create granular rules to allow or block specific traffic patterns.

-

IP Aliases: Define named groups of IP addresses for easier rule management.

-

IP Sets: Manage collections of IP addresses for advanced filtering.

-

Security Logs: Monitor firewall activity and blocked attempts.

-
-
-
-
-
- - {{-- Error Modal --}} - @if($error && $showErrorModal) - - - - -

{{ $error }}

- - - Close - - -
- @endif - - {{-- Master Firewall Control --}} -
-
-
- -

Master Firewall Control

-
- - @if($status->status === 'running') - - Running - @else - - Stopped - @endif - -
- -
-
-
- -
-
-

Firewall Status

-

- @if($firewallEnabled) - - - - - - Active - Your VM is protected - - @else - - - Disabled - No firewall protection - - @endif -

-
-
-
-
- - {{ $firewallEnabled ? 'Enabled' : 'Disabled' }} - - -
- - - {{ $firewallEnabled ? 'Disabling...' : 'Enabling...' }} - -
-
-
- - @if($firewallEnabled) - {{-- Firewall Configuration --}} -
- - {{-- Default Policies --}} -
-
- -

Default Policies

-
- -
- - - - - - - - - - - - - - Update Policies - Updating... - -
-
- - {{-- Network Adapters --}} -
-
- -

Network Adapters

-
- -
- @if(count($networkAdapters) > 0) -
- @foreach($networkAdapters as $adapter) -
-
-
- {{ $adapter['name'] }} - - {{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'Active' : 'Off' }} - -
-
- {{ ucfirst($adapter['model'] ?? 'Unknown') }} - @if($adapter['mac']) - {{ $adapter['mac'] }} - @endif -
-
-
- - {{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'On' : 'Off' }} - - - - - -
-
- @endforeach -
- @else - - @endif -
-
-
- - {{-- Firewall Management Tabs --}} -
-
- - - - - -
- -
- {{-- RULES TAB --}} -
-
-

Firewall Rules

- -
- - @if(count($rules) > 0) -
- - - - - - - - - - - - - - - - - @foreach($rules as $index => $rule) - - - - - - - - - - - - - @endforeach - -
OrderPosActionDirectionSourceDestinationProtocolPortsStatusActions
-
- @if($index > 0) - - @else -
- @endif - @if($index < count($rules) - 1) - - @else -
- @endif -
-
{{ $rule['pos'] }} - - {{ $rule['action'] }} - - {{ $rule['direction'] ?? 'IN' }}{{ $rule['source'] ?? '-' }}{{ $rule['dest'] ?? '-' }}{{ $rule['proto'] ?? '-' }}{{ $rule['dport'] ?? $rule['sport'] ?? '-' }} - - @if($rule['enable']) - - Enabled - @else - Disabled - @endif - - -
- - - -
-
-
- @else - - - - Add Rule - - - - @endif -
- - {{-- ALIASES TAB --}} -
-
-

IP Aliases

- -
- - @if(count($aliases) > 0) -
- @foreach($aliases as $alias) -
-
-
-

{{ $alias['name'] }}

-

{{ $alias['comment'] ?? 'No description' }}

-
-
- - -
-
-
- @if($alias['cidr']) - @foreach(explode(',', $alias['cidr']) as $cidr) -
- - {{ trim($cidr) }} -
- @endforeach - @endif -
-
- @endforeach -
- @else - - - - Add Alias - - - - @endif -
- - {{-- IPSETS TAB --}} -
-
-

IP Sets

- -
- - @if(count($ipsets) > 0) -
- @foreach($ipsets as $ipset) -
-
-
-

{{ $ipset['name'] }}

-

{{ $ipset['comment'] ?? 'No description' }}

-
-
- - -
-
-
-
- Addresses: - {{ count($ipset['cidr'] ?? []) }} -
- @if(isset($ipset['cidr']) && count($ipset['cidr']) > 0) -
- @foreach(array_slice($ipset['cidr'], 0, 5) as $cidr) - {{ $cidr }} - @endforeach - @if(count($ipset['cidr']) > 5) -

... and {{ count($ipset['cidr']) - 5 }} more

- @endif -
- @endif -
-
- @endforeach -
- @else - - - - Add IP Set - - - - @endif -
- - {{-- LOGS TAB --}} -
-
-

Firewall Logs

-
- - Refresh - - - Clear Logs - -
-
- - @if(count($firewallLogs) > 0) -
- @foreach($firewallLogs as $log) -
-
-
- - {{ $log['action'] }} - - {{ $log['time'] }} -
- {{ $log['interface'] ?? 'N/A' }} -
-
-
- Source -
{{ $log['src'] ?? 'N/A' }}
-
-
- Destination -
{{ $log['dst'] ?? 'N/A' }}
-
-
- Protocol -
{{ $log['proto'] ?? 'N/A' }}
-
-
- Port -
{{ $log['dport'] ?? $log['sport'] ?? 'N/A' }}
-
-
-
- @endforeach -
- @else - - @endif -
- - {{-- SAVED CONFIGURATIONS TAB --}} -
-
-

Saved Configurations

- - Save Current Config - -
- - @if(count($savedConfigurations) > 0) -
- @foreach($savedConfigurations as $config) -
-
-
-

{{ $config['name'] ?? 'Configuration ' . $config['id'] }}

-

{{ $config['description'] ?? 'No description' }}

-

{{ $config['created_at'] }}

-
-
- - -
-
-
-
Rules: {{ $config['rules_count'] ?? 0 }}
-
Aliases: {{ $config['aliases_count'] ?? 0 }}
-
IP Sets: {{ $config['ipsets_count'] ?? 0 }}
-
-
- @endforeach -
- @else - - - - Save Current Config - - - - @endif -
-
-
- - {{-- Policy Info Modal --}} - @if($showPolicyInfoModal) -
-
- -
-
-
- -
-
-

Understanding Firewall Policies

-

How default policies control your network traffic

-
-
- -
-
-

- - Policy In (Incoming Traffic) -

-

Controls what happens to traffic coming INTO your VM when it doesn't match any explicit firewall rule.

-
    -
  • ACCEPT: Allow all incoming traffic by default (permissive, requires explicit DENY rules)
  • -
  • REJECT: Block incoming traffic and send rejection notification to sender
  • -
  • DROP: Silently discard incoming traffic without notifying sender (recommended)
  • -
-
- -
-

- - Policy Out (Outgoing Traffic) -

-

Controls what happens to traffic going OUT of your VM when it doesn't match any explicit firewall rule.

-
    -
  • ACCEPT: Allow all outgoing traffic by default (recommended for most VMs)
  • -
  • REJECT: Block outgoing traffic and notify your VM
  • -
  • DROP: Silently discard outgoing traffic
  • -
-
- -
-

- - Creating a Whitelist Configuration -

-

To create a strict whitelist (only allow specific traffic):

-
    -
  1. Set Policy In to DROP
  2. -
  3. Set Policy Out to DROP (if restricting outbound)
  4. -
  5. Create explicit ACCEPT rules for allowed traffic
  6. -
  7. Everything not explicitly allowed will be blocked
  8. -
-
- -
-

Additional Filters

-

IP Filter: Validates that source/destination IP addresses in packets match the VM's assigned IPs. Helps prevent IP spoofing.

-

MAC Filter: Validates that MAC addresses match the VM's network adapters. Provides additional layer of security.

-
-
- -
- -
-
-
-
- @endif - - {{-- Rule Form Modal --}} - @if($showRuleForm) -
-
- -
-

{{ $editingRuleIndex !== null ? 'Edit' : 'Create' }} Firewall Rule

- -
- {{-- Action --}} -
- - -
- - {{-- Type --}} -
- - -
- - {{-- Macro --}} -
- - -

- - Selecting a macro will automatically set the protocol and port(s) -

-
- - {{-- Interface --}} -
- - -
- - {{-- Source --}} -
- - -

Can use alias names or leave empty for any source

-
- - {{-- Destination --}} -
- - -
- - {{-- Protocol --}} -
- - -
- - {{-- Destination Port --}} -
- - -
- - {{-- Source Port --}} -
- - -
- - {{-- Log Level --}} -
- - -
- - {{-- Comment --}} -
- - -
- - {{-- Enable --}} -
- - -
-
- -
- - -
-
-
-
- @endif - - {{-- Delete Rule Modal --}} - @if($showDeleteRuleModal && $deletingRuleIndex !== null && isset($rules[$deletingRuleIndex])) - @php $ruleToDelete = $rules[$deletingRuleIndex]; @endphp -
-
- -
-
- -
-
-

Delete Firewall Rule

-

Are you sure you want to delete this rule? This action cannot be undone.

-
-
Action: {{ $ruleToDelete['action'] ?? '-' }}
-
Source: {{ $ruleToDelete['source'] ?? 'Any' }}
-
Destination: {{ $ruleToDelete['dest'] ?? 'Any' }}
-
-
-
-
- - Cancel - - - Delete Rule - Deleting... - -
-
-
- @endif - - {{-- Alias Form Modal --}} - @if($showAliasForm) -
-
- -

{{ $editingAliasName ? 'Edit' : 'Create' }} IP Alias

- -
-
- - - @error('aliasName') {{ $message }} @enderror -
- -
- - - @error('aliasCidr') {{ $message }} @enderror -
- -
- - -
-
- -
- - -
-
-
- @endif - - {{-- Delete Alias Modal --}} - @if($showDeleteAliasModal && $deletingAliasName) -
-
- -
-
- -
-
-

Delete IP Alias

-

Delete alias "{{ $deletingAliasName }}"?

-

This may affect firewall rules using this alias.

-
-
-
- - Cancel - - - Delete - Deleting... - -
-
-
- @endif - - {{-- IPSet Form Modal --}} - @if($showIPSetForm) -
-
- -

Create IP Set

- -
-
- - - @error('ipsetName') {{ $message }} @enderror -
- -
- - -
- -
-

After creating the IPSet, you can add IP addresses and CIDR ranges to it.

-
-
- -
- - -
-
-
- @endif - - {{-- Delete IPSet Modal --}} - @if($showDeleteIPSetModal && $deletingIPSetName) -
-
- -
-
- -
-
-

Delete IP Set

-

Delete IPSet "{{ $deletingIPSetName }}"?

-

All members will be removed and rules using this IPSet may be affected.

-
-
-
- - Cancel - - - Delete - Deleting... - -
-
-
- @endif - - {{-- IPSet Member Form Modal --}} - @if($showIPSetMemberForm && $selectedIPSetName) -
-
- -

Add IP/CIDR to {{ $selectedIPSetName }}

- -
-
- - - @error('memberCidr') {{ $message }} @enderror -
- -
- - -
- -
- - -
-
- -
- - -
-
-
- @endif - - {{-- Delete IPSet Member Modal --}} - @if($showDeleteIPSetMemberModal && $deletingMemberCidr) -
-
- -
-
- -
-
-

Remove Member

-

Remove "{{ $deletingMemberCidr }}" from {{ $selectedIPSetName }}?

-
-
-
- - Cancel - - - Remove - Removing... - -
-
-
- @endif - - {{-- Save Configuration Modal --}} - @if($showSaveConfigModal) -
-
- -
-
- -
-
-

Save Current Configuration

-

Save your current firewall rules and policies as a reusable configuration

-
-
- -
-
- - - @error('newConfigName') -

{{ $message }}

- @enderror -
- -
-

Configuration Summary

-
-
- Rules - {{ count($rules) }} -
-
- Incoming Policy - {{ $policyIn }} -
-
- Outgoing Policy - {{ $policyOut }} -
-
-
-
- -
- - -
-
-
- @endif - -
- - @endif -
\ No newline at end of file diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php.old b/extensions/Servers/Proxmox/views/firewall.blade.php.old deleted file mode 100644 index 3c8899eea..000000000 --- a/extensions/Servers/Proxmox/views/firewall.blade.php.old +++ /dev/null @@ -1,2636 +0,0 @@ - - {{-- Global Loader Styles --}} - - -
- {{-- Page Header --}} -
- -
- - {{-- Error Modal --}} - @if($error && $showErrorModal) - - - - -

{{ $error }}

- - - Close - - -
- @endif - - {{-- Main Firewall Toggle & Status --}} -
- {{-- Decorative elements --}} -
-
- -
-
-
- -
-
-

Master Firewall Control

-

- @if($firewallEnabled) - - - - - - Active - Your VM is protected - - @else - - - Disabled - No firewall protection - - @endif -

-
-
-
-
- - {{ $firewallEnabled ? 'Enabled' : 'Disabled' }} - - -
- - - {{ $firewallEnabled ? 'Disabling...' : 'Enabling...' }} - -
-
-
- - {{-- Firewall Policies & Network Adapters Grid --}} - @if($firewallEnabled) -
- {{-- Default Policies Card --}} - -
-
-
- -
-
-

Default Policies

-

Control default traffic behavior

-
-
- -
- -
- - - - - - - - - - - - - - Update Policies - Updating... - -
-
- - {{-- Network Adapters --}} - -
-
- -
-
-

Network Adapters

-

Per-device firewall control

-
-
- - @if(count($networkAdapters) > 0) -
- @foreach($networkAdapters as $adapter) -
-
-
-
- {{ $adapter['name'] }} - - {{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'Active' : 'Off' }} - -
-
- {{ ucfirst($adapter['model'] ?? 'Unknown') }} - @if($adapter['mac']) - {{ $adapter['mac'] }} - @endif -
-
-
- @if($adapterFirewallStates[$adapter['name']] ?? false) - - Disable - ... - - @else - - Enable - ... - - @endif -
-
-
- @endforeach -
- @else - - @endif -
-
- - {{-- Policy Info Modal --}} - @if($showPolicyInfoModal) -
-
- -
-
-
- -
-
-

Understanding Firewall Policies

-

How default policies control your network traffic

-
-
- -
-
-

- - Policy In (Incoming Traffic) -

-

Controls what happens to traffic coming INTO your VM when it doesn't match any explicit firewall rule.

-
    -
  • ACCEPT: Allow all incoming traffic by default (permissive, requires explicit DENY rules)
  • -
  • REJECT: Block incoming traffic and send rejection notification to sender
  • -
  • DROP: Silently discard incoming traffic without notifying sender (recommended)
  • -
-
- -
-

- - Policy Out (Outgoing Traffic) -

-

Controls what happens to traffic going OUT of your VM when it doesn't match any explicit firewall rule.

-
    -
  • ACCEPT: Allow all outgoing traffic by default (recommended for most VMs)
  • -
  • REJECT: Block outgoing traffic and notify your VM
  • -
  • DROP: Silently discard outgoing traffic
  • -
-
- -
-

- - Creating a Whitelist Configuration -

-

To create a strict whitelist (only allow specific traffic):

-
    -
  1. Set Policy In to DROP
  2. -
  3. Set Policy Out to DROP (if restricting outbound)
  4. -
  5. Create explicit ACCEPT rules for allowed traffic
  6. -
  7. Everything not explicitly allowed will be blocked
  8. -
-
- -
-

Additional Filters

-

IP Filter: Validates that source/destination IP addresses in packets match the VM's assigned IPs. Helps prevent IP spoofing.

-

MAC Filter: Validates that MAC addresses match the VM's network adapters. Provides additional layer of security.

-
-
- -
- -
-
-
-
- @endif - - {{-- Tabs Navigation --}} -
-
- - - - - -
- - {{-- Tab Contents --}} -
- {{-- RULES TAB --}} -
-
-

Firewall Rules

- -
- - @if(count($rules) > 0) -
- - - - - - - - - - - - - - - - - @foreach($rules as $index => $rule) - - - - - - - - - - - - - @endforeach - -
OrderPosActionDirectionSourceDestinationProtocolPortsStatusActions
-
- @if($index > 0) - - @else -
- @endif - @if($index < count($rules) - 1) - - @else -
- @endif -
-
{{ $rule['pos'] ?? '-' }} - @php - $actionClass = match($rule['action'] ?? '') { - 'ACCEPT' => 'bg-success/20 text-success', - 'REJECT' => 'bg-warning/20 text-warning', - 'DROP' => 'bg-error/20 text-error', - default => 'bg-base/20 text-base/60' - }; - @endphp - - {{ $rule['action'] ?? '-' }} - - - - {{ strtoupper($rule['type'] ?? 'IN') }} - - -
- {{ $rule['source'] ?? '-' }} - @php $ruleSourceIP = $rule['source'] ?? ''; @endphp - @if($ruleSourceIP && $ruleSourceIP !== '-' && $this->checkIsPublicIP($ruleSourceIP)) -
- - - {{-- IP Location Modal (same as logs) --}} - -
- @endif -
-
-
- {{ $rule['dest'] ?? '-' }} - @php $ruleDestIP = $rule['dest'] ?? ''; @endphp - @if($ruleDestIP && $ruleDestIP !== '-' && $this->checkIsPublicIP($ruleDestIP)) -
- - - {{-- IP Location Modal (same structure) --}} - -
- @endif -
-
{{ strtoupper($rule['proto'] ?? ($rule['macro'] ?? 'ANY')) }}{{ $rule['dport'] ?? $rule['sport'] ?? '-' }} - @if(isset($rule['enable']) && $rule['enable'] == 0) - Disabled - @else - Enabled - @endif - -
- - - -
-
-
- @else -
- -

No Firewall Rules

-

Create your first rule to control network traffic

- -
- @endif -
- - {{-- ALIASES TAB --}} -
-
-
-

IP Aliases

-

Create named references to IP addresses or CIDR ranges

-
- -
- - @if(count($aliases) > 0) -
- @foreach($aliases as $alias) -
-
-
-
{{ $alias['name'] }}
-
{{ $alias['cidr'] }}
- @if(!empty($alias['comment'])) -
{{ $alias['comment'] }}
- @endif -
-
- - -
-
-
- @endforeach -
- @else -
- -

No IP Aliases

-

Create aliases to simplify rule management

- -
- @endif -
- - {{-- IPSETS TAB --}} -
-
-
-

IP Sets

-

Group multiple IPs/CIDRs for easier rule management

-
- -
- -
- {{-- IPSets List --}} -
-

Available IP Sets

- @if(count($ipsets) > 0) -
- @foreach($ipsets as $ipset) -
-
-
-
{{ $ipset['name'] }}
- @if(!empty($ipset['comment'])) -
{{ $ipset['comment'] }}
- @endif -
-
- -
-
-
- @endforeach -
- @else -
- -

No IP Sets created yet

-
- @endif -
- - {{-- IPSet Members --}} -
- @if($selectedIPSetName) -
-

Members of: {{ $selectedIPSetName }}

- -
- - @if(count($ipsetMembers) > 0) -
- @foreach($ipsetMembers as $member) -
-
-
-
{{ $member['cidr'] }}
- @if(!empty($member['comment'])) -
{{ $member['comment'] }}
- @endif - @if(isset($member['nomatch']) && $member['nomatch']) - No Match - @endif -
- -
-
- @endforeach -
- @else -
- -

No members in this IPSet

- -
- @endif - @else -
- -

Select an IP Set to view its members

-
- @endif -
-
-
- - {{-- LOGS TAB --}} -
-
-
-

Firewall Logs

-
-

Showing the latest logs first (most recent at top)

- @if($lastLogRefresh) - • Last updated: {{ $lastLogRefresh }} - @endif - • Auto-refreshes every 30s -
-
-
- - -
-
- - @if(!empty($logError)) -
-
- -

{{ $logError }}

-
-
- @endif - - @if(!$logsLoaded && count($firewallLogs) === 0) -
-
-

Loading firewall logs...

-
- @elseif(count($firewallLogs) > 0) -
- - - - - - - - - - - - - - - @foreach($this->paginatedLogs as $log) - - - - - - - - - - - @endforeach - -
TimeActionSourceDestinationProtocolSrc PortDest PortAnalysis
- @if(isset($log['timestamp_iso']) && $log['timestamp_iso'] !== '-') -
- @else - {{ $log['timestamp_display'] ?? $log['timestamp'] ?? '-' }} - @endif -
- - {{ $log['action'] ?? '-' }} - - -
- {{ $log['source'] ?? '-' }} - @php $sourceIP = $log['source'] ?? ''; @endphp - @if($sourceIP && $sourceIP !== '-' && $this->checkIsPublicIP($sourceIP)) -
- - - {{-- IP Location Modal --}} - -
- @endif -
-
-
- {{ $log['destination'] ?? '-' }} - @php $destIP = $log['destination'] ?? ''; @endphp - @if($destIP && $destIP !== '-' && $this->checkIsPublicIP($destIP)) -
- - - {{-- IP Location Modal --}} - -
- @endif -
-
{{ $log['protocol'] ?? '-' }}{{ $log['source_port'] ?? '-' }}{{ $log['destination_port'] ?? '-' }} - @php - $sourceIP = $log['source'] ?? ''; - $destIP = $log['destination'] ?? ''; - $sourcePort = $log['source_port'] ?? ''; - $destPort = $log['destination_port'] ?? ''; - $protocol = $log['protocol'] ?? ''; - @endphp - @if($coreRouterConfigured && $sourceIP && $sourceIP !== '-') -
- - - {{-- Network Analysis Modal --}} - -
- @endif -
-
- - {{-- Pagination Controls --}} -
- {{-- Records per page selector --}} -
- - - per page -
- - {{-- Page info and navigation --}} -
- - Showing {{ (($currentPage - 1) * $perPage) + 1 }} - {{ min($currentPage * $perPage, count($firewallLogs)) }} of {{ count($firewallLogs) }} logs - - -
- {{-- First page --}} - - - {{-- Previous page --}} - - - {{-- Page numbers --}} - @php - $totalPages = $this->totalPages; - $startPage = max(1, $currentPage - 2); - $endPage = min($totalPages, $currentPage + 2); - @endphp - - @if($startPage > 1) - - @if($startPage > 2) - ... - @endif - @endif - - @for($i = $startPage; $i <= $endPage; $i++) - - @endfor - - @if($endPage < $totalPages) - @if($endPage < $totalPages - 1) - ... - @endif - - @endif - - {{-- Next page --}} - - - {{-- Last page --}} - -
-
-
- @else -
- -

No Firewall Logs

-

Logs will appear here when the firewall blocks or allows traffic

-
- @endif -
- - {{-- SAVED CONFIGURATIONS TAB --}} -
-
-
-

Saved Configurations

-

Save and reuse firewall configurations across your VPS instances

-
-
- {{-- Apply to All Button --}} - - {{-- Save Current Configuration Button --}} - -
-
- - @if(count($savedConfigurations) > 0) -
- - - - - - - - - - - - @foreach($savedConfigurations as $config) - - - - - - - - @endforeach - -
Configuration NameRulesPoliciesCreatedActions
-
-
- -
- {{ $config['configuration_name'] }} -
-
- - {{ is_array($config['firewall_rules']) ? count($config['firewall_rules']) : 0 }} rules - - -
- - IN: {{ $config['incoming_policy'] }} - - - OUT: {{ $config['outgoing_policy'] }} - -
-
- {{ \Carbon\Carbon::parse($config['created_at'])->format('M d, Y H:i') }} - -
- - - - -
-
-
- @else -
- -

No Saved Configurations

-

Save your current firewall configuration to reuse it later or apply it to other VPS instances

-
- @endif -
-
-
- @else -
-
-
- -
-

Firewall Protection Disabled

-

Your virtual machine is currently unprotected. Enable the firewall above to secure your network traffic with customizable rules.

-
-
-
- -

Traffic Control

-
-

Define rules to allow or block specific connections

-
-
-
- -

Monitor Activity

-
-

Track and log all firewall events in real-time

-
-
-
- -

Flexible Policies

-
-

Configure default behavior for incoming and outgoing traffic

-
-
-
-
- @endif - - {{-- Rule Form Modal --}} - @if($showRuleForm) -
-
- -
-

{{ $editingRuleIndex !== null ? 'Edit' : 'Create' }} Firewall Rule

- -
- {{-- Action --}} -
- - -
- - {{-- Type --}} -
- - -
- - {{-- Macro --}} -
- - -

- - Selecting a macro will automatically set the protocol and port(s) -

-
- - {{-- Interface --}} -
- - -
- - {{-- Source --}} -
- - -

Can use alias names or leave empty for any source

-
- - {{-- Destination --}} -
- - -
- - {{-- Protocol --}} -
- - -
- - {{-- Destination Port --}} -
- - -
- - {{-- Source Port --}} -
- - -
- - {{-- Log Level --}} -
- - -
- - {{-- Comment --}} -
- - -
- - {{-- Enable --}} -
- - -
-
- -
- - -
-
-
-
- @endif - - {{-- Delete Rule Modal --}} - @if($showDeleteRuleModal && $deletingRuleIndex !== null && isset($rules[$deletingRuleIndex])) - @php $ruleToDelete = $rules[$deletingRuleIndex]; @endphp -
-
- -
-
- -
-
-

Delete Firewall Rule

-

Are you sure you want to delete this rule? This action cannot be undone.

-
-
Action: {{ $ruleToDelete['action'] ?? '-' }}
-
Source: {{ $ruleToDelete['source'] ?? 'Any' }}
-
Destination: {{ $ruleToDelete['dest'] ?? 'Any' }}
-
-
-
-
- - Cancel - - - Delete Rule - Deleting... - -
-
-
- @endif - - {{-- Alias Form Modal --}} - @if($showAliasForm) -
-
- -

{{ $editingAliasName ? 'Edit' : 'Create' }} IP Alias

- -
-
- - - @error('aliasName') {{ $message }} @enderror -
- -
- - - @error('aliasCidr') {{ $message }} @enderror -
- -
- - -
-
- -
- - -
-
-
- @endif - - {{-- Delete Alias Modal --}} - @if($showDeleteAliasModal && $deletingAliasName) -
-
- -
-
- -
-
-

Delete IP Alias

-

Delete alias "{{ $deletingAliasName }}"?

-

This may affect firewall rules using this alias.

-
-
-
- - Cancel - - - Delete - Deleting... - -
-
-
-@endif - - {{-- IPSet Form Modal --}} - @if($showIPSetForm) -
-
- -

Create IP Set

- -
-
- - - @error('ipsetName') {{ $message }} @enderror -
- -
- - -
- -
-

After creating the IPSet, you can add IP addresses and CIDR ranges to it.

-
-
- -
- - -
-
-
- @endif - - {{-- Delete IPSet Modal --}} - @if($showDeleteIPSetModal && $deletingIPSetName) -
-
- -
-
- -
-
-

Delete IP Set

-

Delete IPSet "{{ $deletingIPSetName }}"?

-

All members will be removed and rules using this IPSet may be affected.

-
-
-
- - Cancel - - - Delete - Deleting... - -
-
-
-@endif - - {{-- IPSet Member Form Modal --}} - @if($showIPSetMemberForm && $selectedIPSetName) -
-
- -

Add IP/CIDR to {{ $selectedIPSetName }}

- -
-
- - - @error('memberCidr') {{ $message }} @enderror -
- -
- - -
- -
- - -
-
- -
- - -
-
-
- @endif - - {{-- Delete IPSet Member Modal --}} - @if($showDeleteIPSetMemberModal && $deletingMemberCidr) -
-
- -
-
- -
-
-

Remove Member

-

Remove "{{ $deletingMemberCidr }}" from {{ $selectedIPSetName }}?

-
-
-
- - Cancel - - - Remove - Removing... - -
-
-
-@endif -
- -{{-- Save Configuration Modal --}} -@if($showSaveConfigModal) -
-
- -
-
- -
-
-

Save Current Configuration

-

Save your current firewall rules and policies as a reusable configuration

-
-
- -
-
- - - @error('newConfigName') -

{{ $message }}

- @enderror -
- -
-

Configuration Summary

-
-
- Rules - {{ count($rules) }} -
-
- Incoming Policy - {{ $policyIn }} -
-
- Outgoing Policy - {{ $policyOut }} -
-
-
-
- -
- - -
-
-
-@endif - -{{-- View Configuration Modal --}} -@if($showViewConfigModal && $viewingConfiguration) -
-
-
- -
-
- -
-
-

{{ $viewingConfiguration->configuration_name }}

-

Created on {{ $viewingConfiguration->created_at->format('M d, Y H:i') }}

-
-
-
- -
- {{-- Policies --}} -
-

Traffic Policies

-
-
-
- Incoming Traffic - - {{ $viewingConfiguration->incoming_policy }} - -
-
-
-
- Outgoing Traffic - - {{ $viewingConfiguration->outgoing_policy }} - -
-
-
-
- - {{-- Compatibility Warnings --}} - @if(count($configCompatibilityIssues) > 0) -
-
- -
-

Compatibility Issues

-

The following rules may not be compatible with this VPS:

-
    - @foreach($configCompatibilityIssues as $issue) -
  • • Rule #{{ $issue['rule_index'] + 1 }}: {{ implode(', ', $issue['issues']) }}
  • - @endforeach -
-
-
-
- @endif - - {{-- Rules --}} -
-

Firewall Rules ({{ count($viewingConfiguration->firewall_rules) }})

- @if(count($viewingConfiguration->firewall_rules) > 0) -
- - - - - - - - - - - - - - - @foreach($viewingConfiguration->firewall_rules as $index => $rule) - @php - $hasIssue = collect($configCompatibilityIssues)->contains('rule_index', $index); - @endphp - - - - - - - - - - - @endforeach - -
#ActionDirectionSourceDestinationProtocolPortStatus
{{ $index + 1 }} - - {{ $rule['action'] ?? 'ACCEPT' }} - - {{ strtoupper($rule['type'] ?? '-') }}{{ $rule['source'] ?? 'any' }}{{ $rule['dest'] ?? 'any' }}{{ $rule['proto'] ?? '-' }}{{ $rule['dport'] ?? '-' }} - @if($hasIssue) - Incompatible - @elseif(isset($rule['enable']) && $rule['enable'] == 0) - Disabled - @else - Enabled - @endif -
-
- @else -

No rules in this configuration

- @endif -
-
- -
-
- - -
-
-
-
-@endif - -{{-- Apply Configuration Modal --}} -@if($showApplyConfigModal && $applyingConfiguration) -
-
- -
-
- -
-
-

Apply Configuration

-

Apply "{{ $applyingConfiguration->configuration_name }}" to this VPS

-
-
- -
-
- -
-

Warning

-

This will replace all existing firewall rules with the rules from this configuration. This action cannot be undone.

-
-
-
- - @if(count($applyCompatibilityWarnings) > 0) -
-
- -
-

Compatibility Notice

-

{{ count($applyCompatibilityWarnings) }} rule(s) in this configuration are not fully compatible with this VPS and will be disabled.

-
-
-
- @endif - -
-

Configuration Details

-
-
-

Rules

-

{{ count($applyingConfiguration->firewall_rules) }}

-
-
-

IN Policy

-

{{ $applyingConfiguration->incoming_policy }}

-
-
-

OUT Policy

-

{{ $applyingConfiguration->outgoing_policy }}

-
-
-
- -
- - -
-
-
-@endif - -{{-- Delete Configuration Modal --}} -@if($showDeleteConfigModal && $deletingConfigId) -
-
- -
-
- -
-
-

Delete Configuration

-

Are you sure you want to delete this saved configuration? This action cannot be undone.

-
-
-
- - Cancel - - - Delete - Deleting... - -
-
-
-@endif - -{{-- Apply to All Servers Modal --}} -@if($showApplyToAllModal) -
-
- -
-
- -
-
-

Apply to All Servers

-

Apply the current firewall configuration to all your other VPS instances.

-
-
- - @php $otherServers = $this->getUserOtherServers(); @endphp - - @if(count($applyToAllResults) > 0) - {{-- Show Results --}} -
-

Results

-
- @foreach($applyToAllResults as $result) -
-
- @if($result['success']) - - @else - - @endif - {{ $result['hostname'] }} -
- {{ $result['message'] }} -
- @endforeach -
-
- -
- -
- @else - {{-- Show Confirmation --}} -
-
- -
-

Warning

-

This will replace the firewall configuration on all your other servers. Existing rules will be deleted and replaced with the current configuration.

-
-
-
- - @if(count($otherServers) > 0) -
-

Servers to Update ({{ count($otherServers) }})

-
- @foreach($otherServers as $otherServer) -
-
-
- -
-
- {{ $otherServer['hostname'] ?? 'VPS ' . $otherServer['vm_id'] }} - VM {{ $otherServer['vm_id'] }} -
-
-
- @endforeach -
-
- -
-

Configuration to Apply

-
-
- Rules: - {{ count($rules) }} -
-
- Policy In: - {{ $policyIn }} -
-
- Policy Out: - {{ $policyOut }} -
-
-
- @else -
- -

No other servers found to apply the configuration to.

-
- @endif - -
- - @if(count($otherServers) > 0) - - @endif -
- @endif -
-
-@endif - -{{-- Rename Configuration Modal --}} -@if($showRenameConfigModal && $renamingConfigId) -
-
- -
-
- -
-
-

Rename Configuration

-

Enter a new name for this configuration

-
-
- -
- - - @error('renameConfigName') -

{{ $message }}

- @enderror -
- -
- - -
-
-
-@endif - -@script - -@endscript -
\ No newline at end of file diff --git a/extensions/Servers/Proxmox/views/help.blade.php b/extensions/Servers/Proxmox/views/help.blade.php index e91b5e31d..5d31ede72 100644 --- a/extensions/Servers/Proxmox/views/help.blade.php +++ b/extensions/Servers/Proxmox/views/help.blade.php @@ -1,4 +1,4 @@ - +