vmid) { throw new ProvisioningException('VM ist noch nicht in Proxmox provisioniert.', step: 'vm_not_ready'); } if ($vm->status !== 'active') { throw new ProvisioningException('VM ist nicht aktiv (Status: '.$vm->status.').', step: 'vm_not_active'); } } public function refreshLiveStatus(Customer $vm): array { $this->assertManageable($vm); $raw = $this->proxmox->getVMStatus((int) $vm->vmid); $normalized = $this->proxmox->normalizeLiveStatus($raw); $vm->update([ 'proxmox_status' => $normalized['status'], 'proxmox_uptime' => $normalized['uptime'], 'proxmox_status_at' => now(), ]); return $normalized; } public function power(Customer $vm, VmPowerAction $action, User $user): void { $this->assertManageable($vm); $live = $this->refreshLiveStatus($vm); $isRunning = ($live['status'] ?? '') === 'running'; if ($action->requiresRunning() && ! $isRunning) { throw new ProxmoxException('VM läuft nicht – Aktion nicht möglich.', step: 'power_state'); } if ($action->requiresStopped() && $isRunning) { throw new ProxmoxException('VM läuft bereits.', step: 'power_state'); } try { match ($action) { VmPowerAction::Start => $this->proxmox->startVM((int) $vm->vmid), VmPowerAction::Shutdown => $this->proxmox->shutdownVM((int) $vm->vmid), VmPowerAction::Stop => $this->proxmox->stopVM((int) $vm->vmid), VmPowerAction::Reboot => $this->proxmox->rebootVM((int) $vm->vmid), VmPowerAction::Reset => $this->proxmox->resetVM((int) $vm->vmid), }; $this->log($vm, $user, 'power.'.$action->value, 'success'); $this->refreshLiveStatus($vm); } catch (\Throwable $e) { $this->log($vm, $user, 'power.'.$action->value, 'failed', $e->getMessage()); throw $e; } } public function mountIso(Customer $vm, string $isoVolid, User $user): void { $this->assertManageable($vm); $available = collect($this->proxmox->listIsos())->pluck('volid'); if (! $available->contains($isoVolid)) { throw new ProxmoxException('ISO nicht im Storage gefunden.', step: 'iso_invalid'); } try { $this->proxmox->mountIso((int) $vm->vmid, $isoVolid); $vm->update(['attached_iso' => $isoVolid]); $this->log($vm, $user, 'iso.mount', 'success', null, ['iso' => $isoVolid]); } catch (\Throwable $e) { $this->log($vm, $user, 'iso.mount', 'failed', $e->getMessage()); throw $e; } } public function unmountIso(Customer $vm, User $user): void { $this->assertManageable($vm); try { $this->proxmox->unmountIso((int) $vm->vmid); $vm->update(['attached_iso' => null]); $this->log($vm, $user, 'iso.unmount', 'success'); } catch (\Throwable $e) { $this->log($vm, $user, 'iso.unmount', 'failed', $e->getMessage()); throw $e; } } /** * @return array{token: string, ws_url: string, vmid: int, node: string, expires_at: int} */ public function createConsoleSession(Customer $vm, User $user): array { $this->assertManageable($vm); $live = $this->refreshLiveStatus($vm); if (($live['status'] ?? '') !== 'running') { throw new ProxmoxException('Konsole nur bei laufender VM verfügbar. Bitte VM starten.', step: 'console_not_running'); } $proxy = $this->proxmox->createVncProxy((int) $vm->vmid); if ($proxy['port'] === '' || $proxy['ticket'] === '') { throw new ProxmoxException('VNC-Proxy konnte nicht erstellt werden.', step: 'console_proxy'); } $token = Str::random(64); $proxmoxWsUrl = $this->proxmox->buildVncWebSocketUrl((int) $vm->vmid, $proxy['port'], $proxy['ticket']); $expiresAt = now()->addMinutes(5)->timestamp; $wsUrl = $proxmoxWsUrl; if (config('hosting.console.proxy_enabled') && config('hosting.console.proxy_ws_url')) { $wsUrl = rtrim(config('hosting.console.proxy_ws_url'), '/').'/'.$token; } Cache::put($this->consoleCacheKey($token), [ 'customer_id' => $vm->id, 'user_id' => $user->id, 'ws_url' => $proxmoxWsUrl, 'ticket' => $proxy['ticket'], 'port' => $proxy['port'], 'vmid' => $vm->vmid, 'node' => $proxy['node'], 'cert' => $proxy['cert'], ], 300); $this->log($vm, $user, 'console.open', 'success'); return [ 'token' => $token, 'ws_url' => $wsUrl, 'vmid' => (int) $vm->vmid, 'node' => $proxy['node'], 'expires_at' => $expiresAt, ]; } public function getConsoleSession(string $token, User $user): array { $data = Cache::get($this->consoleCacheKey($token)); if (! $data) { throw new ProxmoxException('Konsole-Session abgelaufen oder ungültig.', step: 'console_expired'); } if ($data['user_id'] !== $user->id && ! $user->isAdmin()) { throw new ProxmoxException('Kein Zugriff auf diese Konsole.', step: 'console_forbidden'); } return $data; } public function consoleCacheKey(string $token): string { return 'vm_console:'.$token; } private function log(Customer $vm, User $user, string $action, string $status, ?string $message = null, array $meta = []): void { VmActivityLog::query()->create([ 'customer_id' => $vm->id, 'user_id' => $user->id, 'action' => $action, 'status' => $status, 'message' => $message, 'meta' => $meta ?: null, ]); } }