'lsi', 'label' => 'LSI 53C895A'], ['value' => 'lsi53c810', 'label' => 'LSI 53C810'], ['value' => 'virtio-scsi-pci', 'label' => 'VirtIO SCSI'], ['value' => 'virtio-scsi-single', 'label' => 'VirtIO SCSI Single'], ['value' => 'megasas', 'label' => 'MegaRAID SAS 8708EM2'], ['value' => 'pvscsi', 'label' => 'VMware PVSCSI'], ]; /** * Available bus/device types with their configurations. */ public array $busTypes = [ 'ide' => ['label' => 'IDE', 'max' => 4, 'ssd_support' => true, 'iothread_support' => false], 'sata' => ['label' => 'SATA', 'max' => 6, 'ssd_support' => true, 'iothread_support' => false], 'scsi' => ['label' => 'SCSI', 'max' => 31, 'ssd_support' => true, 'iothread_support' => true], 'virtio' => ['label' => 'VirtIO Block', 'max' => 16, 'ssd_support' => false, 'iothread_support' => true], ]; /** * Available cache modes. */ public array $cacheOptions = [ ['value' => 'none', 'label' => 'No Cache (Default)'], ['value' => 'directsync', 'label' => 'Direct Sync'], ['value' => 'writethrough', 'label' => 'Write Through'], ['value' => 'writeback', 'label' => 'Write Back'], ['value' => 'unsafe', 'label' => 'Write Back (Unsafe)'], ]; /** * Available Async IO types. */ public array $aioOptions = [ ['value' => 'io_uring', 'label' => 'io_uring (Default)'], ['value' => 'native', 'label' => 'Native'], ['value' => 'threads', 'label' => 'Threads'], ]; /** * Form state for adding a new drive. */ public bool $showAddDriveModal = false; public string $newDriveBus = 'scsi'; public int $newDriveSize = 10; public string $newDriveCache = 'none'; public string $newDriveAio = 'io_uring'; public bool $newDriveSsd = false; public bool $newDriveIothread = true; public bool $newDriveDiscard = true; /** * Form state for editing drive settings. */ public bool $showEditDriveModal = false; public ?string $editingDriveKey = null; public string $editDriveCache = 'none'; public string $editDriveAio = 'io_uring'; public bool $editDriveSsd = false; public bool $editDriveIothread = false; public bool $editDriveDiscard = true; /** * Form state for moving/changing bus type. */ public bool $showMoveDriveModal = false; public ?string $movingDriveKey = null; public string $moveDriveTargetBus = 'scsi'; /** * Form state for detach confirmation modal. */ public bool $showDetachModal = false; public ?string $detachingDriveKey = null; /** * Form state for delete confirmation modal. */ public bool $showDeleteModal = false; public ?string $deletingDriveKey = null; /** * Form state for reattach modal. */ public bool $showReattachModal = false; public ?string $reattachingDriveKey = null; public string $reattachTargetBus = 'scsi'; /** * Storage name for cephpool. */ protected string $storagePool = 'cephpool'; public function mount(Server $server): void { parent::mount($server); $this->loadMaxAllowedStorage(); $this->loadStorageConfiguration(false); } /** * Load the maximum allowed storage from the plan configuration. * Uses ExtensionHelper to merge product settings with service properties, * ensuring upgraded disk sizes are properly reflected. */ protected function loadMaxAllowedStorage(): void { $settings = array_merge( ExtensionHelper::settingsToArray($this->server->service->product->settings), ExtensionHelper::getServiceProperties($this->server->service) ); $this->maxAllowedStorage = isset($settings['disk']) ? (float) $settings['disk'] : 0; } /** * Load the current storage configuration from Proxmox. */ public function loadStorageConfiguration(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->parseStorageConfiguration($config); $this->scsiControllerType = $config['scsihw'] ?? 'virtio-scsi-pci'; $this->originalScsiControllerType = $this->scsiControllerType; if ($notify) { $this->notify('Storage configuration refreshed.', 'success'); } } catch (\Exception $e) { if ($notify) { $this->notify('Failed to load storage configuration: ' . $e->getMessage(), 'error'); } } } /** * Parse the VM configuration to extract disk information. */ protected function parseStorageConfiguration(array $config): void { $drives = []; $totalSize = 0; foreach ($config as $key => $value) { // Skip non-disk entries if (!preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $key, $matches)) { continue; } // Skip cloud-init, EFI, and TPM disks if ($this->isExcludedDisk($value)) { continue; } // Skip CD-ROM drives if (str_contains($value, 'media=cdrom')) { continue; } $busType = $matches[1]; $deviceIndex = (int) $matches[2]; $diskInfo = $this->parseDiskString($value, $busType, $deviceIndex); if ($diskInfo) { $drives[$key] = $diskInfo; $totalSize += $diskInfo['size']; } } // 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]; $busCompare = ($busOrder[$matchA[1]] ?? 4) <=> ($busOrder[$matchB[1]] ?? 4); if ($busCompare !== 0) { return $busCompare; } return (int) $matchA[2] <=> (int) $matchB[2]; }); $this->drives = $drives; // Parse unused/detached drives $unusedDrives = []; $unusedTotalSize = 0; foreach ($config as $key => $value) { if (!preg_match('/^unused(\d+)$/', $key, $matches)) { continue; } // Skip excluded disks if ($this->isExcludedDisk($value)) { continue; } $unusedIndex = (int) $matches[1]; $unusedInfo = $this->parseUnusedDiskString($value, $unusedIndex); if ($unusedInfo) { $unusedDrives[$key] = $unusedInfo; $unusedTotalSize += $unusedInfo['size'] ?? 0; } } // Sort unused drives by index uksort($unusedDrives, function ($a, $b) { preg_match('/^unused(\d+)$/', $a, $matchA); preg_match('/^unused(\d+)$/', $b, $matchB); return (int) $matchA[1] <=> (int) $matchB[1]; }); $this->unusedDrives = $unusedDrives; // Total storage includes both attached and detached drives $this->totalAllocatedStorage = $totalSize + $unusedTotalSize; } /** * Parse an unused disk configuration string. */ protected function parseUnusedDiskString(string $value, int $unusedIndex): ?array { // Example: cephpool:vm-100-disk-1 // Unused disks typically just have the volume reference $volume = trim($value); // Extract storage name from volume (format: storage:volume-name) $storageName = 'cephpool'; if (str_contains($volume, ':')) { [$storageName, ] = explode(':', $volume, 2); } // Try to get the volume size from the storage API $sizeGb = 0; $sizeFormatted = 'Unknown'; try { $proxmox = new Proxmox($this->server->node->server); $storageContent = $proxmox->request( '/nodes/' . $this->server->node->name . '/storage/' . $storageName . '/content', data: ['vmid' => $this->server->vmid] ); if (isset($storageContent['data']) && is_array($storageContent['data'])) { foreach ($storageContent['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; } } } } catch (\Exception $e) { // If we can't fetch size, leave as unknown } return [ 'key' => 'unused' . $unusedIndex, 'index' => $unusedIndex, 'volume' => $volume, 'storage' => $storageName, 'size' => $sizeGb, 'size_formatted' => $sizeFormatted, 'label' => 'Detached Disk ' . $unusedIndex, ]; } /** * Check if a disk should be excluded (cloud-init, EFI, TPM). */ protected function isExcludedDisk(string $value): bool { $excludePatterns = [ 'cloudinit', 'vm-\d+-cloudinit', 'efidisk', 'tpmstate', ]; foreach ($excludePatterns as $pattern) { if (preg_match('/' . $pattern . '/i', $value)) { return true; } } return false; } /** * Parse a disk configuration string into structured data. */ protected function parseDiskString(string $value, string $busType, int $deviceIndex): ?array { // Example: cephpool:vm-100-disk-0,size=32G,cache=none,ssd=1 $parts = explode(',', $value); $volumePart = $parts[0] ?? ''; // Extract size $sizeGb = 0; foreach ($parts as $part) { if (preg_match('/^size=(\d+)([KMGT]?)$/i', $part, $sizeMatch)) { $size = (float) $sizeMatch[1]; $unit = strtoupper($sizeMatch[2] ?? 'G'); switch ($unit) { case 'K': $sizeGb = $size / (1024 * 1024); break; case 'M': $sizeGb = $size / 1024; break; case 'G': case '': $sizeGb = $size; break; case 'T': $sizeGb = $size * 1024; break; } } } // Extract cache mode $cache = 'none'; foreach ($parts as $part) { if (preg_match('/^cache=(\w+)$/', $part, $cacheMatch)) { $cache = $cacheMatch[1]; } } // Extract AIO type $aio = 'io_uring'; foreach ($parts as $part) { if (preg_match('/^aio=(\w+)$/', $part, $aioMatch)) { $aio = $aioMatch[1]; } } // Extract SSD emulation $ssd = false; foreach ($parts as $part) { if (preg_match('/^ssd=(\d)$/', $part, $ssdMatch)) { $ssd = (bool) $ssdMatch[1]; } } // Extract IO thread $iothread = false; foreach ($parts as $part) { if (preg_match('/^iothread=(\d)$/', $part, $iothreadMatch)) { $iothread = (bool) $iothreadMatch[1]; } } // Extract discard $discard = false; foreach ($parts as $part) { if ($part === 'discard=on') { $discard = true; } } // Check if attached (not detached/unused) $isAttached = !str_starts_with($volumePart, 'unused'); return [ 'key' => $busType . $deviceIndex, 'bus' => $busType, 'index' => $deviceIndex, 'volume' => $volumePart, 'size' => $sizeGb, 'cache' => $cache, 'aio' => $aio, 'ssd' => $ssd, 'iothread' => $iothread, 'discard' => $discard, 'attached' => $isAttached, 'raw' => $value, 'label' => $this->getDiskLabel($busType, $deviceIndex), ]; } /** * Get a human-readable label for a disk. */ protected function getDiskLabel(string $busType, int $index): string { $busLabels = [ 'ide' => 'IDE', 'sata' => 'SATA', 'scsi' => 'SCSI', 'virtio' => 'VirtIO', ]; return ($busLabels[$busType] ?? strtoupper($busType)) . ' ' . $index; } /** * Get the next available device index for a bus type. */ protected function getNextAvailableIndex(string $busType): ?int { $maxIndex = $this->busTypes[$busType]['max'] ?? 4; $usedIndices = []; foreach ($this->drives as $drive) { if ($drive['bus'] === $busType) { $usedIndices[] = $drive['index']; } } for ($i = 0; $i < $maxIndex; $i++) { if (!in_array($i, $usedIndices)) { return $i; } } return null; } /** * Open the add drive modal. */ public function openAddDriveModal(): void { $this->resetAddDriveForm(); $this->showAddDriveModal = true; } /** * Close the add drive modal. */ public function closeAddDriveModal(): void { $this->showAddDriveModal = false; $this->resetAddDriveForm(); } /** * Reset the add drive form. */ protected function resetAddDriveForm(): void { $this->newDriveBus = 'scsi'; $this->newDriveSize = 10; $this->newDriveCache = 'none'; $this->newDriveAio = 'io_uring'; $this->newDriveSsd = false; $this->newDriveIothread = true; $this->newDriveDiscard = true; } /** * Add a new drive to the VM. */ public function addDrive(): void { // Validate size if ($this->newDriveSize < 1) { $this->notify('Drive size must be at least 1 GB.', 'error'); return; } // Check storage quota $newTotal = $this->totalAllocatedStorage + $this->newDriveSize; if ($newTotal > $this->maxAllowedStorage) { $remaining = $this->maxAllowedStorage - $this->totalAllocatedStorage; $this->notify("Adding this drive would exceed your storage quota. You have {$remaining} GB remaining.", 'error'); return; } // Get next available index $index = $this->getNextAvailableIndex($this->newDriveBus); if ($index === null) { $this->notify("No available slots for {$this->busTypes[$this->newDriveBus]['label']} drives.", 'error'); return; } // Build drive configuration string $driveConfig = $this->buildDriveConfig( $this->newDriveSize, $this->newDriveCache, $this->newDriveAio, $this->newDriveSsd, $this->newDriveIothread, $this->newDriveDiscard, $this->newDriveBus ); try { $proxmox = new Proxmox; $driveKey = $this->newDriveBus . $index; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ $driveKey => $this->storagePool . ':' . $this->newDriveSize . ',' . $driveConfig, ] ); $this->notify("Drive {$driveKey} created successfully. A reboot may be required.", 'success'); $this->closeAddDriveModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to add drive: ' . $e->getMessage(), 'error'); } } /** * Build drive configuration string. */ protected function buildDriveConfig( int $size, string $cache, string $aio, bool $ssd, bool $iothread, bool $discard, string $busType ): string { $config = []; if ($cache !== 'none') { $config[] = 'cache=' . $cache; } if ($aio !== 'io_uring') { $config[] = 'aio=' . $aio; } // SSD emulation - only available for IDE, SATA, SCSI (not VirtIO Block) if ($ssd && $this->busTypes[$busType]['ssd_support']) { $config[] = 'ssd=1'; } // IO thread - only available for SCSI and VirtIO if ($iothread && $this->busTypes[$busType]['iothread_support']) { $config[] = 'iothread=1'; } if ($discard) { $config[] = 'discard=on'; } return implode(',', $config); } /** * Open the edit drive modal. */ public function openEditDriveModal(string $driveKey): void { if (!isset($this->drives[$driveKey])) { $this->notify('Drive not found.', 'error'); return; } $drive = $this->drives[$driveKey]; $this->editingDriveKey = $driveKey; $this->editDriveCache = $drive['cache']; $this->editDriveAio = $drive['aio']; $this->editDriveSsd = $drive['ssd']; $this->editDriveIothread = $drive['iothread']; $this->editDriveDiscard = $drive['discard']; $this->showEditDriveModal = true; } /** * Close the edit drive modal. */ public function closeEditDriveModal(): void { $this->showEditDriveModal = false; $this->editingDriveKey = null; } /** * Save drive settings. */ public function saveDriveSettings(): void { if (!$this->editingDriveKey || !isset($this->drives[$this->editingDriveKey])) { $this->notify('No drive selected.', 'error'); return; } $drive = $this->drives[$this->editingDriveKey]; $busType = $drive['bus']; // Build the new configuration $configParts = []; // Keep the volume reference $configParts[] = $drive['volume']; // Add size if ($drive['size'] > 0) { $configParts[] = 'size=' . (int)$drive['size'] . 'G'; } // Add cache if not default if ($this->editDriveCache !== 'none') { $configParts[] = 'cache=' . $this->editDriveCache; } // Add AIO if not default if ($this->editDriveAio !== 'io_uring') { $configParts[] = 'aio=' . $this->editDriveAio; } // Add SSD emulation if supported and enabled if ($this->editDriveSsd && $this->busTypes[$busType]['ssd_support']) { $configParts[] = 'ssd=1'; } // Add IO thread if supported and enabled if ($this->editDriveIothread && $this->busTypes[$busType]['iothread_support']) { $configParts[] = 'iothread=1'; } // Add discard if ($this->editDriveDiscard) { $configParts[] = 'discard=on'; } $newConfig = implode(',', $configParts); try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ $this->editingDriveKey => $newConfig, ] ); $this->notify('Drive settings updated successfully. A reboot may be required.', 'success'); $this->closeEditDriveModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to update drive settings: ' . $e->getMessage(), 'error'); } } /** * Open the move drive modal (change bus/device type). */ public function openMoveDriveModal(string $driveKey): void { if (!isset($this->drives[$driveKey])) { $this->notify('Drive not found.', 'error'); return; } $this->movingDriveKey = $driveKey; $this->moveDriveTargetBus = $this->drives[$driveKey]['bus']; $this->showMoveDriveModal = true; } /** * Close the move drive modal. */ public function closeMoveDriveModal(): void { $this->showMoveDriveModal = false; $this->movingDriveKey = null; } /** * Open the detach confirmation modal. */ public function openDetachModal(string $driveKey): void { if (!isset($this->drives[$driveKey])) { $this->notify('Drive not found.', 'error'); return; } $this->detachingDriveKey = $driveKey; $this->showDetachModal = true; } /** * Close the detach confirmation modal. */ public function closeDetachModal(): void { $this->showDetachModal = false; $this->detachingDriveKey = null; } /** * Detach a drive (move to unused). */ public function detachDrive(): void { if (!$this->detachingDriveKey || !isset($this->drives[$this->detachingDriveKey])) { $this->notify('Drive not found.', 'error'); return; } $driveKey = $this->detachingDriveKey; $drive = $this->drives[$driveKey]; // Check VM status - must be stopped to detach if ($this->status && $this->status->status !== 'stopped') { $this->notify('VM must be stopped to detach drives.', 'error'); return; } try { $proxmox = new Proxmox; // Get current config to find next unused slot $config = $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config' ); // Find the next available unused slot $unusedIndex = 0; while (isset($config['data']['unused' . $unusedIndex])) { $unusedIndex++; } // First, assign the volume to an unused slot to preserve it $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'unused' . $unusedIndex => $drive['volume'], ] ); // Then remove the drive configuration (the volume is already safe in unused) $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ $driveKey => 'none', ] ); $this->notify("Drive {$driveKey} detached successfully.", 'success'); $this->closeDetachModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to detach drive: ' . $e->getMessage(), 'error'); } } /** * Move/reattach a drive to a new bus type. */ public function moveDrive(): void { if (!$this->movingDriveKey || !isset($this->drives[$this->movingDriveKey])) { $this->notify('No drive selected.', 'error'); return; } // Check VM status if ($this->status && $this->status->status !== 'stopped') { $this->notify('VM must be stopped to move drives.', 'error'); return; } $drive = $this->drives[$this->movingDriveKey]; $sourceBus = $drive['bus']; $targetBus = $this->moveDriveTargetBus; // Get next available index on target bus $targetIndex = $this->getNextAvailableIndex($targetBus); if ($targetIndex === null) { $this->notify("No available slots for {$this->busTypes[$targetBus]['label']} drives.", 'error'); return; } try { $proxmox = new Proxmox; $targetKey = $targetBus . $targetIndex; // Use Proxmox move_disk API $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/move_disk', 'post', [ 'disk' => $this->movingDriveKey, 'storage' => $this->storagePool, 'target-disk' => $targetKey, ] ); $this->notify("Drive moved from {$this->movingDriveKey} to {$targetKey} successfully.", 'success'); $this->closeMoveDriveModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to move drive: ' . $e->getMessage(), 'error'); } } /** * Open the delete confirmation modal. */ public function openDeleteModal(string $driveKey): void { if (!isset($this->drives[$driveKey])) { $this->notify('Drive not found.', 'error'); return; } $this->deletingDriveKey = $driveKey; $this->showDeleteModal = true; } /** * Close the delete confirmation modal. */ public function closeDeleteModal(): void { $this->showDeleteModal = false; $this->deletingDriveKey = null; } /** * Delete a drive permanently. */ public function deleteDrive(): void { if (!$this->deletingDriveKey || !isset($this->drives[$this->deletingDriveKey])) { $this->notify('Drive not found.', 'error'); return; } $driveKey = $this->deletingDriveKey; // Check VM status if ($this->status && $this->status->status !== 'stopped') { $this->notify('VM must be stopped to delete drives.', 'error'); return; } try { $proxmox = new Proxmox; // First detach the disk, then delete the volume $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'delete' => $driveKey, ] ); // Now find and delete the unused disk // Reload config to get the unused disk reference $config = $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config' )->json()['data'] ?? []; // Find unused disks and delete the one we just detached foreach ($config as $key => $value) { if (str_starts_with($key, 'unused') && str_contains($value, $this->drives[$driveKey]['volume'])) { $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'delete' => $key, ] ); break; } } $this->notify("Drive {$driveKey} deleted successfully.", 'success'); $this->closeDeleteModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to delete drive: ' . $e->getMessage(), 'error'); } } /** * Open the reattach modal for an unused drive. */ public function openReattachModal(string $unusedKey): void { if (!isset($this->unusedDrives[$unusedKey])) { $this->notify('Unused drive not found.', 'error'); return; } $this->reattachingDriveKey = $unusedKey; $this->reattachTargetBus = 'scsi'; $this->showReattachModal = true; } /** * Close the reattach modal. */ public function closeReattachModal(): void { $this->showReattachModal = false; $this->reattachingDriveKey = null; } /** * Reattach an unused drive to a bus. */ public function reattachDrive(): void { if (!$this->reattachingDriveKey || !isset($this->unusedDrives[$this->reattachingDriveKey])) { $this->notify('Unused drive not found.', 'error'); return; } // Check VM status if ($this->status && $this->status->status !== 'stopped') { $this->notify('VM must be stopped to reattach drives.', 'error'); return; } $unusedDrive = $this->unusedDrives[$this->reattachingDriveKey]; $targetBus = $this->reattachTargetBus; // Get next available index on target bus $targetIndex = $this->getNextAvailableIndex($targetBus); if ($targetIndex === null) { $this->notify("No available slots for {$this->busTypes[$targetBus]['label']} drives.", 'error'); return; } $targetKey = $targetBus . $targetIndex; try { $proxmox = new Proxmox; // First, get the current config to retrieve the exact unused disk value $config = $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config' ); $unusedValue = $config['data'][$this->reattachingDriveKey] ?? null; if (!$unusedValue) { $this->notify('Unused drive no longer exists. Please refresh the page.', 'error'); $this->loadStorageConfiguration(false); return; } // Build the disk configuration using the exact volume from the unused entry $configParts = [$unusedValue]; // Add default settings for the bus type $configParts[] = 'cache=none'; $configParts[] = 'aio=io_uring'; // Add IO thread for supported bus types if ($this->busTypes[$targetBus]['iothread_support']) { $configParts[] = 'iothread=1'; } // Add discard $configParts[] = 'discard=on'; $diskConfig = implode(',', $configParts); // First, set the disk on the new bus (this attaches the volume) $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ $targetKey => $diskConfig, ] ); // Then remove the unused entry using delete parameter // This is safe because the volume is now attached to the new bus try { $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'delete' => $this->reattachingDriveKey, ] ); } catch (\Exception $e) { // Unused entry might already be cleaned up automatically, ignore this error } $this->notify("Drive reattached as {$targetKey} successfully.", 'success'); $this->closeReattachModal(); $this->loadStorageConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to reattach drive: ' . $e->getMessage(), 'error'); } } /** * Delete an unused drive permanently. */ public function deleteUnusedDrive(string $unusedKey): void { if (!isset($this->unusedDrives[$unusedKey])) { $this->notify('Unused drive not found.', 'error'); return; } // Check VM status if ($this->status && $this->status->status !== 'stopped') { $this->notify('VM must be stopped to delete drives.', 'error'); return; } try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'delete' => $unusedKey, ] ); $this->notify("Unused drive deleted successfully.", 'success'); $this->loadStorageConfiguration(false); } catch (\Exception $e) { $this->notify('Failed to delete unused drive: ' . $e->getMessage(), 'error'); } } /** * Save the SCSI controller type. */ public function saveScsiController(): void { if ($this->scsiControllerType === $this->originalScsiControllerType) { $this->notify('No changes to save.', 'info'); return; } // Check VM status if ($this->status && $this->status->status !== 'stopped') { $this->notify('VM must be stopped to change SCSI controller type.', 'error'); return; } try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [ 'scsihw' => $this->scsiControllerType, ] ); $this->originalScsiControllerType = $this->scsiControllerType; $this->notify('SCSI controller type updated successfully. Please restart the VM for changes to take effect.', 'success'); } catch (\Exception $e) { $this->notify('Failed to update SCSI controller type: ' . $e->getMessage(), 'error'); } } /** * Get available storage remaining. */ public function getRemainingStorageProperty(): float { return max(0, $this->maxAllowedStorage - $this->totalAllocatedStorage); } /** * Check if SCSI controller settings are synced. */ public function getScsiControllerSyncedProperty(): bool { return $this->scsiControllerType === $this->originalScsiControllerType; } /** * Check if SSD emulation is supported for the current bus type being edited. */ public function getEditDriveSsdSupportedProperty(): bool { if (!$this->editingDriveKey || !isset($this->drives[$this->editingDriveKey])) { return false; } $busType = $this->drives[$this->editingDriveKey]['bus']; return $this->busTypes[$busType]['ssd_support'] ?? false; } /** * Check if IO thread is supported for the current bus type being edited. */ public function getEditDriveIothreadSupportedProperty(): bool { if (!$this->editingDriveKey || !isset($this->drives[$this->editingDriveKey])) { return false; } $busType = $this->drives[$this->editingDriveKey]['bus']; return $this->busTypes[$busType]['iothread_support'] ?? false; } /** * Check if SSD emulation is supported for the new drive bus type. */ public function getNewDriveSsdSupportedProperty(): bool { return $this->busTypes[$this->newDriveBus]['ssd_support'] ?? false; } /** * Check if IO thread is supported for the new drive bus type. */ public function getNewDriveIothreadSupportedProperty(): bool { return $this->busTypes[$this->newDriveBus]['iothread_support'] ?? false; } /** * Handle bus type change for new drive form. */ public function updatedNewDriveBus(): void { // Reset SSD and IO thread options based on bus type support if (!$this->newDriveSsdSupported) { $this->newDriveSsd = false; } if (!$this->newDriveIothreadSupported) { $this->newDriveIothread = false; } else { // Enable IO thread by default for supported bus types $this->newDriveIothread = true; } } public function render() { return view('proxmox::storage')->layoutData([ 'sidebar' => true, ]); } }