223 lines
7.2 KiB
PHP
223 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Ui\System;
|
|
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
|
|
#[Layout('layouts.dvx')]
|
|
#[Title('Installer · Mailwolt')]
|
|
class InstallerPage extends Component
|
|
{
|
|
/* ===== Run state ===== */
|
|
public string $state = 'idle'; // idle | running
|
|
public bool $running = false;
|
|
public ?int $rc = null;
|
|
public ?string $lowState = null;
|
|
public array $logLines = [];
|
|
public int $progressPct = 0;
|
|
public string $component = 'all';
|
|
|
|
public bool $postActionsDone = false;
|
|
|
|
/* ===== Component status ===== */
|
|
public array $componentStatus = [];
|
|
|
|
private const STATE_DIR = '/var/lib/mailwolt/install';
|
|
private const INSTALL_LOG = '/var/log/mailwolt-install.log';
|
|
|
|
private const COMPONENTS = [
|
|
'nginx' => ['label' => 'Nginx', 'service' => 'nginx'],
|
|
'postfix' => ['label' => 'Postfix', 'service' => 'postfix'],
|
|
'dovecot' => ['label' => 'Dovecot', 'service' => 'dovecot'],
|
|
'rspamd' => ['label' => 'Rspamd', 'service' => 'rspamd'],
|
|
'fail2ban' => ['label' => 'Fail2ban', 'service' => 'fail2ban'],
|
|
'certbot' => ['label' => 'Certbot', 'service' => null, 'binary' => 'certbot'],
|
|
];
|
|
|
|
/* ========================================================= */
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->refreshLowLevelState();
|
|
$this->readLogLines();
|
|
|
|
if ($this->running) {
|
|
$this->state = 'running';
|
|
}
|
|
|
|
$this->checkComponentStatus();
|
|
$this->recalcProgress();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.ui.system.installer-page');
|
|
}
|
|
|
|
/* ================== Aktionen ================== */
|
|
|
|
public function openConfirmModal(string $component = 'all'): void
|
|
{
|
|
$this->component = $component;
|
|
$this->dispatch('openModal',
|
|
component: 'ui.system.modal.installer-confirm-modal',
|
|
arguments: ['component' => $component]
|
|
);
|
|
}
|
|
|
|
public function runInstaller(string $component = 'all'): void
|
|
{
|
|
if ($this->running || $this->state === 'running') {
|
|
$this->dispatch('toast', type: 'warn', badge: 'Installer',
|
|
title: 'Läuft bereits',
|
|
text: 'Ein Installer-Prozess ist bereits aktiv.',
|
|
duration: 3000);
|
|
return;
|
|
}
|
|
|
|
$this->component = $component;
|
|
$this->state = 'running';
|
|
$this->running = true;
|
|
$this->rc = null;
|
|
$this->postActionsDone = false;
|
|
$this->logLines = ['Installer gestartet …'];
|
|
$this->progressPct = 5;
|
|
|
|
$safeComponent = preg_replace('/[^a-z0-9_-]/i', '', $component);
|
|
@shell_exec("nohup sudo -n /usr/local/sbin/mailwolt-install {$safeComponent} >/dev/null 2>&1 &");
|
|
}
|
|
|
|
public function pollStatus(): void
|
|
{
|
|
$this->refreshLowLevelState();
|
|
$this->readLogLines();
|
|
$this->recalcProgress();
|
|
|
|
if ($this->rc !== null) {
|
|
$this->running = false;
|
|
}
|
|
|
|
if ($this->lowState === 'done') {
|
|
usleep(300_000);
|
|
$this->readLogLines();
|
|
$this->progressPct = 100;
|
|
|
|
if ($this->rc === 0 && !$this->postActionsDone) {
|
|
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
|
$this->postActionsDone = true;
|
|
|
|
$this->dispatch('toast', type: 'done', badge: 'Installer',
|
|
title: 'Installation abgeschlossen',
|
|
text: 'Die Komponente wurde erfolgreich installiert/konfiguriert.',
|
|
duration: 6000);
|
|
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
|
$this->postActionsDone = true;
|
|
$this->dispatch('toast', type: 'error', badge: 'Installer',
|
|
title: 'Installation fehlgeschlagen',
|
|
text: "Rückgabecode: {$this->rc}. Bitte Log prüfen.",
|
|
duration: 0);
|
|
}
|
|
|
|
$this->state = 'idle';
|
|
$this->checkComponentStatus();
|
|
}
|
|
}
|
|
|
|
public function checkComponentStatus(): void
|
|
{
|
|
$statuses = [];
|
|
|
|
foreach (self::COMPONENTS as $key => $info) {
|
|
$installed = false;
|
|
$active = false;
|
|
|
|
// Check if binary exists
|
|
$binary = $info['binary'] ?? $key;
|
|
$which = @trim(@shell_exec("which {$binary} 2>/dev/null") ?: '');
|
|
$installed = $which !== '';
|
|
|
|
// Check service active state
|
|
if ($installed && isset($info['service']) && $info['service']) {
|
|
$svcState = @trim(@shell_exec("systemctl is-active {$info['service']} 2>/dev/null") ?: '');
|
|
$active = ($svcState === 'active');
|
|
} elseif ($installed && $key === 'certbot') {
|
|
$active = true; // certbot is a one-shot tool, if installed it's "OK"
|
|
}
|
|
|
|
$statuses[$key] = [
|
|
'label' => $info['label'],
|
|
'installed' => $installed,
|
|
'active' => $active,
|
|
];
|
|
}
|
|
|
|
$this->componentStatus = $statuses;
|
|
}
|
|
|
|
public function clearLog(): void
|
|
{
|
|
@file_put_contents(self::INSTALL_LOG, '');
|
|
$this->logLines = [];
|
|
$this->dispatch('toast', type: 'done', badge: 'Installer',
|
|
title: 'Log geleert', text: '', duration: 2500);
|
|
}
|
|
|
|
/* ================== Helpers ================== */
|
|
|
|
protected function refreshLowLevelState(): void
|
|
{
|
|
$state = @trim(@file_get_contents(self::STATE_DIR . '/state') ?: '');
|
|
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
|
|
|
|
$this->lowState = $state !== '' ? $state : null;
|
|
$this->running = ($this->lowState !== 'done');
|
|
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
|
|
}
|
|
|
|
protected function readLogLines(): void
|
|
{
|
|
$p = self::INSTALL_LOG;
|
|
if (!is_readable($p)) {
|
|
$this->logLines = [];
|
|
return;
|
|
}
|
|
$lines = @file($p, FILE_IGNORE_NEW_LINES) ?: [];
|
|
$this->logLines = array_slice($lines, -100);
|
|
}
|
|
|
|
protected function recalcProgress(): void
|
|
{
|
|
if ($this->state !== 'running' && $this->lowState !== 'running') {
|
|
if ($this->lowState === 'done') {
|
|
$this->progressPct = 100;
|
|
}
|
|
return;
|
|
}
|
|
|
|
$text = implode("\n", $this->logLines);
|
|
$pct = 5;
|
|
foreach ([
|
|
'Installation gestartet' => 10,
|
|
'Nginx' => 20,
|
|
'Postfix' => 35,
|
|
'Dovecot' => 50,
|
|
'Rspamd' => 65,
|
|
'Fail2ban' => 78,
|
|
'SSL' => 88,
|
|
'Installation beendet' => 100,
|
|
] as $needle => $val) {
|
|
if (stripos($text, $needle) !== false) {
|
|
$pct = max($pct, $val);
|
|
}
|
|
}
|
|
|
|
if ($this->lowState === 'done') {
|
|
$pct = 100;
|
|
}
|
|
|
|
$this->progressPct = $pct;
|
|
}
|
|
}
|