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,215 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreVmRequest;
use App\Http\Requests\UpdateVmRequest;
use App\Jobs\ProvisionCustomerJob;
use App\Models\Customer;
use App\Models\IpPool;
use App\Models\User;
use App\Models\VmDevice;
use App\Services\Hosting\Proxmox\ProxmoxClient;
use App\Services\Hosting\Provisioning\DeprovisionService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;
class VmController extends Controller
{
public function index(Request $request): View
{
$this->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,
]);
}
}
}