From 061ec6145cf7bb2ea07046a37196b02676a0d89c Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Fri, 5 Dec 2025 04:11:20 -0500 Subject: [PATCH] Add storage management functionality to Proxmox extension - Registered Livewire component for storage management. - Updated routes to include storage management endpoint. - Modified layout to add storage management link. - Created storage management view with features to add, edit, and manage drives. - Implemented modals for adding and editing drives with appropriate validations and options. --- .../Servers/Proxmox/Livewire/Storage.php | 869 ++++++++++++++++++ extensions/Servers/Proxmox/Proxmox.php | 1 + extensions/Servers/Proxmox/routes.php | 2 + .../Proxmox/views/components/layout.blade.php | 1 + .../Servers/Proxmox/views/storage.blade.php | 594 ++++++++++++ 5 files changed, 1467 insertions(+) create mode 100644 extensions/Servers/Proxmox/Livewire/Storage.php create mode 100644 extensions/Servers/Proxmox/views/storage.blade.php diff --git a/extensions/Servers/Proxmox/Livewire/Storage.php b/extensions/Servers/Proxmox/Livewire/Storage.php new file mode 100644 index 000000000..0b0c3d283 --- /dev/null +++ b/extensions/Servers/Proxmox/Livewire/Storage.php @@ -0,0 +1,869 @@ + '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'; + + /** + * 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. + */ + protected function loadMaxAllowedStorage(): void + { + $diskSetting = $this->server->service->product->settings() + ->where('key', 'disk') + ->first(); + + $this->maxAllowedStorage = $diskSetting ? (float) $diskSetting->value : 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; + $this->totalAllocatedStorage = $totalSize; + } + + /** + * 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; + } + + /** + * Detach a drive (move to unused). + */ + public function detachDrive(string $driveKey): void + { + if (!isset($this->drives[$driveKey])) { + $this->notify('Drive not found.', 'error'); + return; + } + + // 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; + + // Use the delete parameter to remove the disk from the config (keeps the volume as unused) + $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', + 'put', + [ + 'delete' => $driveKey, + ] + ); + + $this->notify("Drive {$driveKey} detached successfully.", 'success'); + $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'); + } + } + + /** + * Delete a drive permanently. + */ + public function deleteDrive(string $driveKey): void + { + if (!isset($this->drives[$driveKey])) { + $this->notify('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; + + // 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->loadStorageConfiguration(false); + } catch (\Exception $e) { + $this->notify('Failed to delete 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, + ]); + } +} diff --git a/extensions/Servers/Proxmox/Proxmox.php b/extensions/Servers/Proxmox/Proxmox.php index 2c4a3b0da..380a39d9a 100644 --- a/extensions/Servers/Proxmox/Proxmox.php +++ b/extensions/Servers/Proxmox/Proxmox.php @@ -43,6 +43,7 @@ class Proxmox extends ClassServer \Livewire\Livewire::component('proxmox.boot-order', \Paymenter\Extensions\Servers\Proxmox\Livewire\BootOrder::class); \Livewire\Livewire::component('proxmox.display', \Paymenter\Extensions\Servers\Proxmox\Livewire\Display::class); \Livewire\Livewire::component('proxmox.system', \Paymenter\Extensions\Servers\Proxmox\Livewire\System::class); + \Livewire\Livewire::component('proxmox.storage', \Paymenter\Extensions\Servers\Proxmox\Livewire\Storage::class); \Livewire\Livewire::component('proxmox.cloud-init', \Paymenter\Extensions\Servers\Proxmox\Livewire\CloudInit::class); \Livewire\Livewire::component('proxmox.subdomain', \Paymenter\Extensions\Servers\Proxmox\Livewire\Subdomain::class); \Livewire\Livewire::component('proxmox.reinstall', \Paymenter\Extensions\Servers\Proxmox\Livewire\Reinstall::class); diff --git a/extensions/Servers/Proxmox/routes.php b/extensions/Servers/Proxmox/routes.php index 675cd5f70..fb371f72f 100644 --- a/extensions/Servers/Proxmox/routes.php +++ b/extensions/Servers/Proxmox/routes.php @@ -10,6 +10,7 @@ use Paymenter\Extensions\Servers\Proxmox\Http\Controllers\LiveTrafficController; use Paymenter\Extensions\Servers\Proxmox\Livewire\Firewall; use Paymenter\Extensions\Servers\Proxmox\Livewire\Network; use Paymenter\Extensions\Servers\Proxmox\Livewire\Overview; +use Paymenter\Extensions\Servers\Proxmox\Livewire\Storage; use Paymenter\Extensions\Servers\Proxmox\Livewire\Subdomain; use Paymenter\Extensions\Servers\Proxmox\Livewire\System; use Paymenter\Extensions\Servers\Proxmox\Livewire\VNC; @@ -34,6 +35,7 @@ Route::group([ Route::get('{server}/subdomain', Subdomain::class)->name('server.subdomain'); Route::get('{server}/backups', BackupsIndex::class)->name('server.backups'); Route::get('{server}/statistics', Statistics::class)->name('server.statistics'); + Route::get('{server}/storage', Storage::class)->name('server.storage'); Route::get('{server}/vnc', VNC::class)->name('server.vnc'); Route::get('{server}/xterm', XTerm::class)->name('server.xterm'); Route::get('{server}/help', Help::class)->name('server.help'); diff --git a/extensions/Servers/Proxmox/views/components/layout.blade.php b/extensions/Servers/Proxmox/views/components/layout.blade.php index a943c49c8..f032f3474 100644 --- a/extensions/Servers/Proxmox/views/components/layout.blade.php +++ b/extensions/Servers/Proxmox/views/components/layout.blade.php @@ -158,6 +158,7 @@ 'items' => [ ['label' => 'Boot Settings', 'view' => 'boot-order', 'route' => route('proxmox.server.boot-order', ['server' => $server->id]), 'advanced' => true], ['label' => 'Display', 'view' => 'display', 'route' => route('proxmox.server.display', ['server' => $server->id]), 'advanced' => true], + ['label' => 'Storage', 'view' => 'storage', 'route' => route('proxmox.server.storage', ['server' => $server->id]), 'advanced' => true], ['label' => 'System', 'view' => 'system', 'route' => route('proxmox.server.system', ['server' => $server->id]), 'advanced' => true], ] ], diff --git a/extensions/Servers/Proxmox/views/storage.blade.php b/extensions/Servers/Proxmox/views/storage.blade.php new file mode 100644 index 000000000..07ea63416 --- /dev/null +++ b/extensions/Servers/Proxmox/views/storage.blade.php @@ -0,0 +1,594 @@ + + + +
+ +
+
+

Storage Management

+
+ + + + + +
+
+

Manage virtual hard disks attached to your VPS. Add, configure, or remove storage drives.

+
+ + +
+
+
+

Storage Quota

+

+ {{ number_format($totalAllocatedStorage, 1) }} GB used of {{ number_format($maxAllowedStorage, 1) }} GB allocated +

+
+
+ + @if($this->remainingStorage > 0) + + @endif +
+
+ + +
+ @php + $usagePercent = $maxAllowedStorage > 0 ? min(100, ($totalAllocatedStorage / $maxAllowedStorage) * 100) : 0; + $colorClass = $usagePercent < 70 ? 'bg-success' : ($usagePercent < 90 ? 'bg-warning' : 'bg-danger'); + @endphp +
+
+
+ {{ number_format($this->remainingStorage, 1) }} GB remaining + {{ number_format($usagePercent, 1) }}% used +
+
+ + +
+
+
+
+

SCSI Controller

+
+ + + + + +
+
+

Select the SCSI controller type for SCSI drives.

+
+
+ +
+
+ +
+ +
+ + @if($status && $status->status !== 'stopped' && !$this->scsiControllerSynced) +
+ + VM must be stopped to change the SCSI controller type. +
+ @endif +
+ + +
+
+

Attached Drives

+

Manage the virtual hard disks connected to your VPS.

+
+ + @if(count($drives) === 0) +
+ +

No drives attached to this VM.

+ @if($this->remainingStorage > 0) + + @endif +
+ @else +
+ @foreach($drives as $driveKey => $drive) +
+
+
+
+ +
+
+
+

{{ $drive['label'] }}

+ {{ $driveKey }} + @if($drive['ssd']) + SSD + @endif + @if($drive['iothread']) + IO Thread + @endif +
+
+ {{ number_format($drive['size'], 1) }} GB + Cache: {{ ucfirst($drive['cache']) }} + AIO: {{ $drive['aio'] }} + @if($drive['discard']) + Discard enabled + @endif +
+
+
+ +
+ + + + +
+
+
+ @endforeach +
+ @endif +
+
+ + + @if($showAddDriveModal) +
+
+
+
+
+

Add New Drive

+ +
+ +
+ +
+ + +

+ @if($newDriveBus === 'scsi') + SCSI provides best performance with IO thread support. Recommended for most use cases. + @elseif($newDriveBus === 'virtio') + VirtIO Block offers high performance but does not support SSD emulation. + @elseif($newDriveBus === 'sata') + SATA provides good compatibility with most operating systems. + @else + IDE is primarily for legacy compatibility. + @endif +

+
+ + +
+ + +

Maximum available: {{ number_format($this->remainingStorage, 1) }} GB

+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + + +
+ + @if($status && $status->status !== 'stopped') +
+ + VM must be stopped to add new drives. +
+ @endif +
+ +
+ + +
+
+
+
+ @endif + + + @if($showEditDriveModal && $editingDriveKey && isset($drives[$editingDriveKey])) + @php $editDrive = $drives[$editingDriveKey]; @endphp +
+
+
+
+
+

Edit Drive Settings - {{ $editDrive['label'] }}

+ +
+ +
+
+
+
Device: {{ $editingDriveKey }}
+
Size: {{ number_format($editDrive['size'], 1) }} GB
+
Bus Type: {{ $busTypes[$editDrive['bus']]['label'] ?? ucfirst($editDrive['bus']) }}
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + + +
+ + @if(!$this->editDriveSsdSupported) +

Note: SSD emulation is not available for {{ $busTypes[$editDrive['bus']]['label'] ?? ucfirst($editDrive['bus']) }} drives.

+ @endif + + @if(!$this->editDriveIothreadSupported) +

Note: IO Thread is not available for {{ $busTypes[$editDrive['bus']]['label'] ?? ucfirst($editDrive['bus']) }} drives.

+ @endif +
+ +
+ + +
+
+
+
+ @endif + + + @if($showMoveDriveModal && $movingDriveKey && isset($drives[$movingDriveKey])) + @php $moveDrive = $drives[$movingDriveKey]; @endphp +
+
+
+
+
+

Move Drive - {{ $moveDrive['label'] }}

+ +
+ +
+
+

Current Device: {{ $movingDriveKey }} ({{ $busTypes[$moveDrive['bus']]['label'] ?? ucfirst($moveDrive['bus']) }})

+

Size: {{ number_format($moveDrive['size'], 1) }} GB

+
+ +
+ + Moving a drive changes its bus type. This may require reinstalling drivers in the guest OS. +
+ + +
+ + +
+ + @if($status && $status->status !== 'stopped') +
+ + VM must be stopped to move drives. +
+ @endif +
+ +
+ + +
+
+
+
+ @endif +