1322 lines
42 KiB
PHP
1322 lines
42 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
|
|
|
|
use App\Helpers\ExtensionHelper;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Attributes\Locked;
|
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
|
|
|
class Storage extends Component
|
|
{
|
|
/**
|
|
* List of disk drives attached to the VM.
|
|
*/
|
|
public array $drives = [];
|
|
|
|
/**
|
|
* List of detached/unused drives.
|
|
*/
|
|
public array $unusedDrives = [];
|
|
|
|
/**
|
|
* Total allocated storage size in GB.
|
|
*/
|
|
public float $totalAllocatedStorage = 0;
|
|
|
|
/**
|
|
* Maximum allowed storage in GB from plan configuration.
|
|
*/
|
|
#[Locked]
|
|
public float $maxAllowedStorage = 0;
|
|
|
|
/**
|
|
* Current SCSI controller type.
|
|
*/
|
|
public string $scsiControllerType = 'virtio-scsi-pci';
|
|
|
|
/**
|
|
* Original SCSI controller type for tracking changes.
|
|
*/
|
|
#[Locked]
|
|
public string $originalScsiControllerType = 'virtio-scsi-pci';
|
|
|
|
/**
|
|
* Available SCSI controller types.
|
|
*/
|
|
public array $scsiControllerOptions = [
|
|
['value' => '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;
|
|
$storageContent = $proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/storage/' . $storageName . '/content',
|
|
data: ['vmid' => $this->server->vm_id]
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
// Check if already at maximum detached drives (2)
|
|
if (count($this->unusedDrives) >= 2) {
|
|
$this->notify('Maximum of 2 detached drives allowed. Please reattach or delete existing detached drives first.', '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 using delete (not 'none' which creates CD-ROM on SCSI)
|
|
// The volume is already safe in the unused slot
|
|
$proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
|
|
'put',
|
|
[
|
|
'delete' => $driveKey,
|
|
]
|
|
);
|
|
|
|
$this->notify("Drive {$driveKey} detached successfully.", 'success');
|
|
$this->closeDetachModal();
|
|
$this->loadStorageConfiguration(false);
|
|
|
|
// Dispatch browser event to refresh the page
|
|
$this->dispatch('refresh-page');
|
|
} 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 change device bus.', 'error');
|
|
return;
|
|
}
|
|
|
|
$drive = $this->drives[$this->movingDriveKey];
|
|
$targetBus = $this->moveDriveTargetBus;
|
|
|
|
// Prevent moving to the same bus type
|
|
if ($drive['bus'] === $targetBus) {
|
|
$this->notify('Drive is already on the ' . $this->busTypes[$targetBus]['label'] . ' bus.', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
|
|
// Get current config to check actual slot availability
|
|
$config = $proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config'
|
|
);
|
|
|
|
// Find the next available slot on the target bus by checking actual Proxmox config
|
|
$maxIndex = $this->busTypes[$targetBus]['max'] ?? 4;
|
|
$targetIndex = null;
|
|
|
|
for ($i = 0; $i < $maxIndex; $i++) {
|
|
$slotKey = $targetBus . $i;
|
|
// Check if this slot is occupied in the actual config
|
|
if (!isset($config['data'][$slotKey])) {
|
|
$targetIndex = $i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($targetIndex === null) {
|
|
$this->notify("No available slots for {$this->busTypes[$targetBus]['label']} drives.", 'error');
|
|
return;
|
|
}
|
|
|
|
$targetKey = $targetBus . $targetIndex;
|
|
|
|
// Find the next available unused slot
|
|
$unusedIndex = 0;
|
|
while (isset($config['data']['unused' . $unusedIndex])) {
|
|
$unusedIndex++;
|
|
}
|
|
|
|
// Step 1: Detach - 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'],
|
|
]
|
|
);
|
|
|
|
// Step 2: Remove the old drive configuration using delete (not 'none' which creates CD-ROM)
|
|
$proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
|
|
'put',
|
|
[
|
|
'delete' => $this->movingDriveKey,
|
|
]
|
|
);
|
|
|
|
// Step 3: Reattach - build the new disk configuration
|
|
$configParts = [$drive['volume']];
|
|
|
|
// Add settings
|
|
$configParts[] = 'cache=' . ($drive['cache'] ?? 'none');
|
|
$configParts[] = 'aio=' . ($drive['aio'] ?? 'io_uring');
|
|
|
|
// Add IO thread for supported bus types
|
|
if ($this->busTypes[$targetBus]['iothread_support'] && ($drive['iothread'] ?? false)) {
|
|
$configParts[] = 'iothread=1';
|
|
}
|
|
|
|
// Add SSD emulation for supported bus types
|
|
if ($this->busTypes[$targetBus]['ssd_support'] && ($drive['ssd'] ?? false)) {
|
|
$configParts[] = 'ssd=1';
|
|
}
|
|
|
|
// Add discard if enabled
|
|
if ($drive['discard'] ?? false) {
|
|
$configParts[] = 'discard=on';
|
|
}
|
|
|
|
$diskConfig = implode(',', $configParts);
|
|
|
|
// Attach to the new bus
|
|
$proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
|
|
'put',
|
|
[
|
|
$targetKey => $diskConfig,
|
|
]
|
|
);
|
|
|
|
// Step 4: Clean up the unused entry
|
|
try {
|
|
$proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
|
|
'put',
|
|
[
|
|
'delete' => 'unused' . $unusedIndex,
|
|
]
|
|
);
|
|
} catch (\Exception $e) {
|
|
// Unused entry might already be cleaned up, ignore
|
|
}
|
|
|
|
$this->notify("Drive moved from {$this->movingDriveKey} to {$targetKey} successfully.", 'success');
|
|
$this->closeMoveDriveModal();
|
|
$this->loadStorageConfiguration(false);
|
|
|
|
// Dispatch browser event to refresh the page
|
|
$this->dispatch('refresh-page');
|
|
} catch (\Exception $e) {
|
|
$this->notify('Failed to change device bus: ' . $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;
|
|
|
|
// Check if reattaching this drive would exceed the storage quota
|
|
// Calculate attached drives total (excluding the detached drive being reattached)
|
|
$attachedDrivesTotal = 0;
|
|
foreach ($this->drives as $drive) {
|
|
$attachedDrivesTotal += $drive['size'] ?? 0;
|
|
}
|
|
|
|
// The drive being reattached is already counted in totalAllocatedStorage (which includes unused drives)
|
|
// So we just need to check if the total exceeds the max
|
|
if ($this->totalAllocatedStorage > $this->maxAllowedStorage && $this->maxAllowedStorage > 0) {
|
|
$this->notify('Storage quota exceeded. Total storage (' . number_format($this->totalAllocatedStorage, 1) . ' GB) exceeds your plan allocation (' . number_format($this->maxAllowedStorage, 1) . ' GB). Please upgrade your service or delete drives to allow this configuration.', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
|
|
// First, get the current config to check actual slot availability and retrieve the exact unused disk value
|
|
$config = $proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config'
|
|
);
|
|
|
|
// Find the next available slot on the target bus by checking actual Proxmox config
|
|
$maxIndex = $this->busTypes[$targetBus]['max'] ?? 4;
|
|
$targetIndex = null;
|
|
|
|
for ($i = 0; $i < $maxIndex; $i++) {
|
|
$slotKey = $targetBus . $i;
|
|
// Check if this slot is occupied in the actual config
|
|
if (!isset($config['data'][$slotKey])) {
|
|
$targetIndex = $i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($targetIndex === null) {
|
|
$this->notify("No available slots for {$this->busTypes[$targetBus]['label']} drives.", 'error');
|
|
return;
|
|
}
|
|
|
|
$targetKey = $targetBus . $targetIndex;
|
|
|
|
$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,
|
|
]);
|
|
}
|
|
}
|