Files
Virtnet.bond/extensions/Servers/Proxmox/Livewire/Storage.php

1203 lines
37 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($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,
]);
}
}