initial commit
This commit is contained in:
17
app/Http/Controllers/Web/Admin/SystemHealthController.php
Normal file
17
app/Http/Controllers/Web/Admin/SystemHealthController.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Hosting\Health\SystemHealthService;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SystemHealthController extends Controller
|
||||
{
|
||||
public function __invoke(SystemHealthService $health): View
|
||||
{
|
||||
return view('admin.health', [
|
||||
'checks' => $health->checks(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
72
app/Http/Controllers/Web/Admin/VmTemplateController.php
Normal file
72
app/Http/Controllers/Web/Admin/VmTemplateController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\VmTemplate;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class VmTemplateController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$templates = VmTemplate::query()->orderBy('name')->get();
|
||||
|
||||
return view('admin.templates.index', compact('templates'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.templates.create');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'slug' => ['required', 'string', 'max:64', 'alpha_dash', 'unique:vm_templates,slug'],
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'proxmox_template_vmid' => ['required', 'integer', 'min:100'],
|
||||
'os_family' => ['nullable', 'string', 'max:32'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
VmTemplate::query()->create([
|
||||
...$data,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.templates.index')->with('success', 'Template angelegt.');
|
||||
}
|
||||
|
||||
public function edit(VmTemplate $template): View
|
||||
{
|
||||
return view('admin.templates.edit', compact('template'));
|
||||
}
|
||||
|
||||
public function update(Request $request, VmTemplate $template): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'slug' => ['required', 'string', 'max:64', 'alpha_dash', 'unique:vm_templates,slug,'.$template->id],
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'proxmox_template_vmid' => ['required', 'integer', 'min:100'],
|
||||
'os_family' => ['nullable', 'string', 'max:32'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$template->update([
|
||||
...$data,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.templates.index')->with('success', 'Template aktualisiert.');
|
||||
}
|
||||
|
||||
public function destroy(VmTemplate $template): RedirectResponse
|
||||
{
|
||||
$template->delete();
|
||||
|
||||
return redirect()->route('admin.templates.index')->with('success', 'Template gelöscht.');
|
||||
}
|
||||
}
|
||||
45
app/Http/Controllers/Web/DashboardController.php
Normal file
45
app/Http/Controllers/Web/DashboardController.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Models\IpPool;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$vmQuery = Customer::query()->forUser($user);
|
||||
|
||||
$stats = [
|
||||
'vms_total' => (clone $vmQuery)->count(),
|
||||
'vms_active' => (clone $vmQuery)->where('status', 'active')->count(),
|
||||
'vms_pending' => (clone $vmQuery)->where('status', 'pending')->count(),
|
||||
'vms_failed' => (clone $vmQuery)->where('status', 'failed')->count(),
|
||||
];
|
||||
|
||||
$pools = IpPool::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('type')
|
||||
->get();
|
||||
|
||||
$recentVms = Customer::query()
|
||||
->forUser($user)
|
||||
->with(['owner', 'ipPool'])
|
||||
->latest()
|
||||
->limit(8)
|
||||
->get();
|
||||
|
||||
return view('dashboard', [
|
||||
'stats' => $stats,
|
||||
'pools' => $pools,
|
||||
'recentVms' => $recentVms,
|
||||
'usersCount' => $user->isAdmin() ? User::query()->count() : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
123
app/Http/Controllers/Web/IpPoolController.php
Normal file
123
app/Http/Controllers/Web/IpPoolController.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Enums\IpPoolType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Models\IpPool;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class IpPoolController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorize('viewAny', IpPool::class);
|
||||
|
||||
$pools = IpPool::query()
|
||||
->withCount(['vms' => fn ($q) => $q->whereIn('status', ['pending', 'active'])])
|
||||
->orderBy('type')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(function (IpPool $pool) {
|
||||
$pool->setAttribute('usage_percent', $pool->totalIps() > 0
|
||||
? round(($pool->usedIpsCount() / $pool->totalIps()) * 100, 1)
|
||||
: 0);
|
||||
|
||||
return $pool;
|
||||
});
|
||||
|
||||
$assignments = Customer::query()
|
||||
->forUser($request->user())
|
||||
->with(['owner', 'ipPool'])
|
||||
->whereIn('status', ['pending', 'active'])
|
||||
->where(fn ($q) => $q->whereNotNull('ip_address')->orWhereNotNull('public_ip'))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('ip-pools.index', compact('pools', 'assignments'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$this->authorize('create', IpPool::class);
|
||||
|
||||
return view('ip-pools.create', ['types' => IpPoolType::cases()]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', IpPool::class);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'type' => ['required', Rule::enum(IpPoolType::class)],
|
||||
'start_ip' => ['required', 'ip'],
|
||||
'end_ip' => ['required', 'ip'],
|
||||
'gateway' => ['nullable', 'ip'],
|
||||
'cidr' => ['required', 'integer', 'min:8', 'max:32'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
if (ip2long($data['start_ip']) > ip2long($data['end_ip'])) {
|
||||
return back()->withErrors(['end_ip' => 'End-IP muss größer als Start-IP sein.'])->withInput();
|
||||
}
|
||||
|
||||
IpPool::query()->create([
|
||||
...$data,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
return redirect()->route('ip-pools.index')->with('success', 'IP-Pool erstellt.');
|
||||
}
|
||||
|
||||
public function edit(IpPool $ipPool): View
|
||||
{
|
||||
$this->authorize('update', $ipPool);
|
||||
|
||||
return view('ip-pools.edit', [
|
||||
'pool' => $ipPool,
|
||||
'types' => IpPoolType::cases(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, IpPool $ipPool): RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $ipPool);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'type' => ['required', Rule::enum(IpPoolType::class)],
|
||||
'start_ip' => ['required', 'ip'],
|
||||
'end_ip' => ['required', 'ip'],
|
||||
'gateway' => ['nullable', 'ip'],
|
||||
'cidr' => ['required', 'integer', 'min:8', 'max:32'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$ipPool->update([
|
||||
...$data,
|
||||
'is_active' => $request->boolean('is_active'),
|
||||
]);
|
||||
|
||||
return redirect()->route('ip-pools.index')->with('success', 'IP-Pool aktualisiert.');
|
||||
}
|
||||
|
||||
public function destroy(IpPool $ipPool): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $ipPool);
|
||||
|
||||
if ($ipPool->vms()->whereIn('status', ['pending', 'active'])->exists()) {
|
||||
return back()->withErrors(['pool' => 'Pool wird noch von aktiven VMs verwendet.']);
|
||||
}
|
||||
|
||||
$ipPool->delete();
|
||||
|
||||
return redirect()->route('ip-pools.index')->with('success', 'IP-Pool gelöscht.');
|
||||
}
|
||||
}
|
||||
38
app/Http/Controllers/Web/IsoUploadController.php
Normal file
38
app/Http/Controllers/Web/IsoUploadController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CustomerIsoUpload;
|
||||
use App\Services\Hosting\Iso\IsoUploadService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class IsoUploadController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$uploads = CustomerIsoUpload::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return view('iso-uploads.index', compact('uploads'));
|
||||
}
|
||||
|
||||
public function store(Request $request, IsoUploadService $service): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'iso' => ['required', 'file', 'mimes:iso', 'max:'.((int) config('hosting.iso_upload.max_size_mb', 10240) * 1024)],
|
||||
]);
|
||||
|
||||
try {
|
||||
$service->upload($request->user(), $request->file('iso'));
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['iso' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return redirect()->route('iso-uploads.index')->with('success', 'ISO hochgeladen.');
|
||||
}
|
||||
}
|
||||
106
app/Http/Controllers/Web/UserController.php
Normal file
106
app/Http/Controllers/Web/UserController.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$this->authorize('viewAny', User::class);
|
||||
|
||||
$users = User::query()->withCount('vms')->orderBy('name')->paginate(20);
|
||||
|
||||
return view('users.index', compact('users'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
return view('users.create', ['roles' => UserRole::cases()]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
'role' => ['required', Rule::enum(UserRole::class)],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
User::query()->create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'role' => $data['role'],
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
return redirect()->route('users.index')->with('success', 'Benutzer erstellt.');
|
||||
}
|
||||
|
||||
public function edit(User $user): View
|
||||
{
|
||||
$this->authorize('update', $user);
|
||||
|
||||
return view('users.edit', [
|
||||
'user' => $user,
|
||||
'roles' => UserRole::cases(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'password' => ['nullable', 'confirmed', Password::defaults()],
|
||||
'role' => ['required', Rule::enum(UserRole::class)],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$user->fill([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'role' => $data['role'],
|
||||
'is_active' => $request->boolean('is_active'),
|
||||
]);
|
||||
|
||||
if (! empty($data['password'])) {
|
||||
$user->password = Hash::make($data['password']);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return redirect()->route('users.index')->with('success', 'Benutzer aktualisiert.');
|
||||
}
|
||||
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $user);
|
||||
|
||||
if ($user->vms()->whereIn('status', ['pending', 'active'])->exists()) {
|
||||
return back()->withErrors(['user' => 'Benutzer hat noch aktive VMs.']);
|
||||
}
|
||||
|
||||
$user->delete();
|
||||
|
||||
return redirect()->route('users.index')->with('success', 'Benutzer gelöscht.');
|
||||
}
|
||||
}
|
||||
24
app/Http/Controllers/Web/VmBackupController.php
Normal file
24
app/Http/Controllers/Web/VmBackupController.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Hosting\Backups\BackupService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VmBackupController extends Controller
|
||||
{
|
||||
public function store(Customer $vm, BackupService $backups): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
|
||||
try {
|
||||
$backups->start($vm, auth()->user());
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['backup' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Backup gestartet.');
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Web/VmConsoleController.php
Normal file
47
app/Http/Controllers/Web/VmConsoleController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Hosting\Proxmox\VmManagementService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class VmConsoleController extends Controller
|
||||
{
|
||||
public function create(Request $request, Customer $vm, VmManagementService $management): View
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
|
||||
try {
|
||||
$session = $management->createConsoleSession($vm, $request->user());
|
||||
} catch (\Throwable $e) {
|
||||
return view('vms.console-error', [
|
||||
'vm' => $vm,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return view('vms.console', [
|
||||
'vm' => $vm,
|
||||
'token' => $session['token'],
|
||||
'wsUrl' => $session['ws_url'],
|
||||
'expiresAt' => $session['expires_at'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $token, VmManagementService $management): View
|
||||
{
|
||||
$session = $management->getConsoleSession($token, $request->user());
|
||||
$vm = Customer::query()->findOrFail($session['customer_id']);
|
||||
$this->authorize('view', $vm);
|
||||
|
||||
return view('vms.console', [
|
||||
'vm' => $vm,
|
||||
'token' => $token,
|
||||
'wsUrl' => $session['ws_url'],
|
||||
'expiresAt' => now()->addMinutes(5)->timestamp,
|
||||
]);
|
||||
}
|
||||
}
|
||||
215
app/Http/Controllers/Web/VmController.php
Normal file
215
app/Http/Controllers/Web/VmController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
app/Http/Controllers/Web/VmFirewallController.php
Normal file
53
app/Http/Controllers/Web/VmFirewallController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Hosting\Firewall\FirewallService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class VmFirewallController extends Controller
|
||||
{
|
||||
public function store(Request $request, Customer $vm, FirewallService $firewall): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
|
||||
$data = $request->validate([
|
||||
'direction' => ['required', Rule::in(['in', 'out'])],
|
||||
'action' => ['required', Rule::in(['ACCEPT', 'DROP', 'REJECT'])],
|
||||
'protocol' => ['required', Rule::in(['tcp', 'udp', 'icmp'])],
|
||||
'port' => ['nullable', 'string', 'max:32'],
|
||||
'source' => ['nullable', 'string', 'max:64'],
|
||||
]);
|
||||
|
||||
$vm->firewallRules()->create([
|
||||
...$data,
|
||||
'sort_order' => $vm->firewallRules()->count(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$firewall->syncToProxmox($vm);
|
||||
} catch (\Throwable $e) {
|
||||
return back()->with('warning', 'Regel gespeichert, Proxmox-Sync fehlgeschlagen: '.$e->getMessage());
|
||||
}
|
||||
|
||||
return back()->with('success', 'Firewall-Regel hinzugefügt.');
|
||||
}
|
||||
|
||||
public function destroy(Customer $vm, int $rule, FirewallService $firewall): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
$vm->firewallRules()->whereKey($rule)->delete();
|
||||
|
||||
try {
|
||||
$firewall->syncToProxmox($vm);
|
||||
} catch (\Throwable) {
|
||||
//
|
||||
}
|
||||
|
||||
return back()->with('success', 'Regel entfernt.');
|
||||
}
|
||||
}
|
||||
55
app/Http/Controllers/Web/VmIsoController.php
Normal file
55
app/Http/Controllers/Web/VmIsoController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Hosting\Proxmox\ProxmoxClient;
|
||||
use App\Services\Hosting\Proxmox\VmManagementService;
|
||||
use App\Services\Hosting\Snapshots\SnapshotService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VmIsoController extends Controller
|
||||
{
|
||||
public function store(Request $request, Customer $vm, VmManagementService $management, SnapshotService $snapshots): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
|
||||
$data = $request->validate([
|
||||
'iso_volid' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$snapshots->autoBeforeDestructive($vm, $request->user(), 'iso-mount');
|
||||
|
||||
try {
|
||||
$management->mountIso($vm, $data['iso_volid'], $request->user());
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['iso' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'ISO wurde eingebunden. VM ggf. neu starten, um vom Installationsmedium zu booten.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Customer $vm, VmManagementService $management): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
|
||||
try {
|
||||
$management->unmountIso($vm, $request->user());
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['iso' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'ISO wurde entfernt.');
|
||||
}
|
||||
|
||||
public function index(ProxmoxClient $proxmox): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
try {
|
||||
return response()->json(['isos' => $proxmox->listIsos()]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 502);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/Http/Controllers/Web/VmPowerController.php
Normal file
36
app/Http/Controllers/Web/VmPowerController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Enums\VmPowerAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Hosting\Proxmox\VmManagementService;
|
||||
use App\Services\Hosting\Snapshots\SnapshotService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class VmPowerController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, Customer $vm, VmManagementService $management, SnapshotService $snapshots): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
|
||||
$action = VmPowerAction::from($request->validate([
|
||||
'action' => ['required', Rule::enum(VmPowerAction::class)],
|
||||
])['action']);
|
||||
|
||||
if (in_array($action, [VmPowerAction::Stop, VmPowerAction::Reset, VmPowerAction::Reboot], true)) {
|
||||
$snapshots->autoBeforeDestructive($vm, $request->user(), 'power-'.$action->value);
|
||||
}
|
||||
|
||||
try {
|
||||
$management->power($vm, $action, $request->user());
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['power' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('success', $action->label().' wurde ausgeführt.');
|
||||
}
|
||||
}
|
||||
26
app/Http/Controllers/Web/VmReinstallController.php
Normal file
26
app/Http/Controllers/Web/VmReinstallController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Hosting\Reinstall\ReinstallService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VmReinstallController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, Customer $vm, ReinstallService $reinstall): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
$request->validate(['confirm' => ['required', 'in:REINSTALL']]);
|
||||
|
||||
try {
|
||||
$reinstall->reinstall($vm, $request->user());
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['reinstall' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return redirect()->route('vms.show', $vm)->with('success', 'Neuinstallation gestartet.');
|
||||
}
|
||||
}
|
||||
55
app/Http/Controllers/Web/VmSnapshotController.php
Normal file
55
app/Http/Controllers/Web/VmSnapshotController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Models\VmSnapshot;
|
||||
use App\Services\Hosting\Snapshots\SnapshotService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VmSnapshotController extends Controller
|
||||
{
|
||||
public function store(Request $request, Customer $vm, SnapshotService $snapshots): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
$request->validate(['label' => ['nullable', 'string', 'max:64']]);
|
||||
|
||||
try {
|
||||
$snapshots->create($vm, $request->user(), false, $request->input('label'));
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['snapshot' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Snapshot erstellt.');
|
||||
}
|
||||
|
||||
public function rollback(Customer $vm, VmSnapshot $snapshot, SnapshotService $snapshots): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
abort_unless($snapshot->customer_id === $vm->id, 404);
|
||||
|
||||
try {
|
||||
$snapshots->rollback($vm, $snapshot);
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['snapshot' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Snapshot wiederhergestellt.');
|
||||
}
|
||||
|
||||
public function destroy(Customer $vm, VmSnapshot $snapshot, SnapshotService $snapshots): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $vm);
|
||||
abort_unless($snapshot->customer_id === $vm->id, 404);
|
||||
|
||||
try {
|
||||
$snapshots->delete($vm, $snapshot);
|
||||
} catch (\Throwable $e) {
|
||||
return back()->withErrors(['snapshot' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Snapshot gelöscht.');
|
||||
}
|
||||
}
|
||||
42
app/Http/Controllers/Web/VmStatusController.php
Normal file
42
app/Http/Controllers/Web/VmStatusController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Hosting\Proxmox\VmManagementService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VmStatusController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, Customer $vm, VmManagementService $management): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $vm);
|
||||
|
||||
if (! $vm->vmid || $vm->status !== 'active') {
|
||||
return response()->json([
|
||||
'status' => $vm->status,
|
||||
'proxmox' => null,
|
||||
'message' => 'VM nicht bereit',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$live = $management->refreshLiveStatus($vm);
|
||||
|
||||
return response()->json([
|
||||
'status' => $vm->status,
|
||||
'proxmox' => $live,
|
||||
'attached_iso' => $vm->attached_iso,
|
||||
'refreshed_at' => $vm->proxmox_status_at?->toIso8601String(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'status' => $vm->status,
|
||||
'proxmox' => null,
|
||||
'error' => $e->getMessage(),
|
||||
], 502);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user