193 lines
6.4 KiB
PHP
193 lines
6.4 KiB
PHP
<?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,
|
||
]);
|
||
}
|
||
}
|