Merge pull request 'feat: push-based monitoring with remote agent' (#35) from feat/push-monitoring-agent into master
Reviewed-on: #35
This commit was merged in pull request #35.
This commit is contained in:
83
bin/agent.sh
Normal file
83
bin/agent.sh
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ServerManager Monitoring Agent
|
||||||
|
# This script runs as a systemd service on monitored servers.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONFIG_FILE="/etc/servermanager/agent.conf"
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
SERVER_URL="${SERVER_URL:-}"
|
||||||
|
AGENT_KEY="${AGENT_KEY:-}"
|
||||||
|
INTERVAL="${INTERVAL:-60}"
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_metrics() {
|
||||||
|
CPU=$(top -bn1 2>/dev/null | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1)
|
||||||
|
[ -z "$CPU" ] && CPU=0
|
||||||
|
|
||||||
|
RAM=$(free 2>/dev/null | grep Mem | awk '{printf "%.1f", $3/$2 * 100}')
|
||||||
|
[ -z "$RAM" ] && RAM=0
|
||||||
|
|
||||||
|
DISK=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||||
|
[ -z "$DISK" ] && DISK=0
|
||||||
|
|
||||||
|
LOAD=$(cat /proc/loadavg 2>/dev/null | awk '{print $1}')
|
||||||
|
[ -z "$LOAD" ] && LOAD=0
|
||||||
|
|
||||||
|
UPTIME=$(uptime -p 2>/dev/null | sed 's/^up //')
|
||||||
|
[ -z "$UPTIME" ] && UPTIME=""
|
||||||
|
}
|
||||||
|
|
||||||
|
push_metrics() {
|
||||||
|
local payload
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"agent_key": "$AGENT_KEY",
|
||||||
|
"cpu": $CPU,
|
||||||
|
"ram": $RAM,
|
||||||
|
"disk": $DISK,
|
||||||
|
"load": $LOAD,
|
||||||
|
"uptime": "$UPTIME"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
curl -s -X POST "$SERVER_URL/api/metrics/push" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 30 \
|
||||||
|
-o /dev/null -w "%{http_code}" 2>/dev/null || echo "000"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if [ -z "$SERVER_URL" ] || [ -z "$AGENT_KEY" ]; then
|
||||||
|
echo "ERROR: SERVER_URL and AGENT_KEY must be set in $CONFIG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ServerManager Agent started"
|
||||||
|
echo "Server: $SERVER_URL"
|
||||||
|
echo "Interval: ${INTERVAL}s"
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
collect_metrics
|
||||||
|
http_code=$(push_metrics)
|
||||||
|
|
||||||
|
if [ "$http_code" = "200" ]; then
|
||||||
|
echo "[$(date)] Metrics pushed successfully"
|
||||||
|
else
|
||||||
|
echo "[$(date)] Failed to push metrics (HTTP $http_code)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
9
database/migrations/006_agent_tokens.sql
Normal file
9
database/migrations/006_agent_tokens.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE servers
|
||||||
|
ADD COLUMN agent_key VARCHAR(64) DEFAULT NULL AFTER ssh_key,
|
||||||
|
ADD COLUMN agent_installed TINYINT(1) NOT NULL DEFAULT 0 AFTER agent_key,
|
||||||
|
ADD COLUMN agent_install_path VARCHAR(255) DEFAULT NULL AFTER agent_installed,
|
||||||
|
ADD COLUMN agent_last_seen_at DATETIME DEFAULT NULL AFTER agent_install_path;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_servers_agent_key ON servers(agent_key);
|
||||||
|
|
||||||
|
UPDATE servers SET agent_key = LOWER(HEX(RANDOM_BYTES(32))) WHERE agent_key IS NULL;
|
||||||
136
install/agent-install.sh
Normal file
136
install/agent-install.sh
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ServerManager Agent Installer
|
||||||
|
# Usage: bash agent-install.sh <SERVER_URL> <AGENT_KEY> [INTERVAL]
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER_URL="${1:-}"
|
||||||
|
AGENT_KEY="${2:-}"
|
||||||
|
INTERVAL="${3:-60}"
|
||||||
|
|
||||||
|
if [ -z "$SERVER_URL" ] || [ -z "$AGENT_KEY" ]; then
|
||||||
|
echo "Usage: bash agent-install.sh <SERVER_URL> <AGENT_KEY> [INTERVAL]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
AGENT_BIN="/usr/local/bin/servermanager-agent"
|
||||||
|
CONFIG_DIR="/etc/servermanager"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/agent.conf"
|
||||||
|
SERVICE_FILE="/etc/systemd/system/servermanager-agent.service"
|
||||||
|
|
||||||
|
echo "[1/5] Creating config directory..."
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
|
||||||
|
echo "[2/5] Installing agent binary..."
|
||||||
|
cat > "$AGENT_BIN" << 'AGENTSCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
CONFIG_FILE="/etc/servermanager/agent.conf"
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
SERVER_URL="${SERVER_URL:-}"
|
||||||
|
AGENT_KEY="${AGENT_KEY:-}"
|
||||||
|
INTERVAL="${INTERVAL:-60}"
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_metrics() {
|
||||||
|
CPU=$(top -bn1 2>/dev/null | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1)
|
||||||
|
[ -z "$CPU" ] && CPU=0
|
||||||
|
RAM=$(free 2>/dev/null | grep Mem | awk '{printf "%.1f", $3/$2 * 100}')
|
||||||
|
[ -z "$RAM" ] && RAM=0
|
||||||
|
DISK=$(df / 2>/dev/null | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||||
|
[ -z "$DISK" ] && DISK=0
|
||||||
|
LOAD=$(cat /proc/loadavg 2>/dev/null | awk '{print $1}')
|
||||||
|
[ -z "$LOAD" ] && LOAD=0
|
||||||
|
UPTIME=$(uptime -p 2>/dev/null | sed 's/^up //')
|
||||||
|
[ -z "$UPTIME" ] && UPTIME=""
|
||||||
|
}
|
||||||
|
|
||||||
|
push_metrics() {
|
||||||
|
local payload
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"agent_key": "$AGENT_KEY",
|
||||||
|
"cpu": $CPU,
|
||||||
|
"ram": $RAM,
|
||||||
|
"disk": $DISK,
|
||||||
|
"load": $LOAD,
|
||||||
|
"uptime": "$UPTIME"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
curl -s -X POST "$SERVER_URL/api/metrics/push" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
--connect-timeout 10 --max-time 30 \
|
||||||
|
-o /dev/null -w "%{http_code}" 2>/dev/null || echo "000"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
load_config
|
||||||
|
if [ -z "$SERVER_URL" ] || [ -z "$AGENT_KEY" ]; then
|
||||||
|
echo "ERROR: SERVER_URL and AGENT_KEY must be set in $CONFIG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "ServerManager Agent started (interval: ${INTERVAL}s)"
|
||||||
|
sleep 5
|
||||||
|
while true; do
|
||||||
|
collect_metrics
|
||||||
|
http_code=$(push_metrics)
|
||||||
|
if [ "$http_code" = "200" ]; then
|
||||||
|
echo "[$(date)] Metrics pushed successfully"
|
||||||
|
else
|
||||||
|
echo "[$(date)] Failed to push metrics (HTTP $http_code)"
|
||||||
|
fi
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
main
|
||||||
|
AGENTSCRIPT
|
||||||
|
chmod +x "$AGENT_BIN"
|
||||||
|
|
||||||
|
echo "[3/5] Writing config..."
|
||||||
|
cat > "$CONFIG_FILE" << EOF
|
||||||
|
SERVER_URL="$SERVER_URL"
|
||||||
|
AGENT_KEY="$AGENT_KEY"
|
||||||
|
INTERVAL=$INTERVAL
|
||||||
|
EOF
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
|
||||||
|
echo "[4/5] Installing systemd service..."
|
||||||
|
cat > "$SERVICE_FILE" << 'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=ServerManager Monitoring Agent
|
||||||
|
Documentation=https://github.com/servermanager
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/servermanager-agent
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
StartLimitBurst=5
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNIT
|
||||||
|
|
||||||
|
echo "[5/5] Enabling and starting service..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable servermanager-agent
|
||||||
|
systemctl restart servermanager-agent
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "ServerManager Agent installed successfully!"
|
||||||
|
echo " Binary: $AGENT_BIN"
|
||||||
|
echo " Config: $CONFIG_FILE"
|
||||||
|
echo " Service: $SERVICE_FILE"
|
||||||
22
install/agent-uninstall.sh
Normal file
22
install/agent-uninstall.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ServerManager Agent Uninstaller
|
||||||
|
set -e
|
||||||
|
|
||||||
|
AGENT_BIN="/usr/local/bin/servermanager-agent"
|
||||||
|
CONFIG_DIR="/etc/servermanager"
|
||||||
|
SERVICE_FILE="/etc/systemd/system/servermanager-agent.service"
|
||||||
|
|
||||||
|
echo "[1/3] Stopping and disabling service..."
|
||||||
|
systemctl stop servermanager-agent 2>/dev/null || true
|
||||||
|
systemctl disable servermanager-agent 2>/dev/null || true
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
echo "[2/3] Removing files..."
|
||||||
|
rm -f "$AGENT_BIN"
|
||||||
|
rm -rf "$CONFIG_DIR"
|
||||||
|
rm -f "$SERVICE_FILE"
|
||||||
|
|
||||||
|
echo "[3/3] Reloading systemd..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
echo "ServerManager Agent uninstalled successfully."
|
||||||
21
install/servermanager-agent.service
Normal file
21
install/servermanager-agent.service
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ServerManager Monitoring Agent
|
||||||
|
Documentation=https://github.com/servermanager
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/servermanager-agent
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
StartLimitBurst=5
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -122,3 +122,8 @@ $router->get('/api/users', [ApiController::class, 'users']);
|
|||||||
$router->get('/api/profile', [ApiController::class, 'profile']);
|
$router->get('/api/profile', [ApiController::class, 'profile']);
|
||||||
$router->put('/api/profile', [ApiController::class, 'updateProfile']);
|
$router->put('/api/profile', [ApiController::class, 'updateProfile']);
|
||||||
$router->get('/api/refresh-metrics', [ApiController::class, 'refreshAllMetrics']);
|
$router->get('/api/refresh-metrics', [ApiController::class, 'refreshAllMetrics']);
|
||||||
|
$router->post('/api/metrics/push', [ApiController::class, 'pushMetrics']);
|
||||||
|
|
||||||
|
$router->post('/servers/:id/agent/install', [ServerController::class, 'agentInstall'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||||
|
$router->post('/servers/:id/agent/uninstall', [ServerController::class, 'agentUninstall'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||||
|
$router->post('/servers/:id/agent/regenerate-key', [ServerController::class, 'agentRegenerateKey'], [AuthMiddleware::class, CSRFMiddleware::class]);
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ class ApiController
|
|||||||
$monitoringService = new MonitoringService();
|
$monitoringService = new MonitoringService();
|
||||||
$metrics = $monitoringService->getLatestMetrics($id);
|
$metrics = $monitoringService->getLatestMetrics($id);
|
||||||
|
|
||||||
|
unset($server['agent_key'], $server['ssh_password'], $server['ssh_key']);
|
||||||
|
|
||||||
$this->view->json([
|
$this->view->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'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
|
public function refreshAllMetrics(): void
|
||||||
{
|
{
|
||||||
$user = $this->authenticateRequest();
|
$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
|
public function testConnection(int $id): void
|
||||||
{
|
{
|
||||||
$server = $this->requireServerAccess($id, 'viewer');
|
$server = $this->requireServerAccess($id, 'viewer');
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ class Server
|
|||||||
$data['ssh_key'] = null;
|
$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['created_at'] = date('Y-m-d H:i:s');
|
||||||
$data['updated_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]
|
[$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']));
|
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');
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,58 @@ $canManage = $server_role === 'owner' || $server_role === 'manager';
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2><i class="fas fa-wifi"></i> Monitoring Agent</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (!empty($server['agent_key'])): ?>
|
||||||
|
<dl class="details-list">
|
||||||
|
<dt>Agent Key</dt>
|
||||||
|
<dd>
|
||||||
|
<code id="agentKeyDisplay"><?= substr($server['agent_key'], 0, 12) ?>...<?= substr($server['agent_key'], -4) ?></code>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="copyAgentKey()" title="Copy full key">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<input type="hidden" id="agentKeyFull" value="<?= htmlspecialchars($server['agent_key']) ?>">
|
||||||
|
</dd>
|
||||||
|
<dt>Status</dt>
|
||||||
|
<dd>
|
||||||
|
<?php if ($server['agent_installed'] ?? false): ?>
|
||||||
|
<span class="badge badge-success">Installed</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-secondary">Not installed</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dd>
|
||||||
|
<?php if (!empty($server['agent_install_path'])): ?>
|
||||||
|
<dt>Install Path</dt>
|
||||||
|
<dd><code><?= htmlspecialchars($server['agent_install_path']) ?></code></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<dt>Last Seen</dt>
|
||||||
|
<dd><?= $server['agent_last_seen_at'] ?? 'Never' ?></dd>
|
||||||
|
</dl>
|
||||||
|
<div class="form-actions" style="margin-top:1rem">
|
||||||
|
<?php if ($canManage): ?>
|
||||||
|
<?php if ($server['agent_installed'] ?? false): ?>
|
||||||
|
<button onclick="showAgentModal('uninstall')" class="btn btn-danger">
|
||||||
|
<i class="fas fa-minus-circle"></i> Uninstall Agent
|
||||||
|
</button>
|
||||||
|
<?php else: ?>
|
||||||
|
<button onclick="showAgentModal('install')" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus-circle"></i> Install Agent
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<button onclick="regenerateAgentKey()" class="btn btn-secondary" title="Regenerate agent key">
|
||||||
|
<i class="fas fa-key"></i> Regenerate Key
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-muted">No agent key configured.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -195,6 +247,28 @@ $canManage = $server_role === 'owner' || $server_role === 'manager';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="agentConfirmModal" class="modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="agentModalTitle">Install Monitoring Agent</h3>
|
||||||
|
<button type="button" class="modal-close" onclick="closeAgentModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="agentModalMessage">
|
||||||
|
This will install the monitoring agent on <strong><?= htmlspecialchars($server['name'] ?? '') ?></strong> via SSH.
|
||||||
|
</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
The agent will run as a systemd service, collecting and sending metrics every 60 seconds.
|
||||||
|
It will automatically restart if the server reboots.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="agentModalConfirmBtn" class="btn btn-primary" onclick="confirmAgentAction()">Install</button>
|
||||||
|
<button class="btn btn-secondary" onclick="closeAgentModal()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="csrfForm" style="display:none">
|
<form id="csrfForm" style="display:none">
|
||||||
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
<input type="hidden" name="_csrf_token" value="<?= $this->csrfToken() ?>">
|
||||||
</form>
|
</form>
|
||||||
@@ -315,6 +389,107 @@ if (historicalData.length > 0) {
|
|||||||
initChart(historicalData);
|
initChart(historicalData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pendingAgentAction = null;
|
||||||
|
|
||||||
|
function showAgentModal(action) {
|
||||||
|
pendingAgentAction = action;
|
||||||
|
const modal = document.getElementById('agentConfirmModal');
|
||||||
|
const title = document.getElementById('agentModalTitle');
|
||||||
|
const msg = document.getElementById('agentModalMessage');
|
||||||
|
const btn = document.getElementById('agentModalConfirmBtn');
|
||||||
|
|
||||||
|
if (action === 'install') {
|
||||||
|
title.textContent = 'Install Monitoring Agent';
|
||||||
|
msg.innerHTML = 'This will install the monitoring agent on <strong><?= htmlspecialchars($server['name'] ?? '') ?></strong> via SSH.';
|
||||||
|
btn.textContent = 'Install';
|
||||||
|
btn.className = 'btn btn-primary';
|
||||||
|
} else {
|
||||||
|
title.textContent = 'Uninstall Monitoring Agent';
|
||||||
|
msg.innerHTML = 'This will uninstall the monitoring agent from <strong><?= htmlspecialchars($server['name'] ?? '') ?></strong> via SSH. The service will be stopped and all agent files removed.';
|
||||||
|
btn.textContent = 'Uninstall';
|
||||||
|
btn.className = 'btn btn-danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modal.onclick = function(e) {
|
||||||
|
if (e.target === modal) closeAgentModal();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAgentModal() {
|
||||||
|
document.getElementById('agentConfirmModal').style.display = 'none';
|
||||||
|
pendingAgentAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmAgentAction() {
|
||||||
|
if (!pendingAgentAction) return;
|
||||||
|
|
||||||
|
const action = pendingAgentAction;
|
||||||
|
const btn = document.getElementById('agentModalConfirmBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = action === 'install' ? 'Installing...' : 'Uninstalling...';
|
||||||
|
|
||||||
|
closeAgentModal();
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/agent/' + action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: '_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('success', action === 'install' ? 'Agent installed successfully.' : 'Agent uninstalled successfully.');
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} else {
|
||||||
|
showToast('error', data.error || (action === 'install' ? 'Installation failed.' : 'Uninstallation failed.'));
|
||||||
|
if (data.output) console.log('Output:', data.output);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
showToast('error', 'Request failed: ' + err.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = action === 'install' ? 'Install' : 'Uninstall';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyAgentKey() {
|
||||||
|
const key = document.getElementById('agentKeyFull').value;
|
||||||
|
navigator.clipboard.writeText(key).then(() => {
|
||||||
|
showToast('success', 'Agent key copied to clipboard.');
|
||||||
|
}).catch(() => {
|
||||||
|
showToast('error', 'Failed to copy agent key.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function regenerateAgentKey() {
|
||||||
|
if (!confirm('Regenerate agent key? The current key will stop working immediately.')) return;
|
||||||
|
|
||||||
|
fetch('/servers/<?= $server['id'] ?>/agent/regenerate-key', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRF-Token': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: '_csrf_token=' + encodeURIComponent(getCsrfToken()),
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showToast('success', 'Agent key regenerated.');
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
} else {
|
||||||
|
showToast('error', data.error || 'Failed to regenerate key.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => showToast('error', 'Request failed: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
function getCsrfToken() {
|
function getCsrfToken() {
|
||||||
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
return document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user