authorize('viewAny', Customer::class); $vms = Customer::query() ->forUser($request->user()) ->with(['owner', 'ipPool']) ->when($request->query('status'), fn ($q, $status) => $q->where('status', $status)) ->latest() ->paginate(15) ->withQueryString(); return view('vms.index', compact('vms')); } public function create(Request $request): View { $this->authorize('create', Customer::class); $isos = []; try { $isos = app(ProxmoxClient::class)->listIsos(); } catch (\Throwable) { // Proxmox nicht erreichbar – Formular bleibt nutzbar } return view('vms.create', [ 'privatePools' => IpPool::query()->where('type', 'private')->where('is_active', true)->get(), 'customers' => $request->user()->isAdmin() ? User::query()->where('role', 'customer')->orderBy('name')->get() : collect(), 'deviceTypes' => VmDevice::typesFor($request->user()), 'templates' => \App\Models\VmTemplate::query()->where('is_active', true)->orderBy('name')->get(), 'isos' => $isos, ]); } public function store(StoreVmRequest $request): RedirectResponse { $domain = $request->domain(); if ($domain && Customer::query()->where('domain', $domain)->exists()) { return back()->withErrors(['subdomain' => 'Diese Subdomain ist bereits vergeben.'])->withInput(); } if (! $domain) { $domain = 'direct-'.Str::slug($request->validated('name')).'-'.Str::lower(Str::random(6)).'.internal'; } $vm = Customer::query()->create([ 'user_id' => $request->ownerId(), 'name' => $request->validated('name'), 'domain' => $domain, 'behind_traefik' => $request->boolean('behind_traefik'), 'ip_pool_id' => $request->input('ip_pool_id'), 'cpu' => $request->integer('cpu'), 'ram' => $request->integer('ram'), 'disk' => $request->integer('disk'), 'attached_iso' => $request->validated('install_iso'), 'status' => 'pending', 'provisioning_step' => 'queued', ]); $this->syncDevices($vm, $request->input('devices', [])); ProvisionCustomerJob::dispatch($vm->id); return redirect() ->route('vms.show', $vm) ->with('success', 'VM-Provisioning wurde gestartet.'); } public function show(Request $request, Customer $vm): View { $this->authorize('view', $vm); $vm->load([ 'owner', 'ipPool', 'devices', 'snapshots' => fn ($q) => $q->latest(), 'backups' => fn ($q) => $q->latest()->limit(10), 'firewallRules', 'metrics' => fn ($q) => $q->limit(48), 'activityLogs' => fn ($q) => $q->limit(15)->with('user'), ]); $isos = []; $liveStatus = null; if ($vm->vmid && $vm->status === 'active') { try { $proxmox = app(ProxmoxClient::class); $isos = $proxmox->listIsos(); } catch (\Throwable) { // } try { $proxmox ??= app(ProxmoxClient::class); $liveStatus = $proxmox->normalizeLiveStatus($proxmox->getVMStatus((int) $vm->vmid)); $vm->update([ 'proxmox_status' => $liveStatus['status'], 'proxmox_uptime' => $liveStatus['uptime'], 'proxmox_status_at' => now(), ]); } catch (\Throwable) { // } } return view('vms.show', compact('vm', 'isos', 'liveStatus')); } public function edit(Request $request, Customer $vm): View { $this->authorize('update', $vm); $vm->load('devices'); return view('vms.edit', [ 'vm' => $vm, 'deviceTypes' => VmDevice::typesFor($request->user()), 'templates' => \App\Models\VmTemplate::query()->where('is_active', true)->orderBy('name')->get(), ]); } public function update(UpdateVmRequest $request, Customer $vm): RedirectResponse { $vm->update($request->only(['name', 'cpu', 'ram', 'disk'])); if ($request->has('devices')) { $vm->devices()->delete(); $this->syncDevices($vm, $request->input('devices', [])); } if ($vm->vmid && $vm->status === 'active') { try { app(\App\Services\Hosting\Proxmox\ProxmoxClient::class) ->updateVmResources((int) $vm->vmid, $vm->cpu, $vm->ram, $vm->disk); app(\App\Services\Hosting\Proxmox\ProxmoxClient::class) ->applyDevices((int) $vm->vmid, $vm->devices()->get()); } catch (\Throwable $e) { return back()->with('warning', 'DB gespeichert, Proxmox-Update fehlgeschlagen: '.$e->getMessage()); } } return redirect()->route('vms.show', $vm)->with('success', 'VM-Konfiguration gespeichert.'); } public function destroy(Request $request, Customer $vm, DeprovisionService $deprovision, \App\Services\Hosting\Snapshots\SnapshotService $snapshots): RedirectResponse { $this->authorize('delete', $vm); if ($vm->vmid && $vm->status === 'active') { try { $snapshots->autoBeforeDestructive($vm, $request->user(), 'delete'); } catch (\Throwable) { // } } if ($vm->status === 'pending' && $vm->provisioning_step === 'queued') { if ($vm->vmid) { app(\App\Services\Hosting\Provisioning\VmidReservationService::class) ->scheduleRelease((int) $vm->vmid, $vm); } $vm->devices()->delete(); $vm->delete(); return redirect()->route('vms.index')->with('success', 'VM-Eintrag gelöscht.'); } $deprovision->removeVmOnly($vm, $request->user()); return redirect()->route('vms.index')->with('success', 'VM wurde entfernt. VMID wird nach 48h freigegeben.'); } private function syncDevices(Customer $vm, array $devices): void { $allowed = array_keys(VmDevice::typesFor(auth()->user())); foreach ($devices as $index => $device) { if (empty($device['type']) || ! in_array($device['type'], $allowed, true)) { continue; } $vm->devices()->create([ 'type' => $device['type'], 'slot' => $device['slot'] ?? null, 'config' => $device['config'] ?? [], 'sort_order' => $index, ]); } } }