333 lines
14 KiB
PHP
333 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Admin\Pages;
|
|
|
|
use App\Filament\Concerns\HasPageTour;
|
|
use App\Models\AuditLog;
|
|
use App\Services\Agent\AgentClient;
|
|
use BackedEnum;
|
|
use Exception;
|
|
use Filament\Actions\Action;
|
|
use Filament\Actions\Concerns\InteractsWithActions;
|
|
use Filament\Actions\Contracts\HasActions;
|
|
use Filament\Forms\Concerns\InteractsWithForms;
|
|
use Filament\Forms\Contracts\HasForms;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Contracts\Support\Htmlable;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class Services extends Page implements HasActions, HasForms, HasTable
|
|
{
|
|
use HasPageTour;
|
|
use InteractsWithActions;
|
|
use InteractsWithForms;
|
|
use InteractsWithTable;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cog-6-tooth';
|
|
|
|
protected static ?int $navigationSort = 10;
|
|
|
|
public static function getNavigationLabel(): string
|
|
{
|
|
return __('Services');
|
|
}
|
|
|
|
protected string $view = 'filament.admin.pages.services';
|
|
|
|
public array $services = [];
|
|
|
|
public ?string $selectedService = null;
|
|
|
|
protected ?AgentClient $agent = null;
|
|
|
|
protected array $baseServices = [
|
|
'nginx' => ['name' => 'Nginx', 'description' => 'Web Server', 'icon' => 'globe'],
|
|
'mariadb' => ['name' => 'MariaDB', 'description' => 'Database Server', 'icon' => 'database'],
|
|
'redis-server' => ['name' => 'Redis', 'description' => 'Cache Server', 'icon' => 'bolt'],
|
|
'postfix' => ['name' => 'Postfix', 'description' => 'Mail Transfer Agent', 'icon' => 'envelope'],
|
|
'dovecot' => ['name' => 'Dovecot', 'description' => 'IMAP/POP3 Server', 'icon' => 'inbox'],
|
|
'rspamd' => ['name' => 'Rspamd', 'description' => 'Spam Filter', 'icon' => 'shield'],
|
|
'clamav-daemon' => ['name' => 'ClamAV', 'description' => 'Antivirus Scanner', 'icon' => 'bug'],
|
|
'named' => ['name' => 'BIND9', 'description' => 'DNS Server', 'icon' => 'server'],
|
|
'opendkim' => ['name' => 'OpenDKIM', 'description' => 'DKIM Signing', 'icon' => 'key'],
|
|
'fail2ban' => ['name' => 'Fail2Ban', 'description' => 'Intrusion Prevention', 'icon' => 'lock'],
|
|
'ssh' => ['name' => 'SSH', 'description' => 'Secure Shell', 'icon' => 'terminal'],
|
|
'cron' => ['name' => 'Cron', 'description' => 'Task Scheduler', 'icon' => 'clock'],
|
|
];
|
|
|
|
protected ?array $managedServices = null;
|
|
|
|
protected function getManagedServices(): array
|
|
{
|
|
if ($this->managedServices !== null) {
|
|
return $this->managedServices;
|
|
}
|
|
|
|
$this->managedServices = [];
|
|
foreach ($this->baseServices as $key => $config) {
|
|
$this->managedServices[$key] = $config;
|
|
|
|
if ($key === 'nginx') {
|
|
foreach ($this->detectPhpFpmVersions() as $service => $phpConfig) {
|
|
$this->managedServices[$service] = $phpConfig;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->managedServices;
|
|
}
|
|
|
|
protected function detectPhpFpmVersions(): array
|
|
{
|
|
$phpServices = [];
|
|
$output = [];
|
|
exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $output);
|
|
|
|
foreach ($output as $servicePath) {
|
|
if (preg_match('/php([\d.]+)-fpm\.service$/', $servicePath, $matches)) {
|
|
$version = $matches[1];
|
|
$serviceName = "php{$version}-fpm";
|
|
$phpServices[$serviceName] = [
|
|
'name' => "PHP {$version} FPM",
|
|
'description' => 'PHP FastCGI Process Manager',
|
|
'icon' => 'code',
|
|
];
|
|
}
|
|
}
|
|
|
|
uksort($phpServices, function ($a, $b) {
|
|
preg_match('/php([\d.]+)-fpm/', $a, $matchA);
|
|
preg_match('/php([\d.]+)-fpm/', $b, $matchB);
|
|
|
|
return version_compare($matchB[1] ?? '0', $matchA[1] ?? '0');
|
|
});
|
|
|
|
return $phpServices;
|
|
}
|
|
|
|
public function getTitle(): string|Htmlable
|
|
{
|
|
return __('Service Manager');
|
|
}
|
|
|
|
public function getAgent(): AgentClient
|
|
{
|
|
return $this->agent ??= new AgentClient;
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->loadServices();
|
|
}
|
|
|
|
public function loadServices(): void
|
|
{
|
|
$managedServices = $this->getManagedServices();
|
|
|
|
try {
|
|
$result = $this->getAgent()->send('service.list', [
|
|
'services' => array_keys($managedServices),
|
|
]);
|
|
|
|
if ($result['success'] ?? false) {
|
|
$this->services = [];
|
|
foreach ($result['services'] ?? [] as $name => $status) {
|
|
$config = $managedServices[$name] ?? [
|
|
'name' => ucfirst($name),
|
|
'description' => '',
|
|
'icon' => 'cog',
|
|
];
|
|
$this->services[$name] = array_merge($config, [
|
|
'service' => $name,
|
|
'is_active' => $status['is_active'] ?? false,
|
|
'is_enabled' => $status['is_enabled'] ?? false,
|
|
'status' => $status['status'] ?? 'unknown',
|
|
]);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Error loading services'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->records(fn () => array_values($this->services))
|
|
->columns([
|
|
TextColumn::make('name')
|
|
->label(__('Service'))
|
|
->icon(fn (array $record): string => match ($record['icon'] ?? 'cog') {
|
|
'globe' => 'heroicon-o-globe-alt',
|
|
'code' => 'heroicon-o-code-bracket',
|
|
'database' => 'heroicon-o-circle-stack',
|
|
'bolt' => 'heroicon-o-bolt',
|
|
'envelope' => 'heroicon-o-envelope',
|
|
'inbox' => 'heroicon-o-inbox',
|
|
'shield' => 'heroicon-o-shield-check',
|
|
'server' => 'heroicon-o-server',
|
|
'key' => 'heroicon-o-key',
|
|
'lock' => 'heroicon-o-lock-closed',
|
|
'terminal' => 'heroicon-o-command-line',
|
|
'clock' => 'heroicon-o-clock',
|
|
'bug' => 'heroicon-o-bug-ant',
|
|
default => 'heroicon-o-cog-6-tooth',
|
|
})
|
|
->iconColor(fn (array $record): string => $record['is_active'] ? 'success' : 'danger')
|
|
->description(fn (array $record): string => $record['description'] ?? '')
|
|
->weight('medium')
|
|
->searchable(),
|
|
TextColumn::make('is_active')
|
|
->label(__('Status'))
|
|
->badge()
|
|
->formatStateUsing(fn (array $record): string => $record['is_active'] ? __('Running') : __('Stopped'))
|
|
->color(fn (array $record): string => $record['is_active'] ? 'success' : 'danger'),
|
|
TextColumn::make('is_enabled')
|
|
->label(__('Boot'))
|
|
->badge()
|
|
->formatStateUsing(fn (array $record): string => $record['is_enabled'] ? __('Enabled') : __('Disabled'))
|
|
->color(fn (array $record): string => $record['is_enabled'] ? 'success' : 'warning'),
|
|
])
|
|
->recordActions([
|
|
Action::make('start')
|
|
->label(__('Start'))
|
|
->icon('heroicon-o-play')
|
|
->color('success')
|
|
->size('sm')
|
|
->visible(fn (array $record): bool => ! $record['is_active'])
|
|
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'start')),
|
|
Action::make('stop')
|
|
->label(__('Stop'))
|
|
->icon('heroicon-o-stop')
|
|
->color('danger')
|
|
->size('sm')
|
|
->visible(fn (array $record): bool => $record['is_active'])
|
|
->requiresConfirmation()
|
|
->modalHeading(__('Stop Service'))
|
|
->modalDescription(fn (array $record): string => __('Are you sure you want to stop :service? This may affect running websites and services.', ['service' => $record['name']]))
|
|
->modalSubmitActionLabel(__('Stop Service'))
|
|
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'stop')),
|
|
Action::make('restart')
|
|
->label(fn (array $record): string => $this->shouldReloadService($record['service']) ? __('Reload') : __('Restart'))
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('info')
|
|
->size('sm')
|
|
->visible(fn (array $record): bool => $record['is_active'])
|
|
->action(fn (array $record) => $this->executeServiceAction(
|
|
$record['service'],
|
|
$this->shouldReloadService($record['service']) ? 'reload' : 'restart'
|
|
)),
|
|
Action::make('enable')
|
|
->label(__('Enable'))
|
|
->icon('heroicon-o-check')
|
|
->color('gray')
|
|
->size('sm')
|
|
->visible(fn (array $record): bool => ! $record['is_enabled'])
|
|
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'enable')),
|
|
Action::make('disable')
|
|
->label(__('Disable'))
|
|
->icon('heroicon-o-x-mark')
|
|
->color('warning')
|
|
->size('sm')
|
|
->visible(fn (array $record): bool => $record['is_enabled'])
|
|
->requiresConfirmation()
|
|
->modalHeading(__('Disable Service'))
|
|
->modalDescription(fn (array $record): string => __("Are you sure you want to disable :service? It won't start automatically on boot.", ['service' => $record['name']]))
|
|
->modalSubmitActionLabel(__('Disable Service'))
|
|
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'disable')),
|
|
])
|
|
->headerActions([
|
|
Action::make('refresh')
|
|
->label(__('Refresh'))
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('gray')
|
|
->action(function () {
|
|
$this->loadServices();
|
|
$this->resetTable();
|
|
Notification::make()->title(__('Services refreshed'))->success()->duration(1500)->send();
|
|
}),
|
|
])
|
|
->emptyStateHeading(__('No services found'))
|
|
->emptyStateDescription(__('Unable to load system services'))
|
|
->emptyStateIcon('heroicon-o-cog-6-tooth')
|
|
->striped();
|
|
}
|
|
|
|
public function getTableRecordKey(Model|array $record): string
|
|
{
|
|
return is_array($record) ? $record['service'] : $record->getKey();
|
|
}
|
|
|
|
protected function executeServiceAction(string $service, string $action): void
|
|
{
|
|
try {
|
|
$result = $this->getAgent()->send("service.{$action}", [
|
|
'service' => $service,
|
|
]);
|
|
|
|
if ($result['success'] ?? false) {
|
|
$notificationTitle = match ($action) {
|
|
'start' => __(':service started', ['service' => ucfirst($service)]),
|
|
'stop' => __(':service stopped', ['service' => ucfirst($service)]),
|
|
'restart' => __(':service restarted', ['service' => ucfirst($service)]),
|
|
'reload' => __(':service reloaded', ['service' => ucfirst($service)]),
|
|
'enable' => __(':service enabled', ['service' => ucfirst($service)]),
|
|
'disable' => __(':service disabled', ['service' => ucfirst($service)]),
|
|
default => ucfirst($service).' '.$action
|
|
};
|
|
|
|
$actionPast = match ($action) {
|
|
'start' => 'started',
|
|
'stop' => 'stopped',
|
|
'restart' => 'restarted',
|
|
'reload' => 'reloaded',
|
|
'enable' => 'enabled',
|
|
'disable' => 'disabled',
|
|
default => $action
|
|
};
|
|
|
|
Notification::make()
|
|
->title($notificationTitle)
|
|
->success()
|
|
->send();
|
|
|
|
AuditLog::logServiceAction($actionPast, $service);
|
|
|
|
$this->loadServices();
|
|
$this->resetTable();
|
|
} else {
|
|
throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error'));
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()
|
|
->title(__('Action failed'))
|
|
->body($e->getMessage())
|
|
->danger()
|
|
->send();
|
|
}
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
$this->getTourAction(),
|
|
];
|
|
}
|
|
|
|
protected function shouldReloadService(string $service): bool
|
|
{
|
|
if ($service === 'nginx') {
|
|
return true;
|
|
}
|
|
|
|
return preg_match('/^php(\d+\.\d+)?-fpm$/', $service) === 1;
|
|
}
|
|
}
|