feat: implement reattach and delete functionality for unused drives with UI updates
This commit is contained in:
@@ -236,10 +236,10 @@ class Storage extends Component
|
||||
});
|
||||
|
||||
$this->drives = $drives;
|
||||
$this->totalAllocatedStorage = $totalSize;
|
||||
|
||||
// Parse unused/detached drives
|
||||
$unusedDrives = [];
|
||||
$unusedTotalSize = 0;
|
||||
foreach ($config as $key => $value) {
|
||||
if (!preg_match('/^unused(\d+)$/', $key, $matches)) {
|
||||
continue;
|
||||
@@ -254,6 +254,7 @@ class Storage extends Component
|
||||
$unusedInfo = $this->parseUnusedDiskString($value, $unusedIndex);
|
||||
if ($unusedInfo) {
|
||||
$unusedDrives[$key] = $unusedInfo;
|
||||
$unusedTotalSize += $unusedInfo['size'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +266,9 @@ class Storage extends Component
|
||||
});
|
||||
|
||||
$this->unusedDrives = $unusedDrives;
|
||||
|
||||
// Total storage includes both attached and detached drives
|
||||
$this->totalAllocatedStorage = $totalSize + $unusedTotalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,12 +280,45 @@ class Storage extends Component
|
||||
// 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
|
||||
// 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,
|
||||
];
|
||||
}
|
||||
@@ -881,6 +918,129 @@ class Storage extends Component
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Build the disk configuration
|
||||
$configParts = [$unusedDrive['volume']];
|
||||
|
||||
// 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);
|
||||
|
||||
// Set the disk on the new bus and delete the unused reference
|
||||
$proxmox->request(
|
||||
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
|
||||
'put',
|
||||
[
|
||||
$targetKey => $diskConfig,
|
||||
'delete' => $this->reattachingDriveKey,
|
||||
]
|
||||
);
|
||||
|
||||
$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.
|
||||
*/
|
||||
|
||||
@@ -103,6 +103,9 @@
|
||||
<h3 class="text-lg font-semibold">Storage Quota</h3>
|
||||
<p class="text-sm text-base/60 mt-1">
|
||||
{{ number_format($totalAllocatedStorage, 1) }} GB used of {{ number_format($maxAllowedStorage, 1) }} GB allocated
|
||||
@if(count($unusedDrives) > 0)
|
||||
<span class="text-warning">(includes detached drives)</span>
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -301,6 +304,62 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Detached/Unused Drives -->
|
||||
@if(count($unusedDrives) > 0)
|
||||
<div class="bg-background-secondary border border-neutral rounded-lg overflow-hidden">
|
||||
<div class="p-5 border-b border-neutral">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-ri-eject-line class="size-5 text-warning" />
|
||||
<h3 class="text-lg font-semibold">Detached Drives</h3>
|
||||
</div>
|
||||
<p class="text-sm text-base/60 mt-1">These drives have been detached but their data is preserved. You can reattach them or delete them permanently.</p>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-neutral">
|
||||
@foreach($unusedDrives as $unusedKey => $unusedDrive)
|
||||
<div class="p-4 hover:bg-background/50 transition-colors">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-warning/10 flex-shrink-0">
|
||||
<x-ri-hard-drive-2-line class="size-5 text-warning" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h4 class="font-semibold text-base">{{ $unusedDrive['label'] }}</h4>
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded bg-warning/20 text-warning">{{ $unusedKey }}</span>
|
||||
@if(isset($unusedDrive['size_formatted']))
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded bg-info/20 text-info">{{ $unusedDrive['size_formatted'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-1 text-xs text-base/60">
|
||||
<span class="truncate max-w-[200px]" title="{{ $unusedDrive['volume'] }}">{{ $unusedDrive['volume'] }}</span>
|
||||
@if(isset($unusedDrive['storage']))
|
||||
<span>Storage: {{ $unusedDrive['storage'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button wire:click="openReattachModal('{{ $unusedKey }}')"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-success/50 bg-success/10 text-success hover:bg-success/20 transition-colors">
|
||||
<x-ri-link class="size-3.5" />
|
||||
Reattach
|
||||
</button>
|
||||
<button wire:click="deleteUnusedDrive('{{ $unusedKey }}')"
|
||||
wire:confirm="Are you sure you want to permanently delete this detached drive? This action cannot be undone."
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-danger/50 bg-danger/10 text-danger hover:bg-danger/20 transition-colors">
|
||||
<x-ri-delete-bin-line class="size-3.5" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Add Drive Modal -->
|
||||
@@ -711,4 +770,120 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Reattach Drive Modal -->
|
||||
@if($showReattachModal)
|
||||
<div class="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div class="fixed inset-0 bg-black/50 transition-opacity"></div>
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-lg transform overflow-hidden rounded-xl bg-background border border-neutral shadow-xl transition-all">
|
||||
<div class="flex items-center justify-between p-5 border-b border-neutral">
|
||||
<h3 class="text-lg font-semibold">Reattach Drive</h3>
|
||||
<button wire:click="closeReattachModal" class="text-foreground-secondary hover:text-foreground transition-colors">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
@if($reattachingDriveKey && isset($unusedDrives[$reattachingDriveKey]))
|
||||
<div class="bg-background-secondary rounded-lg p-3 border border-neutral">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-info/10">
|
||||
<x-ri-hard-drive-3-line class="size-5 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ $reattachingDriveKey }}</p>
|
||||
<p class="text-xs text-foreground-secondary">
|
||||
{{ $unusedDrives[$reattachingDriveKey]['size_formatted'] ?? 'Unknown size' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Target Bus</label>
|
||||
<select wire:model="reattachTargetBus"
|
||||
class="w-full px-4 py-2 text-sm rounded-lg border border-neutral bg-background focus:outline-none focus:ring-2 focus:ring-primary/50">
|
||||
<option value="">Select bus type...</option>
|
||||
<optgroup label="IDE (Max: 4)">
|
||||
@for($i = 0; $i <= 3; $i++)
|
||||
@php
|
||||
$key = "ide{$i}";
|
||||
$isUsed = isset($drives[$key]);
|
||||
@endphp
|
||||
<option value="{{ $key }}" @if($isUsed) disabled @endif>
|
||||
ide{{ $i }} @if($isUsed) (In use) @endif
|
||||
</option>
|
||||
@endfor
|
||||
</optgroup>
|
||||
<optgroup label="SATA (Max: 6)">
|
||||
@for($i = 0; $i <= 5; $i++)
|
||||
@php
|
||||
$key = "sata{$i}";
|
||||
$isUsed = isset($drives[$key]);
|
||||
@endphp
|
||||
<option value="{{ $key }}" @if($isUsed) disabled @endif>
|
||||
sata{{ $i }} @if($isUsed) (In use) @endif
|
||||
</option>
|
||||
@endfor
|
||||
</optgroup>
|
||||
<optgroup label="SCSI (Max: 31)">
|
||||
@for($i = 0; $i <= 30; $i++)
|
||||
@php
|
||||
$key = "scsi{$i}";
|
||||
$isUsed = isset($drives[$key]);
|
||||
@endphp
|
||||
<option value="{{ $key }}" @if($isUsed) disabled @endif>
|
||||
scsi{{ $i }} @if($isUsed) (In use) @endif
|
||||
</option>
|
||||
@endfor
|
||||
</optgroup>
|
||||
<optgroup label="VirtIO (Max: 16)">
|
||||
@for($i = 0; $i <= 15; $i++)
|
||||
@php
|
||||
$key = "virtio{$i}";
|
||||
$isUsed = isset($drives[$key]);
|
||||
@endphp
|
||||
<option value="{{ $key }}" @if($isUsed) disabled @endif>
|
||||
virtio{{ $i }} @if($isUsed) (In use) @endif
|
||||
</option>
|
||||
@endfor
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2 p-3 rounded-lg bg-info/10 border border-info/30 text-info text-sm">
|
||||
<x-ri-information-line class="size-4 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p>This will reattach the drive to the selected bus position.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($status && $status->status !== 'stopped')
|
||||
<div class="flex items-center gap-2 p-3 rounded-lg bg-danger/10 border border-danger/30 text-danger text-sm">
|
||||
<x-ri-error-warning-line class="size-4 flex-shrink-0" />
|
||||
<span>VM must be stopped to reattach drives.</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 p-5 border-t border-neutral">
|
||||
<button wire:click="closeReattachModal"
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button wire:click="reattachDrive"
|
||||
wire:loading.attr="disabled"
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
||||
@if(!$reattachTargetBus || ($status && $status->status !== 'stopped')) disabled @endif>
|
||||
<x-loading target="reattachDrive" />
|
||||
<span wire:loading.remove wire:target="reattachDrive">Reattach Drive</span>
|
||||
<span wire:loading wire:target="reattachDrive">Reattaching...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-proxmox::layout>
|
||||
|
||||
Reference in New Issue
Block a user