'pc-q35-8.1', 'pc-i440fx' => 'pc-i440fx-8.1', ]; public array $availableBiosOptions = [ ['value' => 'seabios', 'label' => 'SeaBIOS (Legacy)'], ['value' => 'ovmf', 'label' => 'OVMF (UEFI)'], ]; public array $availableCpuFlags = []; public array $flagOptions = [ 'aes' => 'AES', 'hv-evmcs' => 'Hyper-V EVMCS', 'hvlibflush' => 'Hyper-V LIBFLUSH', 'pdpe1gb' => '1GB Pages', 'amd-no-ssb' => 'AMD No SSB', 'amd-ssbd' => 'AMD SSBD', 'virt-ssbd' => 'Virt SSBD', 'ibpd' => 'IBPD', 'ssbd' => 'SSBD', 'spec-ctrl' => 'Speculative Control', 'pcid' => 'PCID', 'md-clear' => 'MD Clear', 'nested-virt' => 'Nested Virtualization', ]; public array $efiDisks = []; public array $tpmDevices = []; public bool $newEfiPreenroll = false; public string $newTpmVersion = 'v2.0'; #[Locked] public array $original = []; public function mount(Server $server): void { parent::mount($server); $this->loadSystemConfiguration(false); $this->loadCapabilities(); } public function loadSystemConfiguration(bool $notify = true): void { try { $proxmox = new Proxmox; $config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; $this->machineType = (string) ($config['machine'] ?? ''); $this->biosType = (string) ($config['bios'] ?? $this->biosType); [$detectedCpuType, $detectedFlags] = $this->extractCpuDetails( (string) ($config['cpu'] ?? ''), (string) ($config['flags'] ?? '') ); $this->cpuType = $detectedCpuType !== '' ? $detectedCpuType : $this->cpuType; $this->selectedCpuFlags = $this->filterAllowedFlags($detectedFlags); $this->kvmEnabled = !array_key_exists('kvm', $config) || (bool) $config['kvm']; $this->acpiEnabled = !array_key_exists('acpi', $config) || (bool) $config['acpi']; $this->smbiosSettings = (string) ($config['smbios1'] ?? ''); $this->parseDevices($config); // Store original values for change tracking $this->storeOriginalValues(); if ($notify) { $this->notify('System configuration refreshed.', 'success'); } } catch (\Exception $e) { if ($notify) { $this->notify('Failed to load system configuration: ' . $e->getMessage(), 'error'); } } } protected function parseDevices(array $config): void { $efi = []; $tpm = []; foreach ($config as $key => $value) { if (!is_string($key)) { continue; } if (str_starts_with($key, 'efidisk')) { $efi[$key] = $value; } elseif (str_starts_with($key, 'tpmstate')) { $tpm[$key] = $value; } } $this->efiDisks = $efi; $this->tpmDevices = $tpm; } protected function loadCapabilities(): void { $this->availableMachines = $this->fetchMachineOptions(); $this->suggestedMachines = $this->buildSuggestedMachines($this->availableMachines); $this->availableCpuTypes = $this->fetchCpuOptions(); $this->availableCpuFlags = $this->fetchCpuFlags(); $this->selectedCpuFlags = $this->filterAllowedFlags($this->selectedCpuFlags); } protected function fetchMachineOptions(): array { try { $proxmox = new Proxmox; $response = $proxmox->request('/nodes/' . $this->server->node->name . '/capabilities/qemu/machines')->json()['data'] ?? []; return collect($response)->map(function ($machine) { return [ 'value' => $machine['name'] ?? '', 'label' => trim(($machine['name'] ?? '') . ' ' . ($machine['desc'] ?? '')), ]; })->filter(fn ($item) => $item['value'] !== '')->values()->toArray(); } catch (\Exception $e) { return [ ['value' => 'pc-q35-8.1', 'label' => 'pc-q35-8.1 (Q35)'], ['value' => 'pc-i440fx-8.1', 'label' => 'pc-i440fx-8.1 (i440FX)'], ]; } } protected function buildSuggestedMachines(array $machines): array { $targets = collect([ 'pc-q35' => 'q35 (PCIe / Modern)', 'pc-i440fx' => 'i440fx (Legacy PCI)', ])->map(function ($label, $base) { return [ 'label' => $label, 'match' => null, 'fallback' => $this->machineAliases[$base] ?? $base, ]; }); foreach ($machines as $machine) { $value = $machine['value'] ?? ''; if ($value === '') { continue; } foreach ($targets->keys() as $key) { if (str_contains($value, $key) && is_null($targets[$key]['match'])) { $targets[$key]['match'] = [ 'value' => $value, 'label' => $targets[$key]['label'] . ' • ' . $value, ]; } } } return $targets ->map(function ($entry) { if ($entry['match']) { return $entry['match']; } return [ 'value' => $entry['fallback'], 'label' => $entry['label'], ]; }) ->values() ->toArray(); } protected function resolveMachineValue(string $value): string { if ($value === '') { return $value; } $exactMatch = collect($this->availableMachines) ->pluck('value') ->contains($value); if ($exactMatch) { return $value; } foreach ($this->machineAliases as $alias => $resolved) { if ($value === $alias || str_starts_with($value, $alias . '-')) { return $resolved; } } return $value; } protected function fetchCpuOptions(): array { $allowed = [ 'x86-64-v2', 'x86-64-v2-aes', 'x86-64-v3', 'kvm64', 'qemu64', ]; try { $proxmox = new Proxmox; $response = $proxmox->request('/nodes/' . $this->server->node->name . '/capabilities/qemu/cpu')->json()['data'] ?? []; return collect($response) ->map(function ($cpu) { return [ 'value' => $cpu['name'] ?? '', 'label' => trim(($cpu['name'] ?? '') . ' ' . ($cpu['vendor'] ?? '')), ]; }) ->filter(fn ($item) => $item['value'] !== '' && in_array($item['value'], $allowed, true)) ->whenEmpty(function () use ($allowed) { return collect($allowed)->map(fn ($value) => [ 'value' => $value, 'label' => strtoupper($value), ]); }) ->values() ->toArray(); } catch (\Exception $e) { return collect($allowed)->map(fn ($value) => [ 'value' => $value, 'label' => strtoupper($value), ])->toArray(); } } protected function fetchCpuFlags(): array { try { $proxmox = new Proxmox; $response = $proxmox->request('/nodes/' . $this->server->node->name . '/capabilities/qemu/cpu')->json()['data'] ?? []; // Try to get flags from the 'host' CPU entry first, as it contains the actual node flags $hostCpu = collect($response)->firstWhere('name', 'host'); if ($hostCpu && isset($hostCpu['flags']) && is_array($hostCpu['flags'])) { return collect($hostCpu['flags']) ->unique() ->sort() ->values() ->toArray(); } // Fallback: collect flags from all CPU entries return collect($response) ->pluck('flags') ->filter(function ($flags) { return is_array($flags) && !empty($flags); }) ->flatten() ->unique() ->sort() ->values() ->toArray(); } catch (\Exception $e) { return [ 'aes', 'avx', 'avx2', 'pcid', 'ssse3', 'ibpb', 'amd-ssbd', 'amd-no-ssb', 'md-clear', 'spec-ctrl', ]; } } public function saveSystemSettings(): void { $flags = $this->formatFlagsForSave($this->selectedCpuFlags); $resolvedCpu = $this->cpuType !== '' ? $this->cpuType : null; if ($resolvedCpu && $flags) { $resolvedCpu .= ',flags=' . $flags; } $payload = array_filter([ 'machine' => $this->machineType !== '' ? $this->resolveMachineValue($this->machineType) : null, 'bios' => $this->biosType !== '' ? $this->biosType : null, 'cpu' => $resolvedCpu, 'kvm' => $this->kvmEnabled ? 1 : 0, 'acpi' => $this->acpiEnabled ? 1 : 0, 'smbios1' => $this->smbiosSettings !== '' ? $this->smbiosSettings : null, ], function ($value) { return !is_null($value); }); try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', $payload ); $this->notify('System configuration updated. Reboot may be required for some changes.', 'success'); $this->loadSystemConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to update system configuration: ' . $e->getMessage(), 'error'); } } protected function formatFlagsForSave(array $flags): ?string { $normalized = $this->filterAllowedFlags($flags); if (empty($normalized)) { return null; } return collect($normalized) ->map(fn ($flag) => '+' . $flag) ->implode(';'); } protected function extractCpuDetails(string $rawCpu, string $separateFlags): array { $cpuType = trim($rawCpu); $flagsString = trim($separateFlags); if ($cpuType !== '' && str_contains($cpuType, ',')) { $parts = array_map('trim', explode(',', $cpuType)); $cpuType = array_shift($parts) ?: ''; foreach ($parts as $part) { if (str_starts_with($part, 'flags=')) { $flagsString = substr($part, strlen('flags=')); } } } $flags = $this->normalizeFlagList($flagsString); return [$cpuType, $flags]; } protected function normalizeFlagList(?string $flagsString): array { $clean = trim((string) $flagsString); $clean = trim($clean, "\"'"); $clean = str_replace(["\r", "\n", ' '], '', $clean); if ($clean === '') { return []; } return collect(explode(';', $clean)) ->map(fn ($flag) => ltrim($flag, '+-')) ->filter() ->unique() ->values() ->toArray(); } protected function filterAllowedFlags($flags): array { return collect($flags ?? []) ->flatten() ->map(fn ($flag) => trim((string) $flag)) ->filter(fn ($flag) => $flag !== '' && array_key_exists($flag, $this->flagOptions)) ->unique() ->values() ->toArray(); } public function updatedSelectedCpuFlags(): void { $this->selectedCpuFlags = $this->filterAllowedFlags($this->selectedCpuFlags); } public function removeDevice(string $deviceKey): void { if ($deviceKey === '') { $this->notify('Invalid device reference.', 'error'); return; } // Get the device definition before removing it $deviceDefinition = null; if (str_starts_with($deviceKey, 'efidisk') && isset($this->efiDisks[$deviceKey])) { $deviceDefinition = $this->efiDisks[$deviceKey]; } elseif (str_starts_with($deviceKey, 'tpmstate') && isset($this->tpmDevices[$deviceKey])) { $deviceDefinition = $this->tpmDevices[$deviceKey]; } try { $proxmox = new Proxmox; // Extract storage volume from definition before removing device $storageVolume = null; if ($deviceDefinition !== null) { $parts = explode(',', $deviceDefinition, 2); if (!empty($parts[0]) && str_contains($parts[0], ':')) { $storageVolume = trim($parts[0]); } } // Remove device from VM config FIRST // This will log "update VM {vmid}: -delete {deviceKey}" in Proxmox // Note: This may create an "unused" entry if the volume still exists $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', ['delete' => $deviceKey] ); // Immediately clean up any unused entries created by the removal // This prevents orphaned "unused0" entries if ($storageVolume !== null) { $this->cleanupUnusedEntries($proxmox, $storageVolume); } // Then delete the storage volume (after device is removed from config) if ($storageVolume !== null) { $this->deleteStorageVolume($proxmox, $deviceDefinition); } $this->notify(strtoupper($deviceKey) . ' removed successfully.', 'success'); $this->loadSystemConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to remove device: ' . $e->getMessage(), 'error'); } } protected function deleteStorageVolume(Proxmox $proxmox, string $deviceDefinition): void { // Parse storage:volume from definition (format: storage:volume,options...) $parts = explode(',', $deviceDefinition, 2); if (empty($parts[0])) { return; } $storageVolume = trim($parts[0]); // Check if it's a storage volume (format: storage:volume) if (!str_contains($storageVolume, ':')) { return; } [$storage, $volume] = explode(':', $storageVolume, 2); // Only delete from cephpool (RBD storage) for now if ($storage !== 'cephpool') { return; } try { // Delete the volume from storage using the full volid format $proxmox->request( '/nodes/' . $this->server->node->name . '/storage/' . $storage . '/content/' . $storageVolume, 'delete' ); } catch (\Exception $e) { // Volume might already be deleted or deletion might not be needed // Continue silently as the device has already been removed from VM config } } protected function cleanupUnusedEntries(Proxmox $proxmox, string $storageVolume): void { try { // Get current VM config to check for unused entries $config = $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config' )->json()['data'] ?? []; // Extract just the volume name part (e.g., "vm-202500014-efidisk0" from "cephpool:vm-202500014-efidisk0") $volumeName = $storageVolume; if (str_contains($storageVolume, ':')) { [, $volumeName] = explode(':', $storageVolume, 2); } // Find all unused entries that reference the deleted volume foreach ($config as $key => $value) { if (preg_match('/^unused\d+$/', $key) && is_string($value)) { // Check if this unused entry references the deleted volume // Match either the full storage volume or just the volume name if (str_contains($value, $storageVolume) || str_contains($value, $volumeName)) { // Delete the unused entry try { $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', ['delete' => $key] ); } catch (\Exception $e) { // Unused entry might already be cleaned up // Continue to check other entries } } } } } catch (\Exception $e) { // Config fetch or cleanup might fail, but device is already removed // Continue silently } } public function addEfiDisk(): void { if (count($this->efiDisks) > 0) { $this->notify('Only one EFI disk is allowed. Remove the existing EFI disk first.', 'error'); return; } $slot = 'efidisk0'; $volumeName = 'vm-' . $this->server->vm_id . '-efidisk0'; try { $proxmox = new Proxmox; // For RBD storage, create the volume first try { $proxmox->request( '/nodes/' . $this->server->node->name . '/storage/cephpool/content', 'post', [ 'filename' => $volumeName, 'size' => '4M', 'vmid' => $this->server->vm_id, ] ); } catch (\Exception $e) { // Volume might already exist or creation might not be needed // Continue to add device to config } $definition = 'cephpool:' . $volumeName . ',size=4M'; if ($this->newEfiPreenroll) { $definition .= ',pre-enrolled-keys=1'; } $this->applyDeviceChange($slot, $definition, 'EFI disk added.'); $this->newEfiPreenroll = false; } catch (\Exception $e) { $this->notify('Failed to add EFI disk: ' . $e->getMessage(), 'error'); } } public function addTpmDevice(): void { if (count($this->tpmDevices) > 0) { $this->notify('Only one TPM device is allowed. Remove the existing TPM device first.', 'error'); return; } $version = trim($this->newTpmVersion); $slot = 'tpmstate0'; if ($version === '') { $this->notify('Select a TPM version.', 'error'); return; } $volumeName = 'vm-' . $this->server->vm_id . '-tpm'; try { $proxmox = new Proxmox; // For RBD storage, create the volume first try { $proxmox->request( '/nodes/' . $this->server->node->name . '/storage/cephpool/content', 'post', [ 'filename' => $volumeName, 'size' => '4M', 'vmid' => $this->server->vm_id, ] ); } catch (\Exception $e) { // Volume might already exist or creation might not be needed // Continue to add device to config } $definition = 'cephpool:' . $volumeName . ',size=4M,version=' . $version; $this->applyDeviceChange($slot, $definition, 'TPM device added.'); $this->newTpmVersion = 'v2.0'; } catch (\Exception $e) { $this->notify('Failed to add TPM device: ' . $e->getMessage(), 'error'); } } protected function applyDeviceChange(string $slot, string $definition, string $successMessage): void { try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [$slot => $definition] ); $this->notify($successMessage, 'success'); $this->loadSystemConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to update device: ' . $e->getMessage(), 'error'); } } protected function storeOriginalValues(): void { $flags = array_values(array_unique($this->selectedCpuFlags ?? [])); sort($flags); $this->original = [ 'machineType' => $this->machineType, 'biosType' => $this->biosType, 'cpuType' => $this->cpuType, 'selectedCpuFlags' => $flags, 'kvmEnabled' => $this->kvmEnabled, 'acpiEnabled' => $this->acpiEnabled, 'smbiosSettings' => $this->smbiosSettings, 'efiDisks' => json_decode(json_encode($this->efiDisks), true), 'tpmDevices' => json_decode(json_encode($this->tpmDevices), true), ]; } protected function hydrateFromOriginal(): void { $this->machineType = $this->original['machineType'] ?? ''; $this->biosType = $this->original['biosType'] ?? 'seabios'; $this->cpuType = $this->original['cpuType'] ?? 'kvm64'; $this->selectedCpuFlags = $this->original['selectedCpuFlags'] ?? []; $this->kvmEnabled = $this->original['kvmEnabled'] ?? true; $this->acpiEnabled = $this->original['acpiEnabled'] ?? true; $this->smbiosSettings = $this->original['smbiosSettings'] ?? ''; } protected function computeHasChanges(): bool { if (empty($this->original)) { return false; } $currentFlags = array_values(array_unique($this->selectedCpuFlags ?? [])); $originalFlags = $this->original['selectedCpuFlags'] ?? []; // Normalize arrays for comparison (sort and re-index) sort($currentFlags); sort($originalFlags); $currentFlags = array_values($currentFlags); $originalFlags = array_values($originalFlags); $machineTypeChanged = $this->machineType !== ($this->original['machineType'] ?? ''); $biosTypeChanged = $this->biosType !== ($this->original['biosType'] ?? 'seabios'); $cpuTypeChanged = $this->cpuType !== ($this->original['cpuType'] ?? 'kvm64'); $flagsChanged = json_encode($currentFlags) !== json_encode($originalFlags); $kvmChanged = (bool) $this->kvmEnabled !== (bool) ($this->original['kvmEnabled'] ?? true); $acpiChanged = (bool) $this->acpiEnabled !== (bool) ($this->original['acpiEnabled'] ?? true); $smbiosChanged = $this->smbiosSettings !== ($this->original['smbiosSettings'] ?? ''); return $machineTypeChanged || $biosTypeChanged || $cpuTypeChanged || $flagsChanged || $kvmChanged || $acpiChanged || $smbiosChanged; } public function getHasChangesProperty(): bool { return $this->computeHasChanges(); } /** * Check if Firmware & Machine settings are synced with the original values. */ public function getFirmwareMachineSyncedProperty(): bool { // If original hasn't been loaded yet, consider synced if (!isset($this->original['machineType'])) { return true; } $machineTypeMatch = $this->machineType === ($this->original['machineType'] ?? ''); $biosTypeMatch = $this->biosType === ($this->original['biosType'] ?? 'seabios'); $smbiosMatch = $this->smbiosSettings === ($this->original['smbiosSettings'] ?? ''); $acpiMatch = (bool) $this->acpiEnabled === (bool) ($this->original['acpiEnabled'] ?? true); $kvmMatch = (bool) $this->kvmEnabled === (bool) ($this->original['kvmEnabled'] ?? true); return $machineTypeMatch && $biosTypeMatch && $smbiosMatch && $acpiMatch && $kvmMatch; } /** * Check if CPU & Virtualization settings are synced with the original values. */ public function getCpuVirtualizationSyncedProperty(): bool { // If original hasn't been loaded yet, consider synced if (!isset($this->original['cpuType'])) { return true; } $cpuTypeMatch = $this->cpuType === ($this->original['cpuType'] ?? 'kvm64'); $currentFlags = array_values(array_unique($this->selectedCpuFlags ?? [])); $originalFlags = $this->original['selectedCpuFlags'] ?? []; // Normalize arrays for comparison (sort and re-index) sort($currentFlags); sort($originalFlags); $currentFlags = array_values($currentFlags); $originalFlags = array_values($originalFlags); $flagsMatch = json_encode($currentFlags) === json_encode($originalFlags); return $cpuTypeMatch && $flagsMatch; } /** * Check if EFI Disks settings are synced with the original values. */ public function getEfiDisksSyncedProperty(): bool { // If original hasn't been loaded yet, consider synced if (!isset($this->original['efiDisks'])) { return true; } $originalEfiDisks = isset($this->original['efiDisks']) && is_array($this->original['efiDisks']) ? $this->original['efiDisks'] : []; $currentEfiDisks = is_array($this->efiDisks) ? $this->efiDisks : []; // Normalize arrays: sort by keys and compare ksort($originalEfiDisks); ksort($currentEfiDisks); if (count($originalEfiDisks) !== count($currentEfiDisks)) { return false; } foreach ($originalEfiDisks as $key => $value) { if (!isset($currentEfiDisks[$key]) || $currentEfiDisks[$key] !== $value) { return false; } } return true; } /** * Check if TPM Devices settings are synced with the original values. */ public function getTpmDevicesSyncedProperty(): bool { // If original hasn't been loaded yet, consider synced if (!isset($this->original['tpmDevices'])) { return true; } $originalTpmDevices = isset($this->original['tpmDevices']) && is_array($this->original['tpmDevices']) ? $this->original['tpmDevices'] : []; $currentTpmDevices = is_array($this->tpmDevices) ? $this->tpmDevices : []; // Normalize arrays: sort by keys and compare ksort($originalTpmDevices); ksort($currentTpmDevices); if (count($originalTpmDevices) !== count($currentTpmDevices)) { return false; } foreach ($originalTpmDevices as $key => $value) { if (!isset($currentTpmDevices[$key]) || $currentTpmDevices[$key] !== $value) { return false; } } return true; } public function resetSystemSettings(): void { $this->hydrateFromOriginal(); $this->notify('Changes reverted to the values currently stored on the server.', 'info'); } public function render() { return view('proxmox::system')->layoutData([ 'sidebar' => true, ]); } }