feat: push-based monitoring with remote agent
- Add database migration 006 for agent_key, agent_installed, agent_install_path, agent_last_seen_at columns - Add Server model methods: findByAgentKey, generateAgentKey, regenerateAgentKey, updateAgentLastSeen, setAgentInstalled - Auto-generate agent_key on server creation - Create AgentService for SSH-based install/uninstall of remote agent - Create bin/agent.sh - bash agent script for remote servers - Create install/agent-install.sh and install/agent-uninstall.sh - Create install/servermanager-agent.service systemd unit - Add POST /api/metrics/push endpoint (auth via agent_key) - Add web routes for agent install/uninstall/regenerate-key - Add agent status section to server detail view with install/uninstall modals - Make MonitoringService::saveMetrics() public for reuse
This commit is contained in:
@@ -114,6 +114,8 @@ class ApiController
|
||||
$monitoringService = new MonitoringService();
|
||||
$metrics = $monitoringService->getLatestMetrics($id);
|
||||
|
||||
unset($server['agent_key'], $server['ssh_password'], $server['ssh_key']);
|
||||
|
||||
$this->view->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
@@ -296,6 +298,43 @@ class ApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function pushMetrics(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || empty($input['agent_key'])) {
|
||||
$this->view->json(['error' => 'agent_key is required'], 401);
|
||||
}
|
||||
|
||||
$serverModel = new Server();
|
||||
$server = $serverModel->findByAgentKey($input['agent_key']);
|
||||
|
||||
if (!$server || !$server['is_active']) {
|
||||
$this->view->json(['error' => 'Invalid agent key'], 401);
|
||||
}
|
||||
|
||||
$cpu = isset($input['cpu']) ? max(0, min(100, (float) $input['cpu'])) : 0;
|
||||
$ram = isset($input['ram']) ? max(0, min(100, (float) $input['ram'])) : 0;
|
||||
$disk = isset($input['disk']) ? max(0, min(100, (float) $input['disk'])) : 0;
|
||||
$load = (float) ($input['load'] ?? 0);
|
||||
$uptime = (string) ($input['uptime'] ?? '');
|
||||
|
||||
$monitoring = new MonitoringService();
|
||||
$monitoring->saveMetrics([
|
||||
'server_id' => (int) $server['id'],
|
||||
'status' => 'online',
|
||||
'cpu_usage' => $cpu,
|
||||
'ram_usage' => $ram,
|
||||
'disk_usage' => $disk,
|
||||
'load_average' => $load,
|
||||
'uptime' => $uptime,
|
||||
]);
|
||||
|
||||
$serverModel->updateAgentLastSeen((int) $server['id']);
|
||||
|
||||
$this->view->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function refreshAllMetrics(): void
|
||||
{
|
||||
$user = $this->authenticateRequest();
|
||||
|
||||
@@ -255,6 +255,40 @@ class ServerController
|
||||
]);
|
||||
}
|
||||
|
||||
public function agentInstall(int $id): void
|
||||
{
|
||||
$server = $this->requireServerAccess($id, 'manager');
|
||||
|
||||
$agentService = new \ServerManager\Services\AgentService();
|
||||
$result = $agentService->install($id);
|
||||
|
||||
$this->view->json($result);
|
||||
}
|
||||
|
||||
public function agentUninstall(int $id): void
|
||||
{
|
||||
$server = $this->requireServerAccess($id, 'manager');
|
||||
|
||||
$agentService = new \ServerManager\Services\AgentService();
|
||||
$result = $agentService->uninstall($id);
|
||||
|
||||
$this->view->json($result);
|
||||
}
|
||||
|
||||
public function agentRegenerateKey(int $id): void
|
||||
{
|
||||
$this->requireServerAccess($id, 'manager');
|
||||
|
||||
$newKey = $this->serverModel->regenerateAgentKey($id);
|
||||
|
||||
$this->auditService->log('agent_key_regenerated', 'server', $id);
|
||||
|
||||
$this->view->json([
|
||||
'success' => true,
|
||||
'agent_key' => $newKey,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConnection(int $id): void
|
||||
{
|
||||
$server = $this->requireServerAccess($id, 'viewer');
|
||||
|
||||
@@ -122,6 +122,10 @@ class Server
|
||||
$data['ssh_key'] = null;
|
||||
}
|
||||
|
||||
if (empty($data['agent_key'])) {
|
||||
$data['agent_key'] = $this->generateAgentKey();
|
||||
}
|
||||
|
||||
$data['created_at'] = date('Y-m-d H:i:s');
|
||||
$data['updated_at'] = date('Y-m-d H:i:s');
|
||||
|
||||
@@ -405,4 +409,47 @@ class Server
|
||||
[$userId]
|
||||
);
|
||||
}
|
||||
|
||||
public function findByAgentKey(string $key): ?array
|
||||
{
|
||||
$server = $this->db->fetch(
|
||||
'SELECT * FROM servers WHERE agent_key = ? AND status = \'active\'',
|
||||
[$key]
|
||||
);
|
||||
if ($server) {
|
||||
$server = $this->encryption->decryptArray($server, $this->encryptedFields);
|
||||
}
|
||||
return $server;
|
||||
}
|
||||
|
||||
public function generateAgentKey(): string
|
||||
{
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
public function regenerateAgentKey(int $id): string
|
||||
{
|
||||
$key = $this->generateAgentKey();
|
||||
$this->db->update('servers', [
|
||||
'agent_key' => $key,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
], 'id = ?', [$id]);
|
||||
return $key;
|
||||
}
|
||||
|
||||
public function updateAgentLastSeen(int $id): bool
|
||||
{
|
||||
return $this->db->update('servers', [
|
||||
'agent_last_seen_at' => date('Y-m-d H:i:s'),
|
||||
], 'id = ?', [$id]) > 0;
|
||||
}
|
||||
|
||||
public function setAgentInstalled(int $id, bool $installed, ?string $path = null): bool
|
||||
{
|
||||
return $this->db->update('servers', [
|
||||
'agent_installed' => $installed ? 1 : 0,
|
||||
'agent_install_path' => $path,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
], 'id = ?', [$id]) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
159
src/Services/AgentService.php
Normal file
159
src/Services/AgentService.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ServerManager\Services;
|
||||
|
||||
use ServerManager\Core\App;
|
||||
use ServerManager\Core\Database;
|
||||
use ServerManager\Core\Logger;
|
||||
|
||||
class AgentService
|
||||
{
|
||||
private Database $db;
|
||||
private Logger $logger;
|
||||
private string $basePath;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
$this->logger = Logger::getInstance();
|
||||
$this->basePath = dirname(__DIR__, 2);
|
||||
}
|
||||
|
||||
public function install(int $serverId): array
|
||||
{
|
||||
$server = $this->db->fetch(
|
||||
'SELECT * FROM servers WHERE id = ? AND status = \'active\' AND is_active = 1',
|
||||
[$serverId]
|
||||
);
|
||||
|
||||
if (!$server) {
|
||||
return ['success' => false, 'error' => 'Server not found or inactive'];
|
||||
}
|
||||
|
||||
$server = App::getEncryption()->decryptArray($server, ['ssh_password', 'ssh_key']);
|
||||
|
||||
$agentKey = $server['agent_key'];
|
||||
$serverUrl = rtrim((App::getConfig()['app']['url'] ?? ''), '/');
|
||||
|
||||
if (empty($serverUrl)) {
|
||||
$serverUrl = ($_SERVER['HTTPS'] ?? '' === 'on' ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||
}
|
||||
|
||||
$scriptPath = $this->basePath . '/install/agent-install.sh';
|
||||
|
||||
if (!file_exists($scriptPath)) {
|
||||
return ['success' => false, 'error' => 'Agent install script not found'];
|
||||
}
|
||||
|
||||
$script = file_get_contents($scriptPath);
|
||||
|
||||
$remoteCommand = sprintf(
|
||||
'echo %s | base64 -d | bash -s -- %s %s 2>&1',
|
||||
escapeshellarg(base64_encode($script)),
|
||||
escapeshellarg($serverUrl),
|
||||
escapeshellarg($agentKey)
|
||||
);
|
||||
|
||||
try {
|
||||
$ssh = new SSHService();
|
||||
if (!$ssh->connect($server, true)) {
|
||||
return ['success' => false, 'error' => 'SSH connection failed'];
|
||||
}
|
||||
|
||||
$result = $ssh->exec($remoteCommand, 120);
|
||||
$ssh->disconnect();
|
||||
|
||||
$output = trim($result['output']);
|
||||
$exitStatus = $result['exit_status'] ?? -1;
|
||||
|
||||
if ($exitStatus !== 0) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Agent installation failed on remote server',
|
||||
'output' => $output,
|
||||
];
|
||||
}
|
||||
|
||||
$path = '/usr/local/bin/servermanager-agent';
|
||||
|
||||
$serverModel = new \ServerManager\Models\Server();
|
||||
$serverModel->setAgentInstalled($serverId, true, $path);
|
||||
|
||||
$auditService = new AuditService();
|
||||
$auditService->log('agent_installed', 'server', $serverId, [
|
||||
'name' => $server['name'],
|
||||
'path' => $path,
|
||||
]);
|
||||
|
||||
return ['success' => true, 'path' => $path, 'output' => $output];
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error("Agent install failed for server {$serverId}: " . $e->getMessage());
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function uninstall(int $serverId): array
|
||||
{
|
||||
$server = $this->db->fetch(
|
||||
'SELECT * FROM servers WHERE id = ? AND status = \'active\' AND is_active = 1',
|
||||
[$serverId]
|
||||
);
|
||||
|
||||
if (!$server) {
|
||||
return ['success' => false, 'error' => 'Server not found or inactive'];
|
||||
}
|
||||
|
||||
$server = App::getEncryption()->decryptArray($server, ['ssh_password', 'ssh_key']);
|
||||
|
||||
$scriptPath = $this->basePath . '/install/agent-uninstall.sh';
|
||||
|
||||
if (!file_exists($scriptPath)) {
|
||||
return ['success' => false, 'error' => 'Agent uninstall script not found'];
|
||||
}
|
||||
|
||||
$script = file_get_contents($scriptPath);
|
||||
|
||||
$remoteCommand = sprintf(
|
||||
'echo %s | base64 -d | bash 2>&1',
|
||||
escapeshellarg(base64_encode($script))
|
||||
);
|
||||
|
||||
try {
|
||||
$ssh = new SSHService();
|
||||
if (!$ssh->connect($server, true)) {
|
||||
return ['success' => false, 'error' => 'SSH connection failed'];
|
||||
}
|
||||
|
||||
$result = $ssh->exec($remoteCommand, 60);
|
||||
$ssh->disconnect();
|
||||
|
||||
$output = trim($result['output']);
|
||||
$exitStatus = $result['exit_status'] ?? -1;
|
||||
|
||||
$serverModel = new \ServerManager\Models\Server();
|
||||
$serverModel->setAgentInstalled($serverId, false, null);
|
||||
|
||||
$auditService = new AuditService();
|
||||
$auditService->log('agent_uninstalled', 'server', $serverId, [
|
||||
'name' => $server['name'],
|
||||
]);
|
||||
|
||||
if ($exitStatus !== 0) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Agent uninstallation may have failed on remote server',
|
||||
'output' => $output,
|
||||
];
|
||||
}
|
||||
|
||||
return ['success' => true, 'output' => $output];
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error("Agent uninstall failed for server {$serverId}: " . $e->getMessage());
|
||||
$serverModel = new \ServerManager\Models\Server();
|
||||
$serverModel->setAgentInstalled($serverId, false, null);
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class MonitoringService
|
||||
return trim(str_replace('up ', '', $result['output']));
|
||||
}
|
||||
|
||||
private function saveMetrics(array $metrics): void
|
||||
public function saveMetrics(array $metrics): void
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user