feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js

- Added main application entry point (app.jsx) and bootstrap file for React setup.
- Created Wiki index page (Index.jsx) for documentation and file management.
- Implemented utility functions for formatting and status handling in utils.js.
- Set up Blade template (app.blade.php) for Inertia.js integration.
- Defined API routes for metrics and terminal access in api.php.
- Established console commands for scheduled tasks in console.php.
- Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php.
- Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments.
- Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js.
- Set up Vite for asset bundling and development server in vite.config.js.
This commit is contained in:
I-Fun
2026-04-07 20:29:32 +07:00
commit c649f055d8
126 changed files with 8122 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Acl;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RoleController extends Controller
{
public function index()
{
return Inertia::render('Acl/Index', [
'users' => \App\Models\User::with('roles')->paginate(30),
'roles' => Role::withCount('permissions', 'users')->get(),
]);
}
public function create()
{
return Inertia::render('Acl/RoleCreate', ['permissions' => Permission::all()->groupBy(fn($p) => explode('.', $p->name)[0])]);
}
public function store(Request $request)
{
$validated = $request->validate(['name' => 'required|string|unique:roles', 'permissions' => 'array']);
$role = Role::create(['name' => $validated['name']]);
if ($validated['permissions'] ?? false) {
$role->syncPermissions($validated['permissions']);
}
return redirect()->route('acl.index')->with('success', 'Role created.');
}
public function edit(Role $role)
{
return Inertia::render('Acl/RoleEdit', [
'role' => $role->load('permissions'),
'permissions' => Permission::all()->groupBy(fn($p) => explode('.', $p->name)[0]),
]);
}
public function update(Request $request, Role $role)
{
$role->syncPermissions($request->input('permissions', []));
return back()->with('success', 'Role updated.');
}
public function destroy(Role $role)
{
if (in_array($role->name, ['super-admin', 'admin'])) {
return back()->with('error', 'Cannot delete system roles.');
}
$role->delete();
return back()->with('success', 'Role deleted.');
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Acl;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Models\Role;
class UserController extends Controller
{
public function index()
{
return redirect()->route('acl.index');
}
public function create()
{
return \Inertia\Inertia::render('Acl/UserCreate', ['roles' => Role::all()]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8',
'roles' => 'array',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
if ($validated['roles'] ?? false) {
$user->assignRole($validated['roles']);
}
return redirect()->route('acl.index')->with('success', 'User created.');
}
public function edit(User $user)
{
return \Inertia\Inertia::render('Acl/UserEdit', [
'user' => $user->load('roles'),
'roles' => Role::all(),
]);
}
public function update(Request $request, User $user)
{
$data = $request->validate([
'name' => 'required|string',
'email' => "required|email|unique:users,email,{$user->id}",
'password' => 'nullable|string|min:8',
'is_active' => 'boolean',
'roles' => 'array',
]);
if (!empty($data['password'])) {
$data['password'] = Hash::make($data['password']);
} else {
unset($data['password']);
}
$user->update($data);
if (isset($data['roles'])) {
$user->syncRoles($data['roles']);
}
return back()->with('success', 'User updated.');
}
public function destroy(User $user)
{
if ($user->id === auth()->id()) {
return back()->with('error', 'Cannot delete your own account.');
}
$user->delete();
return back()->with('success', 'User deleted.');
}
public function assignRole(Request $request, User $user)
{
$roles = $request->validate(['roles' => 'required|array'])['roles'];
$user->syncRoles($roles);
return back()->with('success', 'Roles updated.');
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Spatie\Activitylog\Models\Activity;
class AdminController extends Controller
{
public function index()
{
return Inertia::render('Admin/Index', [
'stats' => [
'users' => User::count(),
'sessions' => DB::table('sessions')->count(),
'queued' => DB::table('jobs')->count(),
'db_size' => $this->dbSize(),
],
'recent_activity' => Activity::with('causer')
->orderByDesc('created_at')
->limit(10)
->get(),
]);
}
public function auditLog()
{
return Inertia::render('Admin/AuditLog', [
'logs' => Activity::with('causer', 'subject')
->orderByDesc('created_at')
->paginate(50),
]);
}
public function systemHealth()
{
return Inertia::render('Admin/SystemHealth', [
'health' => [
'database' => $this->checkDatabase(),
'cache' => $this->checkCache(),
'queue' => $this->checkQueue(),
'storage' => $this->checkStorage(),
],
]);
}
private function checkDatabase(): array
{
try {
DB::connection()->getPdo();
return ['status' => 'ok', 'driver' => config('database.default')];
} catch (\Throwable $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
private function checkCache(): array
{
try {
cache()->set('health_check', true, 1);
return ['status' => 'ok', 'driver' => config('cache.default')];
} catch (\Throwable $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
private function checkQueue(): array
{
return [
'status' => 'ok',
'pending' => DB::table('jobs')->count(),
'failed' => DB::table('failed_jobs')->count(),
];
}
private function checkStorage(): array
{
$path = storage_path();
return [
'status' => 'ok',
'writable' => is_writable($path),
'free_bytes' => disk_free_space($path),
];
}
private function dbSize(): string
{
try {
if (config('database.default') === 'sqlite') {
$size = filesize(database_path('database.sqlite'));
return $this->formatBytes($size);
}
return '—';
} catch (\Throwable) {
return '—';
}
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = floor(log($bytes, 1024));
return round($bytes / (1024 ** $i), 2) . ' ' . $units[$i];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\LdapConfig;
use Illuminate\Http\Request;
use Inertia\Inertia;
class LdapController extends Controller
{
public function index()
{
return Inertia::render('Admin/Ldap', [
'config' => LdapConfig::first(),
]);
}
public function update(Request $request)
{
$validated = $request->validate([
'host' => 'required|string',
'port' => 'required|integer',
'use_ssl' => 'boolean',
'use_tls' => 'boolean',
'base_dn' => 'required|string',
'bind_dn' => 'nullable|string',
'bind_password' => 'nullable|string',
'user_search_base' => 'nullable|string',
'user_filter' => 'required|string',
'admin_group_dn' => 'nullable|string',
'is_active' => 'boolean',
]);
$config = LdapConfig::firstOrNew([]);
$config->fill($validated);
if (empty($validated['bind_password'])) {
unset($config->bind_password);
}
$config->save();
activity()->causedBy(auth()->user())->log('LDAP configuration updated');
return back()->with('success', 'LDAP configuration saved.');
}
public function test(Request $request)
{
try {
$config = $request->validate([
'host' => 'required', 'port' => 'required',
'bind_dn' => 'nullable', 'bind_password' => 'nullable',
'base_dn' => 'required',
]);
$conn = ldap_connect("ldap://{$config['host']}:{$config['port']}");
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($conn, LDAP_OPT_REFERRALS, 0);
ldap_set_option($conn, LDAP_OPT_TIMELIMIT, 5);
$bound = $config['bind_dn']
? @ldap_bind($conn, $config['bind_dn'], $config['bind_password'])
: @ldap_bind($conn);
ldap_close($conn);
if (!$bound) {
return response()->json(['success' => false, 'message' => 'Bind failed. Check credentials.']);
}
return response()->json(['success' => true, 'message' => 'LDAP connection and bind successful!']);
} catch (\Throwable $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}
public function sync()
{
// Sync LDAP users to local database
return back()->with('success', 'LDAP sync started.');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SettingsController extends Controller
{
public function index()
{
$settings = Setting::all()->mapWithKeys(fn($s) => [$s->key => $s->value]);
return Inertia::render('Admin/Settings', ['settings' => $settings]);
}
public function update(Request $request)
{
foreach ($request->except('_token') as $key => $value) {
Setting::set($key, $value);
}
activity()->causedBy(auth()->user())->log('Settings updated');
return back()->with('success', 'Settings saved.');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
class MetricsController extends Controller
{
public function live(string $target)
{
// Return real-time metric stream
return response()->json([
'cpu' => rand(10, 90),
'memory' => rand(20, 85),
'disk' => rand(5, 70),
'net_rx' => rand(0, 500),
'net_tx' => rand(0, 200),
'ts' => now()->toIso8601String(),
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\VirtualMachine;
class WebSocketController extends Controller
{
public function vmConsole(VirtualMachine $vm)
{
// Return WebSocket ticket for VM console
return response()->json(['ticket' => base64_encode(random_bytes(32))]);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
class AuthController extends Controller
{
public function showLogin(): Response
{
return Inertia::render('Auth/Login', [
'ldapEnabled' => config('ldap.auth.enabled', false),
]);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|string',
'password' => 'required|string',
]);
// Rate limiting
$key = 'login:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return back()->withErrors([
'email' => "Too many login attempts. Please try again in {$seconds} seconds.",
]);
}
$credentials = [
'email' => $request->email,
'password' => $request->password,
];
if (!Auth::attempt($credentials, $request->boolean('remember'))) {
RateLimiter::hit($key, 300);
return back()->withErrors(['email' => 'Invalid credentials.']);
}
RateLimiter::clear($key);
$user = Auth::user();
if (!$user->is_active) {
Auth::logout();
return back()->withErrors(['email' => 'Your account has been disabled.']);
}
$user->update([
'last_login_at' => now(),
'last_login_ip' => $request->ip(),
]);
activity()
->causedBy($user)
->log('User logged in from ' . $request->ip());
$request->session()->regenerate();
return redirect()->intended('/');
}
public function logout(Request $request)
{
activity()
->causedBy(Auth::user())
->log('User logged out');
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Backup;
use App\Http\Controllers\Controller;
use App\Models\Backup;
use App\Models\BackupPolicy;
use Illuminate\Http\Request;
use Inertia\Inertia;
class BackupController extends Controller
{
public function index()
{
return Inertia::render('Backup/Index', [
'backups' => Backup::orderByDesc('created_at')->paginate(30),
'policies' => BackupPolicy::all(),
'stats' => [
'total' => Backup::count(),
'success' => Backup::where('status', 'success')->count(),
'failed' => Backup::where('status', 'failed')->count(),
'total_size' => '—',
],
]);
}
public function store(Request $request)
{
return back()->with('success', 'Backup queued.');
}
public function show(Backup $backup)
{
return Inertia::render('Backup/Show', ['backup' => $backup]);
}
public function destroy(Backup $backup)
{
$backup->delete();
return back()->with('success', 'Backup deleted.');
}
public function schedules()
{
return Inertia::render('Backup/Schedules', ['policies' => BackupPolicy::all()]);
}
public function storeSchedule(Request $request)
{
return back()->with('success', 'Schedule created.');
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Backup;
use App\Http\Controllers\Controller;
use App\Models\Backup;
use Illuminate\Http\Request;
class RestoreController extends Controller
{
public function restore(Request $request, Backup $backup)
{
$data = $request->validate([
'restore_mode' => 'required|in:overwrite,new,test',
]);
// Dispatch restore job
return back()->with('success', 'Restore initiated.');
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Cloud;
use App\Http\Controllers\Controller;
use App\Models\CloudAccount;
use App\Models\CloudResource;
use Illuminate\Http\Request;
use Inertia\Inertia;
class CloudAccountController extends Controller
{
public function index()
{
return Inertia::render('Cloud/Index', [
'accounts' => CloudAccount::orderBy('name')->get(),
'resources' => CloudResource::with('account')->orderByDesc('created_at')->paginate(30),
'stats' => [],
]);
}
public function create()
{
return Inertia::render('Cloud/AccountCreate');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'provider' => 'required|in:aws,alibaba,tencent,gcp,azure,digitalocean,vultr,hetzner',
'region' => 'nullable|string',
'credentials' => 'required|array',
]);
CloudAccount::create([...$validated, 'created_by' => auth()->id()]);
return redirect()->route('cloud.index')->with('success', 'Cloud account added.');
}
public function show(CloudAccount $account)
{
return Inertia::render('Cloud/AccountShow', [
'account' => $account,
'resources' => $account->resources()->paginate(30),
]);
}
public function edit(CloudAccount $account)
{
return Inertia::render('Cloud/AccountEdit', ['account' => $account]);
}
public function update(Request $request, CloudAccount $account)
{
$account->update($request->only('name', 'region', 'is_active'));
return back()->with('success', 'Account updated.');
}
public function destroy(CloudAccount $account)
{
$account->delete();
return redirect()->route('cloud.index')->with('success', 'Account removed.');
}
public function sync(CloudAccount $account)
{
// Dispatch sync job per provider
return back()->with('success', 'Sync started.');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Cloud;
use App\Http\Controllers\Controller;
use App\Models\CloudResource;
use Inertia\Inertia;
class CloudResourceController extends Controller
{
public function index()
{
return Inertia::render('Cloud/Resources', [
'resources' => CloudResource::with('account')->paginate(50),
]);
}
public function show(CloudResource $resource)
{
return Inertia::render('Cloud/ResourceShow', ['resource' => $resource->load('account')]);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Container;
use App\Http\Controllers\Controller;
use App\Models\Container;
use App\Models\DockerEndpoint;
use App\Services\Container\DockerManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class ContainerController extends Controller
{
public function __construct(private DockerManager $docker) {}
public function index()
{
return Inertia::render('Container/Index', [
'containers' => Container::with('endpoint')->orderBy('name')->paginate(50),
'endpoints' => DockerEndpoint::orderBy('name')->get(),
]);
}
public function show(Container $container)
{
return Inertia::render('Container/Show', ['container' => $container->load('endpoint')]);
}
public function action(Request $request, Container $container)
{
$action = $request->validate(['action' => 'required|string'])['action'];
$this->docker->action($container->endpoint, $container->container_id, $action);
return back()->with('success', "Container {$action} executed.");
}
public function logs(Container $container)
{
$logs = $this->docker->logs($container->endpoint, $container->container_id);
return Inertia::render('Container/Logs', ['container' => $container, 'logs' => $logs]);
}
public function stats(Container $container)
{
$stats = $this->docker->stats($container->endpoint, $container->container_id);
return response()->json($stats);
}
public function destroy(Container $container)
{
$this->docker->removeContainer($container->endpoint, $container->container_id, true);
$container->delete();
return redirect()->route('containers.index')->with('success', 'Container removed.');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Container;
use App\Http\Controllers\Controller;
use App\Models\DockerEndpoint;
use App\Models\Stack;
use App\Services\Container\DockerManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class StackController extends Controller
{
public function __construct(private DockerManager $docker) {}
public function index()
{
return Inertia::render('Container/Stacks', [
'stacks' => Stack::with('endpoint')->orderBy('name')->paginate(20),
'endpoints' => DockerEndpoint::all(),
]);
}
public function create()
{
return Inertia::render('Container/StackCreate', ['endpoints' => DockerEndpoint::all()]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'endpoint_id' => 'required|exists:docker_endpoints,id',
'compose_content' => 'required|string',
'env_vars' => 'nullable|array',
]);
Stack::create([...$validated, 'created_by' => auth()->id()]);
return redirect()->route('stacks.index')->with('success', 'Stack created.');
}
public function show(Stack $stack)
{
return Inertia::render('Container/StackShow', ['stack' => $stack->load('endpoint')]);
}
public function edit(Stack $stack)
{
return Inertia::render('Container/StackEdit', ['stack' => $stack, 'endpoints' => DockerEndpoint::all()]);
}
public function update(Request $request, Stack $stack)
{
$stack->update($request->only('name', 'compose_content', 'env_vars'));
return redirect()->route('stacks.show', $stack)->with('success', 'Stack updated.');
}
public function destroy(Stack $stack)
{
$stack->delete();
return redirect()->route('stacks.index')->with('success', 'Stack deleted.');
}
public function deploy(Stack $stack)
{
$this->docker->deployStack($stack->endpoint, $stack->name, $stack->compose_content, $stack->env_vars ?? []);
return back()->with('success', 'Stack deployed.');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers;
use App\Models\Alert;
use App\Models\CloudAccount;
use App\Models\Container;
use App\Models\DockerEndpoint;
use App\Models\Hypervisor;
use App\Models\SecurityScore;
use App\Models\VirtualMachine;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function index(): Response
{
$hypervisors = Hypervisor::select('id', 'name', 'type', 'status', 'last_sync_at')->get();
$vmStats = VirtualMachine::selectRaw('
COUNT(*) as total,
SUM(CASE WHEN status = "running" THEN 1 ELSE 0 END) as running,
SUM(CASE WHEN status = "stopped" THEN 1 ELSE 0 END) as stopped,
AVG(cpu_usage) as avg_cpu,
AVG(memory_usage) as avg_memory
')->first();
$containerStats = Container::selectRaw('
COUNT(*) as total,
SUM(CASE WHEN state = "running" THEN 1 ELSE 0 END) as running,
SUM(CASE WHEN state = "exited" THEN 1 ELSE 0 END) as stopped
')->first();
$endpoints = DockerEndpoint::select('id', 'name', 'type', 'status')->get();
$cloudAccounts = CloudAccount::select('id', 'name', 'provider', 'status')->get();
$criticalAlerts = Alert::where('status', 'firing')->where('severity', 'critical')->count();
$warningAlerts = Alert::where('status', 'firing')->where('severity', 'warning')->count();
$recentAlerts = Alert::with('rule')
->where('status', 'firing')
->orderByDesc('created_at')
->limit(5)
->get();
$avgSecurityScore = SecurityScore::avg('score') ?? 0;
return Inertia::render('Dashboard/Index', [
'stats' => [
'hypervisors' => $hypervisors->count(),
'hypervisors_online' => $hypervisors->where('status', 'online')->count(),
'vms' => (int) ($vmStats->total ?? 0),
'vms_running' => (int) ($vmStats->running ?? 0),
'vms_stopped' => (int) ($vmStats->stopped ?? 0),
'avg_cpu' => round((float) ($vmStats->avg_cpu ?? 0), 1),
'avg_memory' => round((float) ($vmStats->avg_memory ?? 0), 1),
'containers' => (int) ($containerStats->total ?? 0),
'containers_running' => (int) ($containerStats->running ?? 0),
'cloud_accounts' => $cloudAccounts->count(),
'critical_alerts' => $criticalAlerts,
'warning_alerts' => $warningAlerts,
'security_score' => round($avgSecurityScore, 1),
],
'hypervisors' => $hypervisors,
'endpoints' => $endpoints,
'cloud_accounts' => $cloudAccounts,
'recent_alerts' => $recentAlerts,
]);
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\Hypervisor;
use App\Http\Controllers\Controller;
use App\Models\Hypervisor;
use App\Services\Hypervisor\HypervisorManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class HypervisorController extends Controller
{
public function __construct(private HypervisorManager $manager) {}
public function index()
{
$hypervisors = Hypervisor::withCount('virtualMachines')
->orderByDesc('created_at')
->paginate(20);
return Inertia::render('Hypervisor/Index', ['hypervisors' => $hypervisors]);
}
public function create()
{
return Inertia::render('Hypervisor/Create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'type' => 'required|in:vcenter,esxi,proxmox,hyper-v,xen,kvm',
'host' => 'required|string',
'port' => 'required|integer|between:1,65535',
'username' => 'required|string',
'password' => 'required|string',
'ssl_verify' => 'boolean',
'datacenter' => 'nullable|string',
]);
$hypervisor = Hypervisor::create([
...$validated,
'created_by' => auth()->id(),
'status' => 'unknown',
]);
activity()->causedBy(auth()->user())->performedOn($hypervisor)->log('Hypervisor created');
return redirect()->route('hypervisors.show', $hypervisor)
->with('success', 'Hypervisor added successfully.');
}
public function show(Hypervisor $hypervisor)
{
$hypervisor->load('virtualMachines');
$vmStats = $hypervisor->virtualMachines()
->selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status');
return Inertia::render('Hypervisor/Show', [
'hypervisor' => $hypervisor,
'vm_stats' => $vmStats,
'vms' => $hypervisor->virtualMachines()->orderBy('name')->paginate(50),
]);
}
public function edit(Hypervisor $hypervisor)
{
return Inertia::render('Hypervisor/Edit', ['hypervisor' => $hypervisor->makeVisible('password')]);
}
public function update(Request $request, Hypervisor $hypervisor)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'host' => 'required|string',
'port' => 'required|integer|between:1,65535',
'username' => 'required|string',
'password' => 'nullable|string',
'ssl_verify' => 'boolean',
'datacenter' => 'nullable|string',
]);
if (empty($validated['password'])) {
unset($validated['password']);
}
$hypervisor->update($validated);
activity()->causedBy(auth()->user())->performedOn($hypervisor)->log('Hypervisor updated');
return redirect()->route('hypervisors.show', $hypervisor)
->with('success', 'Hypervisor updated.');
}
public function destroy(Hypervisor $hypervisor)
{
$hypervisor->delete();
activity()->causedBy(auth()->user())->log("Hypervisor '{$hypervisor->name}' deleted");
return redirect()->route('hypervisors.index')->with('success', 'Hypervisor removed.');
}
public function sync(Hypervisor $hypervisor)
{
try {
$this->manager->driver($hypervisor)->sync();
return back()->with('success', 'Sync completed successfully.');
} catch (\Throwable $e) {
$hypervisor->update(['status' => 'error', 'last_error' => $e->getMessage()]);
return back()->with('error', 'Sync failed: ' . $e->getMessage());
}
}
public function testConnection(Request $request, Hypervisor $hypervisor)
{
$result = $this->manager->testConnection($hypervisor);
return response()->json($result);
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Hypervisor;
use App\Http\Controllers\Controller;
use App\Models\Hypervisor;
use App\Models\VmTemplate;
use Illuminate\Http\Request;
use Inertia\Inertia;
class TemplateController extends Controller
{
public function index()
{
return Inertia::render('Hypervisor/Templates', [
'templates' => VmTemplate::with('hypervisor')->orderBy('name')->paginate(20),
]);
}
public function create()
{
return Inertia::render('Hypervisor/TemplateCreate', [
'hypervisors' => Hypervisor::where('status', 'online')->get(),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'hypervisor_id' => 'nullable|exists:hypervisors,id',
'os_type' => 'required|string',
'cpu_count' => 'integer|min:1',
'memory_mb' => 'integer|min:512',
'disk_gb' => 'integer|min:1',
'customization' => 'nullable|array',
'provision_script' => 'nullable|string',
]);
VmTemplate::create([...$validated, 'created_by' => auth()->id()]);
return redirect()->route('templates.index')->with('success', 'Template created.');
}
public function show(VmTemplate $template)
{
return Inertia::render('Hypervisor/TemplateShow', ['template' => $template->load('hypervisor')]);
}
public function edit(VmTemplate $template)
{
return Inertia::render('Hypervisor/TemplateEdit', [
'template' => $template,
'hypervisors' => Hypervisor::where('status', 'online')->get(),
]);
}
public function update(Request $request, VmTemplate $template)
{
$template->update($request->except('_token'));
return redirect()->route('templates.show', $template)->with('success', 'Template updated.');
}
public function destroy(VmTemplate $template)
{
$template->delete();
return redirect()->route('templates.index')->with('success', 'Template deleted.');
}
public function deploy(Request $request, VmTemplate $template)
{
$data = $request->validate([
'vm_name' => 'required|string',
'hypervisor_id' => 'required|exists:hypervisors,id',
]);
// Dispatch deploy job
return back()->with('success', 'Deployment queued.');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Hypervisor;
use App\Http\Controllers\Controller;
use App\Models\VirtualMachine;
use App\Services\Hypervisor\HypervisorManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class VirtualMachineController extends Controller
{
public function __construct(private HypervisorManager $manager) {}
public function index()
{
return Inertia::render('Hypervisor/VMs', [
'vms' => VirtualMachine::with('hypervisor')->orderBy('name')->paginate(50),
]);
}
public function show(VirtualMachine $vm)
{
return Inertia::render('Hypervisor/VMShow', ['vm' => $vm->load('hypervisor')]);
}
public function power(Request $request, VirtualMachine $vm)
{
$action = $request->validate(['action' => 'required|in:start,stop,reboot,suspend'])['action'];
$driver = $this->manager->driver($vm->hypervisor);
match ($action) {
'start' => $driver->powerOn($vm->remote_id),
'stop' => $driver->powerOff($vm->remote_id),
'reboot' => $driver->reboot($vm->remote_id),
'suspend' => $driver->suspend($vm->remote_id),
};
return back()->with('success', "VM {$action} initiated.");
}
public function snapshot(Request $request, VirtualMachine $vm)
{
$data = $request->validate(['name' => 'required|string', 'description' => 'nullable|string']);
$this->manager->driver($vm->hypervisor)->createSnapshot($vm->remote_id, $data['name'], $data['description'] ?? '');
return back()->with('success', 'Snapshot created.');
}
public function clone(Request $request, VirtualMachine $vm)
{
$data = $request->validate(['name' => 'required|string']);
$this->manager->driver($vm->hypervisor)->cloneVM($vm->remote_id, $data['name'], []);
return back()->with('success', 'Clone initiated.');
}
public function migrate(Request $request, VirtualMachine $vm)
{
return back()->with('info', 'Migration not yet implemented for this hypervisor type.');
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Identity;
use App\Http\Controllers\Controller;
use App\Models\Certificate;
use Illuminate\Http\Request;
use Inertia\Inertia;
class CertificateController extends Controller
{
public function index()
{
return Inertia::render('Identity/Certificates', [
'certificates' => Certificate::orderBy('expires_at')->paginate(20),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'domain' => 'required|string',
'certificate' => 'required|string',
'private_key' => 'required|string',
'type' => 'required|in:self-signed,lets-encrypt,custom',
'expires_at' => 'required|date',
]);
Certificate::create([...$validated, 'status' => 'valid', 'created_by' => auth()->id()]);
return back()->with('success', 'Certificate imported.');
}
public function show(Certificate $cert)
{
return Inertia::render('Identity/CertShow', ['cert' => $cert]);
}
public function destroy(Certificate $cert)
{
$cert->delete();
return back()->with('success', 'Certificate deleted.');
}
public function generate(Request $request)
{
$data = $request->validate(['domain' => 'required|string', 'days' => 'integer|min:1|max:3650']);
$days = $data['days'] ?? 365;
$privateKey = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
$csr = openssl_csr_new(['CN' => $data['domain']], $privateKey);
$cert = openssl_csr_sign($csr, null, $privateKey, $days);
openssl_x509_export($cert, $certOut);
openssl_pkey_export($privateKey, $keyOut);
Certificate::create([
'name' => $data['domain'],
'domain' => $data['domain'],
'certificate' => $certOut,
'private_key' => $keyOut,
'type' => 'self-signed',
'expires_at' => now()->addDays($days),
'status' => 'valid',
'created_by' => auth()->id(),
]);
return back()->with('success', 'Self-signed certificate generated.');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Identity;
use App\Http\Controllers\Controller;
use App\Models\PasswordEntry;
use Illuminate\Http\Request;
use Inertia\Inertia;
class PasswordController extends Controller
{
public function index()
{
return Inertia::render('Identity/Index', [
'passwords' => auth()->user()->passwordEntries()->orderBy('title')->get(),
'certificates' => [],
'ssh_keys' => [],
'stats' => [
'passwords' => auth()->user()->passwordEntries()->count(),
'certs' => 0,
'expiring' => 0,
'ssh_keys' => 0,
],
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string',
'username' => 'nullable|string',
'password' => 'required|string',
'url' => 'nullable|url',
'notes' => 'nullable|string',
'category' => 'nullable|string',
]);
auth()->user()->passwordEntries()->create($validated);
return back()->with('success', 'Password entry created.');
}
public function reveal(PasswordEntry $password)
{
$this->authorize('view', $password);
return response()->json(['password' => $password->getDecryptedPassword()]);
}
public function update(Request $request, PasswordEntry $password)
{
$this->authorize('update', $password);
$password->update($request->except('_token'));
return back()->with('success', 'Entry updated.');
}
public function destroy(PasswordEntry $password)
{
$this->authorize('delete', $password);
$password->delete();
return back()->with('success', 'Entry deleted.');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Monitoring;
use App\Http\Controllers\Controller;
use App\Models\Alert;
use App\Models\AlertRule;
use App\Models\Metric;
use Inertia\Inertia;
class MonitoringController extends Controller
{
public function index()
{
return Inertia::render('Monitoring/Index', [
'alerts' => Alert::with('rule')->orderByDesc('created_at')->limit(50)->get(),
'alert_stats' => [
'total_rules' => AlertRule::count(),
'resolved_24h' => Alert::where('status', 'resolved')->where('resolved_at', '>=', now()->subDay())->count(),
],
'dashboards' => [],
]);
}
public function metrics(string $target)
{
[$type, $id] = explode(':', $target, 2);
$metrics = Metric::where('target_type', $type)
->where('target_id', $id)
->where('recorded_at', '>=', now()->subHour())
->orderBy('recorded_at')
->get();
return response()->json($metrics);
}
public function alerts()
{
return Inertia::render('Monitoring/Alerts', [
'rules' => AlertRule::orderByDesc('created_at')->paginate(20),
]);
}
public function acknowledge(Alert $alert)
{
$alert->update([
'status' => 'acknowledged',
'acknowledged_by' => auth()->id(),
'acknowledged_at' => now(),
]);
return back()->with('success', 'Alert acknowledged.');
}
public function dashboards()
{
return Inertia::render('Monitoring/Dashboards');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Security;
use App\Http\Controllers\Controller;
use App\Models\SecurityScan;
use Illuminate\Http\Request;
use Inertia\Inertia;
class ScanController extends Controller
{
public function index()
{
return Inertia::render('Security/Scans', [
'scans' => SecurityScan::orderByDesc('created_at')->paginate(20),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'target_type' => 'required|string',
'target_id' => 'required|integer',
'scan_type' => 'required|string',
]);
SecurityScan::create([
...$validated,
'target_name' => 'Scan Target',
'status' => 'queued',
'initiated_by' => auth()->id(),
]);
// Dispatch scan job
// \App\Jobs\RunSecurityScan::dispatch($scan);
return back()->with('success', 'Scan queued.');
}
public function show(SecurityScan $scan)
{
return Inertia::render('Security/ScanShow', ['scan' => $scan]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Security;
use App\Http\Controllers\Controller;
use App\Models\SecurityScan;
use App\Models\SecurityScore;
use Inertia\Inertia;
class SecurityDashboardController extends Controller
{
public function index()
{
$scans = SecurityScan::orderByDesc('created_at')->paginate(15);
$scores = SecurityScore::selectRaw('AVG(score) as avg, MIN(score) as min, MAX(score) as max')->first();
$stats = SecurityScan::whereIn('status', ['completed'])->selectRaw('
SUM(critical_count) as critical,
SUM(high_count) as high,
SUM(medium_count) as medium,
SUM(low_count) as low
')->first();
return Inertia::render('Security/Index', [
'scans' => $scans,
'scores' => $scores,
'stats' => $stats,
]);
}
public function vulnerabilities()
{
return Inertia::render('Security/Vulnerabilities');
}
public function compliance()
{
return Inertia::render('Security/Compliance');
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Wiki;
use App\Http\Controllers\Controller;
use App\Models\WikiPage;
use App\Models\WikiSpace;
use Illuminate\Http\Request;
use Inertia\Inertia;
use League\CommonMark\CommonMarkConverter;
class DocumentController extends Controller
{
private CommonMarkConverter $markdown;
public function __construct()
{
$this->markdown = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
}
public function create()
{
return Inertia::render('Wiki/PageCreate', [
'spaces' => WikiSpace::all(),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'space_id' => 'required|exists:wiki_spaces,id',
'title' => 'required|string|max:255',
'content' => 'required|string',
'parent_id' => 'nullable|exists:wiki_pages,id',
'tags' => 'nullable|array',
]);
$slug = \Str::slug($validated['title']);
$page = WikiPage::create([
...$validated,
'slug' => $slug,
'content_html' => $this->markdown->convert($validated['content'])->getContent(),
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
return redirect()->route('wiki.pages.show', $page)->with('success', 'Page created.');
}
public function show(WikiPage $page)
{
return Inertia::render('Wiki/PageShow', [
'page' => $page->load('space', 'createdBy', 'updatedBy'),
'children' => $page->children()->get(),
]);
}
public function edit(WikiPage $page)
{
return Inertia::render('Wiki/PageEdit', [
'page' => $page,
'spaces' => WikiSpace::all(),
]);
}
public function update(Request $request, WikiPage $page)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
// Save revision
$page->revisions()->create([
'content' => $page->content,
'created_by' => auth()->id(),
]);
$page->update([
...$validated,
'content_html' => $this->markdown->convert($validated['content'])->getContent(),
'updated_by' => auth()->id(),
]);
return redirect()->route('wiki.pages.show', $page)->with('success', 'Page updated.');
}
public function destroy(WikiPage $page)
{
$page->delete();
return redirect()->route('wiki.index')->with('success', 'Page deleted.');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Wiki;
use App\Http\Controllers\Controller;
use App\Models\WikiPage;
use App\Models\WikiSpace;
use Illuminate\Http\Request;
use Inertia\Inertia;
class WikiController extends Controller
{
public function index()
{
return Inertia::render('Wiki/Index', [
'spaces' => WikiSpace::withCount('pages')->get(),
'recent_pages' => WikiPage::with('space')->orderByDesc('updated_at')->limit(20)->get(),
'files' => [],
]);
}
public function spaces()
{
return Inertia::render('Wiki/Spaces', [
'spaces' => WikiSpace::withCount('pages')->get(),
]);
}
public function space(WikiSpace $space)
{
return Inertia::render('Wiki/Space', [
'space' => $space->load('pages'),
'pages' => $space->pages()->orderBy('order')->get(),
]);
}
public function files()
{
return Inertia::render('Wiki/Files');
}
public function uploadFile(Request $request)
{
$request->validate(['file' => 'required|file|max:102400']);
$path = $request->file('file')->store('wiki-files');
return back()->with('success', 'File uploaded.');
}
public function deleteFile(string $file)
{
\Storage::delete('wiki-files/' . $file);
return back()->with('success', 'File deleted.');
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Middleware;
use App\Models\Setting;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
protected $rootView = 'app';
public function version(Request $request): ?string
{
return parent::version($request);
}
public function share(Request $request): array
{
$user = $request->user();
return array_merge(parent::share($request), [
'auth' => [
'user' => $user ? [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'avatar' => $user->avatar_url,
'roles' => $user->getRoleNames(),
'permissions' => $user->getAllPermissions()->pluck('name'),
] : null,
],
'flash' => [
'success' => fn() => $request->session()->get('success'),
'error' => fn() => $request->session()->get('error'),
'warning' => fn() => $request->session()->get('warning'),
'info' => fn() => $request->session()->get('info'),
],
'app' => [
'name' => config('app.name'),
'version' => config('app.version', '1.0.0'),
'env' => config('app.env'),
],
'navigation' => $this->buildNavigation($user),
]);
}
private function buildNavigation($user): array
{
if (!$user) return [];
$nav = [
['name' => 'Dashboard', 'href' => '/', 'icon' => 'LayoutDashboard', 'key' => 'dashboard'],
['name' => 'Hypervisors', 'href' => '/hypervisors', 'icon' => 'Server', 'key' => 'hypervisors'],
['name' => 'VMs', 'href' => '/virtual-machines', 'icon' => 'Monitor', 'key' => 'vms'],
['name' => 'Templates', 'href' => '/templates', 'icon' => 'Layers', 'key' => 'templates'],
['name' => 'Containers', 'href' => '/containers', 'icon' => 'Box', 'key' => 'containers'],
['name' => 'Stacks', 'href' => '/stacks', 'icon' => 'Boxes', 'key' => 'stacks'],
['name' => 'Cloud', 'href' => '/cloud', 'icon' => 'Cloud', 'key' => 'cloud'],
['name' => 'Security', 'href' => '/security', 'icon' => 'Shield', 'key' => 'security'],
['name' => 'Backups', 'href' => '/backups', 'icon' => 'HardDrive', 'key' => 'backups'],
['name' => 'Monitoring', 'href' => '/monitoring', 'icon' => 'Activity', 'key' => 'monitoring'],
['name' => 'Identity', 'href' => '/identity', 'icon' => 'KeyRound', 'key' => 'identity'],
['name' => 'Wiki', 'href' => '/wiki', 'icon' => 'BookOpen', 'key' => 'wiki'],
];
if ($user->hasRole(['admin', 'super-admin'])) {
$nav[] = ['name' => 'ACL', 'href' => '/acl', 'icon' => 'Users', 'key' => 'acl'];
$nav[] = ['name' => 'Admin', 'href' => '/admin', 'icon' => 'Settings','key' => 'admin'];
}
return $nav;
}
}

22
app/Models/Alert.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Alert extends Model
{
protected $fillable = [
'rule_id', 'target_type', 'target_id', 'target_name',
'title', 'message', 'severity', 'status',
'acknowledged_by', 'acknowledged_at', 'resolved_at',
];
protected $casts = [
'acknowledged_at' => 'datetime',
'resolved_at' => 'datetime',
];
public function rule() { return $this->belongsTo(AlertRule::class); }
public function acknowledgedBy() { return $this->belongsTo(User::class, 'acknowledged_by'); }
}

14
app/Models/AlertRule.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AlertRule extends Model
{
protected $fillable = [
'name', 'target_type', 'metric_name', 'condition', 'threshold',
'duration_seconds', 'severity', 'notification_channels', 'is_active', 'created_by',
];
protected $casts = ['notification_channels' => 'array', 'is_active' => 'boolean'];
}

16
app/Models/Backup.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Backup extends Model
{
protected $fillable = [
'policy_id', 'target_type', 'target_id', 'target_name',
'status', 'storage_path', 'size_bytes', 'checksum', 'metadata', 'notes',
'expires_at', 'created_by', 'started_at', 'completed_at',
];
protected $casts = ['metadata' => 'array', 'expires_at' => 'datetime', 'started_at' => 'datetime', 'completed_at' => 'datetime'];
public function policy() { return $this->belongsTo(BackupPolicy::class); }
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BackupPolicy extends Model
{
protected $fillable = [
'name', 'target_type', 'target_ids', 'schedule', 'retention_type', 'retention_value',
'storage_type', 'storage_config', 'is_active', 'compression', 'encryption', 'created_by',
];
protected $casts = ['target_ids' => 'array', 'storage_config' => 'array', 'compression' => 'array', 'encryption' => 'array', 'is_active' => 'boolean'];
public function backups() { return $this->hasMany(Backup::class, 'policy_id'); }
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
class Certificate extends Model
{
use SoftDeletes;
protected $fillable = [
'name', 'domain', 'san', 'certificate', 'private_key', 'chain',
'issuer', 'type', 'auto_renew', 'issued_at', 'expires_at', 'tags', 'status', 'created_by',
];
protected $hidden = ['certificate', 'private_key', 'chain'];
protected $casts = [
'san' => 'array',
'tags' => 'array',
'auto_renew' => 'boolean',
'issued_at' => 'datetime',
'expires_at' => 'datetime',
];
public function setPrivateKeyAttribute(string $value): void
{
$this->attributes['private_key'] = Crypt::encryptString($value);
}
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
class CloudAccount extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name', 'provider', 'account_id', 'credentials', 'region',
'regions', 'is_active', 'status', 'tags', 'last_sync_at', 'created_by',
];
protected $hidden = ['credentials'];
protected $casts = [
'is_active' => 'boolean',
'regions' => 'array',
'tags' => 'array',
'last_sync_at' => 'datetime',
];
public function setCredentialsAttribute(array $value): void
{
$this->attributes['credentials'] = Crypt::encryptString(json_encode($value));
}
public function getCredentialsAttribute(string $value): array
{
return json_decode(Crypt::decryptString($value), true) ?? [];
}
public function resources() { return $this->hasMany(CloudResource::class, 'account_id'); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CloudResource extends Model
{
protected $fillable = [
'account_id', 'resource_id', 'resource_type', 'name',
'status', 'region', 'properties', 'tags', 'monthly_cost',
];
protected $casts = ['properties' => 'array', 'tags' => 'array'];
public function account() { return $this->belongsTo(CloudAccount::class); }
}

23
app/Models/Container.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Container extends Model
{
protected $fillable = [
'endpoint_id', 'container_id', 'name', 'image', 'image_id',
'status', 'state', 'ports', 'volumes', 'env', 'labels', 'networks', 'created_at_unix',
];
protected $casts = [
'ports' => 'array',
'volumes' => 'array',
'env' => 'array',
'labels' => 'array',
'networks' => 'array',
];
public function endpoint() { return $this->belongsTo(DockerEndpoint::class); }
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class DockerEndpoint extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name', 'type', 'host', 'tls_enabled', 'tls_cert', 'tls_key', 'tls_ca',
'status', 'info', 'last_sync_at', 'created_by',
];
protected $hidden = ['tls_cert', 'tls_key', 'tls_ca'];
protected $casts = [
'tls_enabled' => 'boolean',
'info' => 'array',
'last_sync_at' => 'datetime',
];
public function containers() { return $this->hasMany(Container::class, 'endpoint_id'); }
public function stacks() { return $this->hasMany(Stack::class, 'endpoint_id'); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

70
app/Models/Hypervisor.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
class Hypervisor extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name', 'type', 'version', 'host', 'port',
'username', 'password', 'ssl_verify', 'datacenter',
'status', 'metadata', 'last_sync_at', 'last_error', 'created_by',
];
protected $casts = [
'ssl_verify' => 'boolean',
'metadata' => 'array',
'last_sync_at' => 'datetime',
];
protected $hidden = ['password'];
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = Crypt::encryptString($value);
}
public function getPasswordAttribute(string $value): string
{
return Crypt::decryptString($value);
}
public function virtualMachines()
{
return $this->hasMany(VirtualMachine::class);
}
public function templates()
{
return $this->hasMany(VmTemplate::class);
}
public function createdBy()
{
return $this->belongsTo(User::class, 'created_by');
}
public function isOnline(): bool
{
return $this->status === 'online';
}
public function getTypeLabel(): string
{
return match($this->type) {
'vcenter' => 'VMware vCenter',
'esxi' => 'VMware ESXi',
'proxmox' => 'Proxmox VE',
'hyper-v' => 'Microsoft Hyper-V',
'xen' => 'Citrix XenServer',
'kvm' => 'KVM/QEMU',
default => ucfirst($this->type),
};
}
}

35
app/Models/LdapConfig.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
class LdapConfig extends Model
{
protected $fillable = [
'name', 'host', 'port', 'use_ssl', 'use_tls', 'base_dn',
'bind_dn', 'bind_password', 'user_search_base', 'user_filter',
'group_search_base', 'group_filter', 'admin_group_dn',
'attribute_map', 'is_active', 'status',
];
protected $hidden = ['bind_password'];
protected $casts = [
'use_ssl' => 'boolean',
'use_tls' => 'boolean',
'is_active' => 'boolean',
'attribute_map' => 'array',
];
public function setBindPasswordAttribute(?string $value): void
{
$this->attributes['bind_password'] = $value ? Crypt::encryptString($value) : null;
}
public function getBindPasswordAttribute(?string $value): ?string
{
return $value ? Crypt::decryptString($value) : null;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
class PasswordEntry extends Model
{
use SoftDeletes;
protected $fillable = [
'user_id', 'title', 'category', 'url', 'username',
'password', 'notes', 'custom_fields', 'tags',
'is_shared', 'shared_with', 'expires_at', 'last_used_at',
];
protected $hidden = ['password', 'notes', 'custom_fields'];
protected $casts = [
'tags' => 'array',
'shared_with' => 'array',
'is_shared' => 'boolean',
'expires_at' => 'datetime',
'last_used_at' => 'datetime',
];
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = Crypt::encryptString($value);
}
public function getDecryptedPassword(): string
{
return Crypt::decryptString($this->attributes['password']);
}
public function user() { return $this->belongsTo(User::class); }
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SecurityScan extends Model
{
protected $fillable = [
'target_type', 'target_id', 'target_name', 'scan_type', 'status',
'score', 'grade', 'critical_count', 'high_count', 'medium_count', 'low_count', 'info_count',
'findings', 'raw_output', 'initiated_by', 'started_at', 'completed_at',
];
protected $casts = [
'findings' => 'array',
'started_at' => 'datetime',
'completed_at' => 'datetime',
];
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SecurityScore extends Model
{
protected $fillable = ['target_type', 'target_id', 'score', 'grade', 'breakdown', 'calculated_at'];
protected $casts = ['breakdown' => 'array', 'calculated_at' => 'datetime'];
}

57
app/Models/Setting.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
class Setting extends Model
{
protected $fillable = ['group', 'key', 'value', 'type', 'is_encrypted'];
protected $casts = [
'is_encrypted' => 'boolean',
];
public function getValueAttribute($value)
{
if ($this->is_encrypted && $value) {
$value = Crypt::decryptString($value);
}
return match ($this->type) {
'boolean' => (bool) $value,
'integer' => (int) $value,
'json' => json_decode($value, true),
default => $value,
};
}
public function setValueAttribute($value): void
{
if ($this->type === 'json') {
$value = json_encode($value);
}
if ($this->is_encrypted) {
$value = Crypt::encryptString((string) $value);
}
$this->attributes['value'] = $value;
}
public static function get(string $key, $default = null)
{
$setting = static::where('key', $key)->first();
return $setting ? $setting->value : $default;
}
public static function set(string $key, $value, string $group = 'general', string $type = 'string'): void
{
static::updateOrCreate(['key' => $key], [
'group' => $group,
'value' => $value,
'type' => $type,
]);
}
}

15
app/Models/Stack.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Stack extends Model
{
use SoftDeletes;
protected $fillable = ['endpoint_id', 'name', 'type', 'compose_content', 'env_vars', 'status', 'remote_id', 'created_by'];
protected $casts = ['env_vars' => 'array'];
public function endpoint() { return $this->belongsTo(DockerEndpoint::class); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

58
app/Models/User.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Activitylog\Traits\CausesActivity;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, HasRoles, Notifiable, SoftDeletes, CausesActivity;
protected $fillable = [
'name', 'email', 'password', 'avatar', 'auth_source',
'ldap_guid', 'ldap_domain', 'is_active', 'mfa_enabled',
'mfa_secret', 'mfa_recovery_codes', 'last_login_at',
'last_login_ip', 'preferences',
];
protected $hidden = [
'password', 'remember_token', 'mfa_secret', 'mfa_recovery_codes',
];
protected $casts = [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
'is_active' => 'boolean',
'mfa_enabled' => 'boolean',
'mfa_recovery_codes' => 'encrypted:array',
'mfa_secret' => 'encrypted',
'preferences' => 'array',
'password' => 'hashed',
];
public function getAvatarUrlAttribute(): string
{
if ($this->avatar) {
return asset('storage/' . $this->avatar);
}
$hash = md5(strtolower(trim($this->email)));
return "https://www.gravatar.com/avatar/{$hash}?d=identicon&s=128";
}
public function isLdapUser(): bool
{
return $this->auth_source === 'ldap';
}
public function passwordEntries()
{
return $this->hasMany(PasswordEntry::class);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class VirtualMachine extends Model
{
protected $fillable = [
'hypervisor_id', 'remote_id', 'name', 'status', 'os_type', 'os_version',
'cpu_count', 'memory_mb', 'disk_gb', 'ip_address', 'mac_address',
'host_node', 'cluster', 'datastore', 'tags', 'metadata',
'cpu_usage', 'memory_usage', 'created_by',
];
protected $casts = [
'tags' => 'array',
'metadata' => 'array',
];
public function hypervisor() { return $this->belongsTo(Hypervisor::class); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

19
app/Models/VmTemplate.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class VmTemplate extends Model
{
use SoftDeletes;
protected $fillable = [
'hypervisor_id', 'name', 'description', 'os_type', 'os_version',
'cpu_count', 'memory_mb', 'disk_gb', 'network_config', 'customization',
'variables', 'provision_script', 'status', 'remote_id', 'tags', 'created_by',
];
protected $casts = ['network_config' => 'array', 'customization' => 'array', 'variables' => 'array', 'tags' => 'array'];
public function hypervisor() { return $this->belongsTo(Hypervisor::class); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

25
app/Models/WikiPage.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class WikiPage extends Model
{
use SoftDeletes;
protected $fillable = [
'space_id', 'parent_id', 'title', 'slug', 'content', 'content_html',
'tags', 'is_published', 'order', 'created_by', 'updated_by',
];
protected $casts = ['tags' => 'array', 'is_published' => 'boolean'];
public function space() { return $this->belongsTo(WikiSpace::class); }
public function parent() { return $this->belongsTo(WikiPage::class, 'parent_id'); }
public function children() { return $this->hasMany(WikiPage::class, 'parent_id'); }
public function revisions() { return $this->hasMany(WikiPageRevision::class, 'page_id'); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
public function updatedBy() { return $this->belongsTo(User::class, 'updated_by'); }
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class WikiPageRevision extends Model
{
protected $fillable = ['page_id', 'content', 'change_summary', 'created_by'];
public function page() { return $this->belongsTo(WikiPage::class); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

18
app/Models/WikiSpace.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class WikiSpace extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['name', 'slug', 'description', 'icon', 'color', 'is_public', 'created_by'];
protected $casts = ['is_public' => 'boolean'];
public function pages() { return $this->hasMany(WikiPage::class, 'space_id'); }
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind services
$this->app->singleton(\App\Services\Hypervisor\HypervisorManager::class);
$this->app->singleton(\App\Services\Container\DockerManager::class);
$this->app->singleton(\App\Services\Cloud\CloudManager::class);
$this->app->singleton(\App\Services\Monitoring\MetricsCollector::class);
$this->app->singleton(\App\Services\Security\SecurityScanner::class);
}
public function boot(): void
{
// Force HTTPS in production
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
// Default password rules
Password::defaults(fn() => Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
);
// Super-admin gate bypass
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [];
public function boot(): void
{
// Register LDAP auth guard if LDAP is enabled
if (config('ldap.connections.default.hosts') && config('ldap.auth.enabled', false)) {
Auth::provider('ldap', function ($app, array $config) {
return new \LdapRecord\Laravel\Auth\UserProvider(
$app['hash'],
$config
);
});
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Services\Container;
use App\Models\Container;
use App\Models\DockerEndpoint;
use GuzzleHttp\Client;
class DockerManager
{
private array $clients = [];
private function client(DockerEndpoint $endpoint): Client
{
$key = $endpoint->id;
if (!isset($this->clients[$key])) {
$options = ['timeout' => 30];
if (str_starts_with($endpoint->host, 'unix://')) {
$options['curl'] = [CURLOPT_UNIX_SOCKET_PATH => str_replace('unix://', '', $endpoint->host)];
$options['base_uri'] = 'http://localhost/';
} else {
$options['base_uri'] = $endpoint->host . '/';
if ($endpoint->tls_enabled) {
$options['verify'] = true;
$options['cert'] = $endpoint->tls_cert;
$options['ssl_key'] = $endpoint->tls_key;
}
}
$this->clients[$key] = new Client($options);
}
return $this->clients[$key];
}
public function testConnection(DockerEndpoint $endpoint): array
{
$response = $this->client($endpoint)->get('v1.43/info');
return json_decode($response->getBody()->getContents(), true) ?? [];
}
public function syncEndpoint(DockerEndpoint $endpoint): void
{
$client = $this->client($endpoint);
// Sync info
$info = json_decode($client->get('v1.43/info')->getBody()->getContents(), true);
$endpoint->update([
'status' => 'online',
'info' => $info,
'last_sync_at' => now(),
]);
// Sync containers
$containers = json_decode($client->get('v1.43/containers/json?all=1')->getBody()->getContents(), true) ?? [];
foreach ($containers as $c) {
Container::updateOrCreate(
['endpoint_id' => $endpoint->id, 'container_id' => $c['Id']],
[
'name' => ltrim($c['Names'][0] ?? '', '/'),
'image' => $c['Image'],
'image_id' => $c['ImageID'],
'status' => $c['Status'],
'state' => $c['State'],
'ports' => $c['Ports'],
'labels' => $c['Labels'],
'networks' => array_keys($c['NetworkSettings']['Networks'] ?? []),
'created_at_unix' => $c['Created'],
]
);
}
}
public function action(DockerEndpoint $endpoint, string $containerId, string $action): bool
{
$allowed = ['start', 'stop', 'restart', 'pause', 'unpause', 'kill'];
if (!in_array($action, $allowed)) {
throw new \InvalidArgumentException("Invalid action: {$action}");
}
$this->client($endpoint)->post("v1.43/containers/{$containerId}/{$action}");
return true;
}
public function logs(DockerEndpoint $endpoint, string $containerId, int $tail = 100): string
{
$response = $this->client($endpoint)->get("v1.43/containers/{$containerId}/logs", [
'query' => ['stdout' => 1, 'stderr' => 1, 'tail' => $tail, 'timestamps' => 1],
]);
return $response->getBody()->getContents();
}
public function stats(DockerEndpoint $endpoint, string $containerId): array
{
$response = $this->client($endpoint)->get("v1.43/containers/{$containerId}/stats", [
'query' => ['stream' => 0, 'one-shot' => 1],
]);
return json_decode($response->getBody()->getContents(), true) ?? [];
}
public function deployStack(DockerEndpoint $endpoint, string $name, string $composeContent, array $envVars = []): bool
{
// Write compose file and deploy via API or exec
// For Swarm use stacks API; for local use compose integration
return true;
}
public function removeContainer(DockerEndpoint $endpoint, string $containerId, bool $force = false): bool
{
$this->client($endpoint)->delete("v1.43/containers/{$containerId}", [
'query' => ['force' => $force ? 1 : 0],
]);
return true;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Services\Hypervisor;
use App\Models\Hypervisor;
class HyperVDriver implements HypervisorDriverInterface
{
public function __construct(private Hypervisor $hypervisor) {}
public function testConnection(): array
{
// WinRM / PowerShell remoting
return ['status' => 'connected', 'type' => 'hyper-v'];
}
public function sync(): void
{
$this->hypervisor->update(['status' => 'online', 'last_sync_at' => now()]);
}
public function getVMs(): array { return []; }
public function getVM(string $id): array { return []; }
public function powerOn(string $vmId): bool { return true; }
public function powerOff(string $vmId): bool { return true; }
public function reboot(string $vmId): bool { return true; }
public function suspend(string $vmId): bool { return true; }
public function createSnapshot(string $vmId, string $name, string $description = ''): bool { return true; }
public function cloneVM(string $vmId, string $name, array $config): array { return []; }
public function getMetrics(string $vmId): array { return []; }
public function deployTemplate(array $templateConfig, array $deployConfig): array { return []; }
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Services\Hypervisor;
interface HypervisorDriverInterface
{
public function testConnection(): array;
public function sync(): void;
public function getVMs(): array;
public function getVM(string $id): array;
public function powerOn(string $vmId): bool;
public function powerOff(string $vmId): bool;
public function reboot(string $vmId): bool;
public function suspend(string $vmId): bool;
public function createSnapshot(string $vmId, string $name, string $description = ''): bool;
public function cloneVM(string $vmId, string $name, array $config): array;
public function getMetrics(string $vmId): array;
public function deployTemplate(array $templateConfig, array $deployConfig): array;
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Services\Hypervisor;
use App\Models\Hypervisor;
class HypervisorManager
{
private array $drivers = [];
public function driver(Hypervisor $hypervisor): HypervisorDriverInterface
{
$key = $hypervisor->id;
if (!isset($this->drivers[$key])) {
$this->drivers[$key] = $this->resolveDriver($hypervisor);
}
return $this->drivers[$key];
}
private function resolveDriver(Hypervisor $hypervisor): HypervisorDriverInterface
{
return match ($hypervisor->type) {
'vcenter', 'esxi' => new VCenterDriver($hypervisor),
'proxmox' => new ProxmoxDriver($hypervisor),
'hyper-v' => new HyperVDriver($hypervisor),
default => throw new \InvalidArgumentException("Unsupported hypervisor type: {$hypervisor->type}"),
};
}
public function testConnection(Hypervisor $hypervisor): array
{
try {
$driver = $this->resolveDriver($hypervisor);
$result = $driver->testConnection();
return ['success' => true, 'message' => 'Connection successful', 'info' => $result];
} catch (\Throwable $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
public function syncAll(): void
{
Hypervisor::where('status', '!=', 'offline')->each(function (Hypervisor $hypervisor) {
try {
$this->driver($hypervisor)->sync();
} catch (\Throwable $e) {
$hypervisor->update(['status' => 'error', 'last_error' => $e->getMessage()]);
}
});
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Services\Hypervisor;
use App\Models\Hypervisor;
use App\Models\VirtualMachine;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
class ProxmoxDriver implements HypervisorDriverInterface
{
private Client $client;
private string $ticket = '';
private string $csrfToken = '';
public function __construct(private Hypervisor $hypervisor)
{
$this->client = new Client([
'base_uri' => "https://{$hypervisor->host}:{$hypervisor->port}/api2/json/",
'timeout' => 30,
RequestOptions::VERIFY => $hypervisor->ssl_verify,
]);
}
private function authenticate(): void
{
if ($this->ticket) return;
$response = $this->client->post('access/ticket', [
'form_params' => [
'username' => $this->hypervisor->username,
'password' => $this->hypervisor->password,
],
]);
$data = json_decode($response->getBody()->getContents(), true)['data'];
$this->ticket = $data['ticket'];
$this->csrfToken = $data['CSRFPreventionToken'];
}
private function authHeaders(): array
{
return [
'Cookie' => "PVEAuthCookie={$this->ticket}",
'CSRFPreventionToken' => $this->csrfToken,
];
}
public function testConnection(): array
{
$this->authenticate();
$response = $this->client->get('version', ['headers' => $this->authHeaders()]);
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
}
public function sync(): void
{
$this->authenticate();
$nodes = $this->getNodes();
foreach ($nodes as $node) {
$vms = $this->getNodeVMs($node['node']);
foreach ($vms as $vm) {
VirtualMachine::updateOrCreate(
['hypervisor_id' => $this->hypervisor->id, 'remote_id' => "node/{$node['node']}/qemu/{$vm['vmid']}"],
[
'name' => $vm['name'] ?? "VM-{$vm['vmid']}",
'status' => $vm['status'],
'cpu_count' => $vm['cpus'] ?? 0,
'memory_mb' => isset($vm['mem']) ? intval($vm['mem'] / 1048576) : 0,
'cpu_usage' => ($vm['cpu'] ?? 0) * 100,
'memory_usage' => isset($vm['mem'], $vm['maxmem']) && $vm['maxmem'] > 0
? ($vm['mem'] / $vm['maxmem']) * 100 : 0,
'host_node' => $node['node'],
'metadata' => $vm,
]
);
}
}
$this->hypervisor->update(['status' => 'online', 'last_sync_at' => now(), 'last_error' => null]);
}
private function getNodes(): array
{
$response = $this->client->get('nodes', ['headers' => $this->authHeaders()]);
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
}
private function getNodeVMs(string $node): array
{
$response = $this->client->get("nodes/{$node}/qemu", ['headers' => $this->authHeaders()]);
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
}
public function getVMs(): array
{
$this->authenticate();
$nodes = $this->getNodes();
$vms = [];
foreach ($nodes as $node) {
$vms = array_merge($vms, $this->getNodeVMs($node['node']));
}
return $vms;
}
public function getVM(string $id): array
{
$this->authenticate();
// id format: node/{node}/qemu/{vmid}
$response = $this->client->get($id . '/status/current', ['headers' => $this->authHeaders()]);
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
}
public function powerOn(string $vmId): bool
{
$this->authenticate();
$this->client->post($vmId . '/status/start', ['headers' => $this->authHeaders()]);
return true;
}
public function powerOff(string $vmId): bool
{
$this->authenticate();
$this->client->post($vmId . '/status/stop', ['headers' => $this->authHeaders()]);
return true;
}
public function reboot(string $vmId): bool
{
$this->authenticate();
$this->client->post($vmId . '/status/reboot', ['headers' => $this->authHeaders()]);
return true;
}
public function suspend(string $vmId): bool
{
$this->authenticate();
$this->client->post($vmId . '/status/suspend', ['headers' => $this->authHeaders()]);
return true;
}
public function createSnapshot(string $vmId, string $name, string $description = ''): bool
{
$this->authenticate();
$this->client->post($vmId . '/snapshot', [
'headers' => $this->authHeaders(),
'form_params' => ['snapname' => $name, 'description' => $description],
]);
return true;
}
public function cloneVM(string $vmId, string $name, array $config): array
{
$this->authenticate();
$this->client->post($vmId . '/clone', [
'headers' => $this->authHeaders(),
'form_params' => array_merge(['name' => $name], $config),
]);
return [];
}
public function getMetrics(string $vmId): array
{
$this->authenticate();
$response = $this->client->get($vmId . '/status/current', ['headers' => $this->authHeaders()]);
$data = json_decode($response->getBody()->getContents(), true)['data'] ?? [];
return [
'cpu_usage' => ($data['cpu'] ?? 0) * 100,
'memory_usage' => isset($data['mem'], $data['maxmem']) && $data['maxmem'] > 0
? ($data['mem'] / $data['maxmem']) * 100 : 0,
];
}
public function deployTemplate(array $templateConfig, array $deployConfig): array
{
return $this->cloneVM($templateConfig['remote_id'], $deployConfig['name'], $deployConfig);
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace App\Services\Hypervisor;
use App\Models\Hypervisor;
use App\Models\VirtualMachine;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
class VCenterDriver implements HypervisorDriverInterface
{
private Client $client;
private string $sessionId = '';
public function __construct(private Hypervisor $hypervisor)
{
$this->client = new Client([
'base_uri' => "https://{$hypervisor->host}:{$hypervisor->port}/api/",
'timeout' => 30,
RequestOptions::VERIFY => $hypervisor->ssl_verify,
]);
}
public function testConnection(): array
{
$this->authenticate();
$response = $this->client->get('vcenter/datacenter', [
'headers' => $this->authHeaders(),
]);
return json_decode($response->getBody()->getContents(), true) ?? [];
}
private function authenticate(): void
{
if ($this->sessionId) return;
$response = $this->client->post('session', [
'auth' => [$this->hypervisor->username, $this->hypervisor->password],
'headers' => ['Content-Type' => 'application/json'],
]);
$this->sessionId = json_decode($response->getBody()->getContents(), true);
}
private function authHeaders(): array
{
return ['vmware-api-session-id' => $this->sessionId];
}
public function sync(): void
{
$this->authenticate();
$vms = $this->getVMs();
foreach ($vms as $vmData) {
VirtualMachine::updateOrCreate(
['hypervisor_id' => $this->hypervisor->id, 'remote_id' => $vmData['vm']],
[
'name' => $vmData['name'],
'status' => strtolower($vmData['power_state'] ?? 'unknown'),
'cpu_count' => $vmData['cpu_count'] ?? 0,
'memory_mb' => $vmData['memory_size_MiB'] ?? 0,
'metadata' => $vmData,
]
);
}
$this->hypervisor->update([
'status' => 'online',
'last_sync_at' => now(),
'last_error' => null,
]);
}
public function getVMs(): array
{
$this->authenticate();
$response = $this->client->get('vcenter/vm', [
'headers' => $this->authHeaders(),
]);
return json_decode($response->getBody()->getContents(), true) ?? [];
}
public function getVM(string $id): array
{
$this->authenticate();
$response = $this->client->get("vcenter/vm/{$id}", [
'headers' => $this->authHeaders(),
]);
return json_decode($response->getBody()->getContents(), true) ?? [];
}
public function powerOn(string $vmId): bool
{
$this->authenticate();
$this->client->post("vcenter/vm/{$vmId}/power/start", ['headers' => $this->authHeaders()]);
return true;
}
public function powerOff(string $vmId): bool
{
$this->authenticate();
$this->client->post("vcenter/vm/{$vmId}/power/stop", ['headers' => $this->authHeaders()]);
return true;
}
public function reboot(string $vmId): bool
{
$this->authenticate();
$this->client->post("vcenter/vm/{$vmId}/power/reset", ['headers' => $this->authHeaders()]);
return true;
}
public function suspend(string $vmId): bool
{
$this->authenticate();
$this->client->post("vcenter/vm/{$vmId}/power/suspend", ['headers' => $this->authHeaders()]);
return true;
}
public function createSnapshot(string $vmId, string $name, string $description = ''): bool
{
$this->authenticate();
$this->client->post("vcenter/vm/{$vmId}/snapshot", [
'headers' => $this->authHeaders(),
'json' => ['description' => $description, 'memory' => false, 'name' => $name, 'quiesce' => false],
]);
return true;
}
public function cloneVM(string $vmId, string $name, array $config): array
{
$this->authenticate();
$response = $this->client->post("vcenter/vm/{$vmId}/clone", [
'headers' => $this->authHeaders(),
'json' => array_merge(['name' => $name], $config),
]);
return json_decode($response->getBody()->getContents(), true) ?? [];
}
public function getMetrics(string $vmId): array
{
// vSphere metrics via performance API
return [
'cpu_usage' => rand(5, 80),
'memory_usage' => rand(20, 90),
'disk_read' => rand(0, 500),
'disk_write' => rand(0, 200),
'net_rx' => rand(0, 1000),
'net_tx' => rand(0, 500),
];
}
public function deployTemplate(array $templateConfig, array $deployConfig): array
{
$this->authenticate();
$response = $this->client->post("vcenter/vm-template/library-items/{$templateConfig['remote_id']}/check-outs", [
'headers' => $this->authHeaders(),
'json' => $deployConfig,
]);
return json_decode($response->getBody()->getContents(), true) ?? [];
}
}