feat(control): real systemctl actions + sudo-aware journal
Service start/stop/restart now run over SSH (root direct, else `sudo -n`) and
report the real result instead of an optimistic state flip; the Services list is
reloaded afterwards to reflect the actual unit state. journalctl in systemd() is
likewise run via sudo/root so the full system journal is available once a
privileged credential exists. Unit names are validated before interpolation.
Without a privileged credential the action fails honestly ("sudo: a password is
required") rather than pretending to succeed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
parent
1376772a0c
commit
f849076418
|
|
@ -79,14 +79,32 @@ class Index extends Component
|
|||
);
|
||||
}
|
||||
|
||||
/** Applies the confirmed service state change (optimistic until SSH exec lands). */
|
||||
/** Performs the confirmed systemctl action over SSH, then reloads real state. */
|
||||
#[On('serviceConfirmed')]
|
||||
public function applyService(string $op, string $name): void
|
||||
public function applyService(string $op, string $name, FleetService $fleet): void
|
||||
{
|
||||
foreach ($this->services as $i => $svc) {
|
||||
if ($svc['name'] === $name) {
|
||||
$this->services[$i]['status'] = $op === 'stop' ? 'offline' : 'online';
|
||||
}
|
||||
$active = $this->activeServer();
|
||||
|
||||
if (! $active || ! $active->credential_exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = $fleet->serviceAction($active, $op, $name);
|
||||
} catch (Throwable $e) {
|
||||
$this->dispatch('notify', message: "{$name}: ".$e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatch('notify', message: $res['ok']
|
||||
? "{$name}: {$op} ausgeführt."
|
||||
: "{$name}: fehlgeschlagen — ".\Illuminate\Support\Str::limit($res['output'] ?: 'keine Rechte', 90));
|
||||
|
||||
try {
|
||||
$this->services = $fleet->systemd($active)['services'];
|
||||
} catch (Throwable) {
|
||||
// keep current list on reload failure
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ class FleetService
|
|||
$cmd = 'export LC_ALL=C; '
|
||||
.'echo '.self::MARK.'units===; systemctl list-units --type=service --all --plain --no-legend --no-pager; '
|
||||
.'echo '.self::MARK.'files===; systemctl list-unit-files --type=service --no-legend --no-pager; '
|
||||
.'echo '.self::MARK.'journal===; journalctl -n '.(int) $journalLines.' --no-pager -o short-iso 2>&1';
|
||||
.'echo '.self::MARK.'journal===; if [ "$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo -n"; fi; $SUDO journalctl -n '.(int) $journalLines.' --no-pager -o short-iso 2>&1';
|
||||
|
||||
try {
|
||||
$s = $this->sections($ssh->exec($cmd));
|
||||
|
|
@ -373,6 +373,35 @@ class FleetService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* systemctl start/stop/restart. Runs directly as root, else via `sudo -n`
|
||||
* (needs a root or nopasswd-sudo credential).
|
||||
*
|
||||
* @return array{ok: bool, output: string}
|
||||
*/
|
||||
public function serviceAction(Server $server, string $op, string $unit): array
|
||||
{
|
||||
if (! in_array($op, ['start', 'stop', 'restart'], true)) {
|
||||
throw new InvalidArgumentException('Unbekannte Aktion.');
|
||||
}
|
||||
if (! preg_match('/^[\w@.:-]+$/', $unit)) {
|
||||
throw new InvalidArgumentException('Ungueltige Unit.');
|
||||
}
|
||||
|
||||
$ssh = $this->client($server);
|
||||
|
||||
try {
|
||||
[$out, $code] = $ssh->run(
|
||||
'export LC_ALL=C; if [ "$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo -n"; fi; '
|
||||
."\$SUDO systemctl {$op} {$unit} 2>&1"
|
||||
);
|
||||
|
||||
return ['ok' => $code === 0, 'output' => trim($out)];
|
||||
} finally {
|
||||
$ssh->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// ── parsers ─────────────────────────────────────────────────────────
|
||||
|
||||
private function lines(string $body): array
|
||||
|
|
|
|||
Loading…
Reference in New Issue