Files
HexaHost-Panel/app/Services/Hosting/Proxmox/VmManagementService.php
2026-05-17 13:26:14 +02:00

193 lines
6.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\Hosting\Proxmox;
use App\Enums\VmPowerAction;
use App\Exceptions\Hosting\ProxmoxException;
use App\Exceptions\Hosting\ProvisioningException;
use App\Models\Customer;
use App\Models\User;
use App\Models\VmActivityLog;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class VmManagementService
{
public function __construct(
private readonly ProxmoxClient $proxmox,
) {}
public function assertManageable(Customer $vm): void
{
if (! $vm->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,
]);
}
}