997 lines
30 KiB
PHP
997 lines
30 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;
|
|
$this->totalAllocatedStorage = $totalSize;
|
|
|
|
// Parse unused/detached drives
|
|
$unusedDrives = [];
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Try to extract size from volume name or fetch from storage
|
|
// For now, we'll mark size as unknown until reattached
|
|
return [
|
|
'key' => 'unused' . $unusedIndex,
|
|
'index' => $unusedIndex,
|
|
'volume' => $volume,
|
|
'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;
|
|
|
|
// 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->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');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
]);
|
|
}
|
|
}
|