setActionType($action); $log->setServer($server); $log->setUser($user); try { $ssh = $this->connect($server, $user); if ($sudoPassword !== null) { $cmd = sprintf('echo %s | sudo -S systemctl %s %s 2>&1', escapeshellarg($sudoPassword), $action, escapeshellarg($serviceName)); ['output' => $output, 'exit_code' => $exitCode] = $this->execPty($ssh, $cmd); if ($this->isWrongPassword($output)) { $log->setStatus('needs_sudo'); $log->setOutput(''); } else { $log->setStatus($exitCode === 0 ? 'success' : 'failure'); $log->setOutput($output); $server->setLastStatus('online'); } } else { $cmd = sprintf('sudo systemctl %s %s 2>&1', $action, escapeshellarg($serviceName)); $output = (string) $ssh->exec($cmd); if ($this->needsSudoPassword($output)) { $log->setStatus('needs_sudo'); $log->setOutput(''); } else { $exitCode = (int) ($ssh->getExitStatus() ?? 1); $log->setStatus($exitCode === 0 ? 'success' : 'failure'); $log->setOutput($output); $server->setLastStatus('online'); } } } catch (\Throwable $e) { $log->setStatus('failure'); $log->setOutput($e->getMessage()); } $this->entityManager->persist($log); $this->entityManager->flush(); return $log; } /** * @brief Execute une commande libre sur le serveur via SSH (terminal interactif) * * Utilisé par le terminal dans l'interface. Si la commande contient 'sudo' * et que le mot de passe est fourni, on utilise execPty() pour avoir * un vrai pseudo-terminal. Pas de log pour les commandes libres. * * @param Server $server Le serveur cible * @param string $command La commande a executer * @param string|null $sudoPassword Mot de passe sudo si la commande en a besoin * @param User|null $currentUser L'utilisateur courant (pour choisir les bons credentials) * @return array ['success' => bool, 'output' => string, 'needs_sudo' => bool, 'exit_code' => int] */ public function runCommand(Server $server, string $command, ?string $sudoPassword = null, ?User $currentUser = null): array { try { $ssh = $this->connect($server, $currentUser); $hasSudo = (bool) preg_match('/\bsudo\b/', $command); if ($sudoPassword !== null && $hasSudo) { $inner = preg_replace('/^\s*sudo\s+/', '', $command); $cmd = sprintf('echo %s | sudo -S %s 2>&1', escapeshellarg($sudoPassword), $inner); ['output' => $output, 'exit_code' => $exitCode] = $this->execPty($ssh, $cmd); if ($this->isWrongPassword($output)) { return ['success' => false, 'output' => '', 'needs_sudo' => true, 'exit_code' => 1]; } return ['success' => $exitCode === 0, 'output' => $output, 'needs_sudo' => false, 'exit_code' => $exitCode]; } $output = (string) $ssh->exec($command . ' 2>&1'); $exitCode = (int) ($ssh->getExitStatus() ?? 0); if ($this->needsSudoPassword($output)) { return ['success' => false, 'output' => '', 'needs_sudo' => true, 'exit_code' => 1]; } return ['success' => $exitCode === 0, 'output' => $output, 'needs_sudo' => false, 'exit_code' => $exitCode]; } catch (\Throwable $e) { return ['success' => false, 'output' => 'Error: ' . $e->getMessage(), 'needs_sudo' => false, 'exit_code' => 1]; } } /** * @brief Verifie si le serveur est accessible en tentant une connexion SSH rapide * * Utilise un timeout de 5 secondes pour pas bloquer l'interface trop longtemps. * Retourne 'offline' si la connexion echoue ou si une exception est lancée. * * @param Server $server Le serveur a verifier * @return string 'online' si SSH accessible, 'offline' sinon */ public function checkStatus(Server $server): string { try { $ssh = new SSH2($server->getIpAddress(), $server->getSshPort(), 5); if (!$ssh->login($server->getSshUser(), $server->getSshPassword())) { return 'offline'; } return 'online'; } catch (\Throwable) { return 'offline'; } } /** * @brief Etablit une connexion SSH sur le serveur avec les bons credentials * * Si l'utilisateur courant est un opérateur (pas admin), on utilise * ses credentials SSH personnels depuis ServerAccess. Les admins * utilisent les credentials principaux du serveur. * * @param Server $server Le serveur sur lequel se connecter * @param User|null $currentUser L'utilisateur courant ou null pour les admins * @return SSH2 L'objet de connexion SSH prêt a etre utilisé * @throws \RuntimeException Si l'authentification SSH echoue */ private function connect(Server $server, ?User $currentUser = null): SSH2 { $sshUser = $server->getSshUser(); $sshPassword = $server->getSshPassword(); if ($currentUser !== null && !$currentUser->isAdmin()) { $access = $this->accessRepo->findOneBy(['server' => $server, 'user' => $currentUser]); if ($access) { $sshUser = $access->getSshUser(); $sshPassword = $access->getSshPassword(); } } $ssh = new SSH2($server->getIpAddress(), $server->getSshPort(), 10); if (!$ssh->login($sshUser, $sshPassword)) { throw new \RuntimeException(sprintf('SSH authentication failed for user "%s".', $sshUser)); } return $ssh; } /** * @brief Execute une commande via un pseudo-terminal (PTY) pour supporter sudo * * Necesaire pour les commandes sudo car sudo a besoin d'un vrai terminal * pour afficher le prompt de mot de passe. On nettoie les codes d'echappement * ANSI de la sortie avant de la retourner. * * @param SSH2 $ssh La connexion SSH active * @param string $command La commande a executer avec le PTY * @return array ['output' => string, 'exit_code' => int] */ private function execPty(SSH2 $ssh, string $command): array { $ssh->enablePTY(); $ssh->exec($command); $ssh->setTimeout(15); $raw = (string) ($ssh->read() ?: ''); $output = preg_replace('/\x1b\[[0-9;]*[a-zA-Z]|\x1b[()][\w]|\r/', '', $raw) ?? $raw; $output = preg_replace('/^\[sudo\] password for .*?:\s*$/m', '', $output); $output = trim($output); $exitCode = (int) ($ssh->getExitStatus() ?? 0); return ['output' => $output, 'exit_code' => $exitCode]; } /** * @brief Verifie le statut de plusieurs services systemd en une seule commande * * Utilise 'systemctl is-active' avec plusieurs noms en meme temps pour * limiter le nombre de connexions SSH. Retourne 'unknown' pour tout les * services si la connexion SSH echoue. * * @param Server $server Le serveur cible * @param array $serviceNames Tableau des noms de services a verifier * @return array Tableau associatif ['service_name' => 'active'|'inactive'|'unknown'] */ public function checkServicesStatus(Server $server, array $serviceNames): array { if (empty($serviceNames)) return []; try { $ssh = $this->connect($server); $escaped = implode(' ', array_map('escapeshellarg', $serviceNames)); $output = (string) $ssh->exec("systemctl is-active $escaped 2>/dev/null"); $lines = array_values(array_filter(explode("\n", trim($output)), fn($l) => $l !== '')); $result = []; foreach (array_values($serviceNames) as $i => $name) { $result[$name] = ($lines[$i] ?? '') === 'active' ? 'active' : 'inactive'; } return $result; } catch (\Throwable) { return array_fill_keys($serviceNames, 'unknown'); } } /** * @brief Scanne les services systemd actifs sur le serveur * * Utilisé automatiquement lors de l'ajout d'un serveur pour pre-remplir * la liste des services a surveiller. Retourne un tableau vide si la * connexion SSH echoue (le serveur est quand meme crée). * * @param Server $server Le serveur a scanner * @return array Tableau des noms de services systemd actifs (sans '.service') */ public function scanServices(Server $server): array { try { $ssh = $this->connect($server); $output = (string) $ssh->exec( 'systemctl list-units --type=service --state=active --no-pager --no-legend --plain 2>/dev/null' ); $services = []; foreach (explode("\n", $output) as $line) { $line = trim($line); if (!$line) continue; $parts = preg_split('/\s+/', $line, 2); $name = $parts[0] ?? ''; if (str_ends_with($name, '.service')) { $serviceName = substr($name, 0, -8); if ($serviceName !== '') { $services[] = $serviceName; } } } return $services; } catch (\Throwable) { return []; } } /** * @brief Detecte si la sortie SSH indique que sudo demande un mot de passe * * @param string $output La sortie brute de la commande SSH * @return bool true si sudo a besoin d'un mot de passe */ private function needsSudoPassword(string $output): bool { return (bool) preg_match( '/\[sudo\] password|sudo: a password is required|sudo: a terminal is required/i', $output ); } /** * @brief Detecte si le mot de passe sudo fourni est incorrect * * @param string $output La sortie brute de la commande PTY * @return bool true si le mot de passe est mauvais */ private function isWrongPassword(string $output): bool { return (bool) preg_match( '/sorry, try again|sudo: \d+ incorrect password attempt|authentication failure/i', $output ); } }