server = $server; if ($server->user_id != auth()->id()) { abort(403); } $this->loadVmStatus(); } /** * Perform a VM lifecycle action (start/stop/etc). */ public function do(string $action): void { if (!in_array($action, ['start', 'shutdown', 'reboot', 'stop', 'reset'], true)) { $this->notify('Invalid action', 'error'); 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( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/status/' . $action, 'post', ['vmid' => $this->server->vm_id] ); if ($response->successful()) { $this->notify('Action performed successfully', 'success'); if (property_exists($this, 'fastPolling')) { $this->fastPolling = true; } } else { $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; $this->notify('Action failed: ' . $message, 'error'); } } catch (\Throwable $e) { $this->notify('Action failed: ' . $e->getMessage(), 'error'); } finally { $this->loadVmStatus(); } } /** * 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. */ public function loadVmStatus(): void { try { $proxmox = new Proxmox; $data = $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/status/current' )->json()['data'] ?? null; $this->status = $data ? (object) $data : null; } catch (\Throwable $e) { $this->status = null; } } }