feat: implement storage quota checks and VM state validation for drive management

This commit is contained in:
2025-12-05 06:47:33 -05:00
parent 2a07fe78fc
commit 61b74aa0a1
2 changed files with 265 additions and 36 deletions

View File

@@ -2,6 +2,7 @@
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
use App\Helpers\ExtensionHelper;
use App\Livewire\Component as BaseComponent;
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
@@ -47,6 +48,15 @@ class Component extends BaseComponent
return;
}
// Check storage quota before starting the VM
if ($action === 'start') {
$quotaCheck = $this->checkStorageQuota();
if ($quotaCheck !== true) {
$this->notify($quotaCheck, 'error');
return;
}
}
try {
$proxmox = new Proxmox;
$response = $proxmox->request(
@@ -71,6 +81,149 @@ class Component extends BaseComponent
}
}
/**
* Check if storage quota allows VM to start.
* Returns true if OK, or an error message string if over quota.
*/
protected function checkStorageQuota(): bool|string
{
try {
// Get max allowed storage from package
$settings = array_merge(
ExtensionHelper::settingsToArray($this->server->service->product->settings),
ExtensionHelper::getServiceProperties($this->server->service)
);
$maxAllowedStorage = isset($settings['disk']) ? (float) $settings['disk'] : 0;
// If no limit is set, allow start
if ($maxAllowedStorage <= 0) {
return true;
}
// Get VM config to calculate total storage
$proxmox = new Proxmox;
$configResponse = $proxmox->request(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config'
);
if (!$configResponse->successful()) {
// If we can't check, allow the action (fail open)
return true;
}
$config = $configResponse->json()['data'] ?? [];
$totalStorage = $this->calculateTotalStorage($config);
if ($totalStorage > $maxAllowedStorage) {
return 'Cannot start VM: Storage quota exceeded. Your server has ' . number_format($totalStorage, 1) . ' GB of storage configured, but your plan only allows ' . number_format($maxAllowedStorage, 1) . ' GB. Please configure your server storage within your package limits or upgrade your service to allow this configuration.';
}
return true;
} catch (\Throwable $e) {
// If check fails, allow the action (fail open)
return true;
}
}
/**
* Calculate total storage from VM config (including unused/detached drives).
*/
protected function calculateTotalStorage(array $config): float
{
$totalSize = 0;
foreach ($config as $key => $value) {
// Check for attached drives (ide, sata, scsi, virtio)
if (preg_match('/^(ide|sata|scsi|virtio)(\d+)$/', $key)) {
// Skip excluded disks (cloud-init, EFI, TPM)
if ($this->isExcludedDisk($value)) {
continue;
}
// Skip CD-ROM drives
if (str_contains($value, 'media=cdrom')) {
continue;
}
$totalSize += $this->extractDiskSize($value);
}
// Check for unused/detached drives
elseif (preg_match('/^unused(\d+)$/', $key)) {
if ($this->isExcludedDisk($value)) {
continue;
}
$totalSize += $this->extractUnusedDiskSize($value);
}
}
return $totalSize;
}
/**
* Check if a disk should be excluded (cloud-init, EFI, TPM).
*/
protected function isExcludedDisk(string $value): bool
{
$excludePatterns = ['cloudinit', 'efidisk', 'tpmstate'];
foreach ($excludePatterns as $pattern) {
if (str_contains(strtolower($value), $pattern)) {
return true;
}
}
return false;
}
/**
* Extract disk size in GB from a disk configuration string.
*/
protected function extractDiskSize(string $value): float
{
// Example: cephpool:vm-100-disk-0,cache=writeback,size=50G
if (preg_match('/size=(\d+)([GMTK])?/i', $value, $matches)) {
$size = (float) $matches[1];
$unit = strtoupper($matches[2] ?? 'G');
return match ($unit) {
'T' => $size * 1024,
'G' => $size,
'M' => $size / 1024,
'K' => $size / 1024 / 1024,
default => $size,
};
}
return 0;
}
/**
* Extract size from unused disk by querying Proxmox.
*/
protected function extractUnusedDiskSize(string $value): float
{
// For unused disks, we need to query the volume info
// Format: storage:volume (e.g., cephpool:vm-100-disk-1)
try {
if (!preg_match('/^([^:]+):(.+)$/', $value, $matches)) {
return 0;
}
$storage = $matches[1];
$volume = $matches[2];
$proxmox = new Proxmox;
$response = $proxmox->request(
'/nodes/' . $this->server->node->name . '/storage/' . $storage . '/content/' . $storage . ':' . $volume
);
if ($response->successful()) {
$data = $response->json()['data'] ?? [];
$sizeBytes = $data['size'] ?? 0;
return $sizeBytes / 1024 / 1024 / 1024; // Convert to GB
}
} catch (\Throwable $e) {
// Ignore errors, return 0
}
return 0;
}
/**
* Refresh the cached VM status from Proxmox.
*/