initial commit

This commit is contained in:
TheOnlyMace
2026-05-17 13:26:14 +02:00
commit 75299b723d
176 changed files with 20327 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
<?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,
]);
}
}