890 lines
31 KiB
PHP
890 lines
31 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
|
|
|
|
use Illuminate\Support\Collection;
|
|
use Livewire\Attributes\Locked;
|
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
|
|
|
class System extends Component
|
|
{
|
|
public string $machineType = '';
|
|
|
|
public string $biosType = 'seabios';
|
|
|
|
public string $cpuType = 'kvm64';
|
|
|
|
public array $selectedCpuFlags = [];
|
|
|
|
public bool $kvmEnabled = true;
|
|
|
|
public bool $acpiEnabled = true;
|
|
|
|
public string $smbiosSettings = '';
|
|
|
|
public array $availableMachines = [];
|
|
|
|
public array $availableCpuTypes = [];
|
|
|
|
public array $suggestedMachines = [];
|
|
|
|
protected array $machineAliases = [
|
|
'pc-q35' => '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;
|
|
}
|
|
}
|
|
|
|
// Reject unknown/unrecognised values to prevent arbitrary strings reaching the API
|
|
return '';
|
|
}
|
|
|
|
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
|
|
{
|
|
// Validate biosType against allowed options
|
|
$allowedBios = array_column($this->availableBiosOptions, 'value');
|
|
if ($this->biosType !== '' && !in_array($this->biosType, $allowedBios, true)) {
|
|
$this->notify('Invalid BIOS type selected.', 'error');
|
|
return;
|
|
}
|
|
|
|
// Validate cpuType against allowed options
|
|
if ($this->cpuType !== '') {
|
|
$allowedCpuTypes = array_column($this->availableCpuTypes, 'value');
|
|
if (!empty($allowedCpuTypes) && !in_array($this->cpuType, $allowedCpuTypes, true)) {
|
|
$this->notify('Invalid CPU type selected.', 'error');
|
|
return;
|
|
}
|
|
}
|
|
|
|
$flags = $this->formatFlagsForSave($this->selectedCpuFlags);
|
|
$resolvedCpu = $this->cpuType !== '' ? $this->cpuType : null;
|
|
|
|
if ($resolvedCpu && $flags) {
|
|
$resolvedCpu .= ',flags=' . $flags;
|
|
}
|
|
|
|
$resolvedMachine = $this->machineType !== '' ? $this->resolveMachineValue($this->machineType) : null;
|
|
|
|
$payload = array_filter([
|
|
'machine' => $resolvedMachine !== '' ? $resolvedMachine : 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(fn ($flag) => $flag !== '' && !str_starts_with($flag, '-'))
|
|
->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;
|
|
}
|
|
|
|
// Only allow removal of EFI disk and TPM devices via this method
|
|
$isAllowed = (str_starts_with($deviceKey, 'efidisk') && isset($this->efiDisks[$deviceKey]))
|
|
|| (str_starts_with($deviceKey, 'tpmstate') && isset($this->tpmDevices[$deviceKey]));
|
|
|
|
if (!$isAllowed) {
|
|
$this->notify('Only EFI disk and TPM devices can be removed here.', 'error');
|
|
return;
|
|
}
|
|
|
|
// Check VM status - must be stopped to remove EFI/TPM devices
|
|
if (!$this->status || $this->status->status !== 'stopped') {
|
|
$this->notify('VM must be stopped to remove system devices.', '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;
|
|
}
|
|
|
|
// Check VM status - must be stopped to add EFI disk
|
|
if (!$this->status || $this->status->status !== 'stopped') {
|
|
$this->notify('VM must be stopped to add an EFI disk.', '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;
|
|
}
|
|
|
|
// Check VM status - must be stopped to add TPM device
|
|
if (!$this->status || $this->status->status !== 'stopped') {
|
|
$this->notify('VM must be stopped to add a TPM device.', 'error');
|
|
return;
|
|
}
|
|
|
|
$version = trim($this->newTpmVersion);
|
|
$slot = 'tpmstate0';
|
|
|
|
if ($version === '') {
|
|
$this->notify('Select a TPM version.', 'error');
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate TPM version against allowed values
|
|
$allowedVersions = ['v1.2', 'v2.0'];
|
|
if (!in_array($version, $allowedVersions, true)) {
|
|
$this->notify('Invalid TPM version. Must be v1.2 or v2.0.', '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,
|
|
]);
|
|
}
|
|
}
|
|
|