You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
104 lines
5.4 KiB
104 lines
5.4 KiB
package controlplane
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const restartCooldown = 60 * time.Second
|
|
|
|
// AgentRestart is the safe lifecycle state rendered by the controller. It
|
|
// contains no remote process details or credentials.
|
|
type AgentRestart struct {
|
|
RequestID string `json:"requestID"`
|
|
AgentID string `json:"agentID"`
|
|
Status string `json:"status"`
|
|
RequestedAt string `json:"requestedAt"`
|
|
ConfirmedAt string `json:"confirmedAt"`
|
|
Failure string `json:"failure"`
|
|
}
|
|
|
|
// RestartAgent sends an authenticated command over the existing paired WSS
|
|
// session. The controller deliberately never starts or terminates a remote
|
|
// machine process itself.
|
|
func (c *ControlPlane) RestartAgent(agentID string) (AgentRestart, error) {
|
|
agentID = strings.TrimSpace(agentID)
|
|
if agentID == "" {
|
|
return AgentRestart{}, errors.New("Agent 标识无效")
|
|
}
|
|
c.expireStaleAgents()
|
|
var status string
|
|
if err := c.db.QueryRow("SELECT status FROM managed_agents WHERE agent_id = ?", agentID).Scan(&status); err != nil {
|
|
return AgentRestart{}, errors.New("未找到 Agent")
|
|
}
|
|
client := c.clientFor(agentID)
|
|
if status != "online" || client == nil {
|
|
c.recordAudit("remote.restart_rejected", "controller", "agent:"+agentID, "failed", "目标 Agent 当前离线,无法通过已认证连接发送重启指令。")
|
|
return AgentRestart{}, errors.New("目标 Agent 当前离线,无法重启;请等待其重新连接")
|
|
}
|
|
var existing AgentRestart
|
|
err := c.db.QueryRow("SELECT request_id, agent_id, status, requested_at, COALESCE(confirmed_at, ''), COALESCE(failure, '') FROM agent_restart_operations WHERE agent_id = ?", agentID).Scan(&existing.RequestID, &existing.AgentID, &existing.Status, &existing.RequestedAt, &existing.ConfirmedAt, &existing.Failure)
|
|
if err == nil {
|
|
if existing.Status == "restarting" || existing.Status == "sent" {
|
|
return existing, nil // idempotent repeated click; do not send another command.
|
|
}
|
|
if existing.Status == "reconnected" {
|
|
if requested, parseErr := time.Parse(time.RFC3339Nano, existing.RequestedAt); parseErr == nil && time.Since(requested) < restartCooldown {
|
|
return existing, errors.New("该 Agent 刚完成重启,请稍后再试")
|
|
}
|
|
}
|
|
}
|
|
requestID, err := randomHex(16)
|
|
if err != nil {
|
|
return AgentRestart{}, err
|
|
}
|
|
now := time.Now().UTC().Format(time.RFC3339Nano)
|
|
op := AgentRestart{RequestID: requestID, AgentID: agentID, Status: "restarting", RequestedAt: now}
|
|
if _, err := c.db.Exec(`INSERT INTO agent_restart_operations(request_id, agent_id, status, requested_at, confirmed_at, failure)
|
|
VALUES (?, ?, ?, ?, '', '') ON CONFLICT(agent_id) DO UPDATE SET request_id=excluded.request_id, status=excluded.status, requested_at=excluded.requested_at, confirmed_at='', failure=''`, requestID, agentID, op.Status, now); err != nil {
|
|
return AgentRestart{}, fmt.Errorf("无法记录重启操作: %w", err)
|
|
}
|
|
c.recordAudit("remote.restart_requested", "controller", "agent:"+agentID, "restarting", "已通过认证控制连接请求 Agent 重启。")
|
|
if err := client.send(map[string]any{"type": "agent_restart", "schema_version": protocolVersion, "request_id": requestID, "sent_at": now}); err != nil {
|
|
_, _ = c.db.Exec("UPDATE agent_restart_operations SET status='failed', failure=? WHERE agent_id=? AND request_id=?", "重启指令未发送成功。", agentID, requestID)
|
|
c.recordAudit("remote.restart_delivery", "controller", "agent:"+agentID, "failed", "重启指令未发送成功。")
|
|
return AgentRestart{}, errors.New("重启指令未发送成功,请刷新后重试")
|
|
}
|
|
c.recordAudit("remote.restart_delivery", "controller", "agent:"+agentID, "sent", "重启指令已下发,等待 Agent 确认。")
|
|
return op, nil
|
|
}
|
|
|
|
func (c *ControlPlane) applyRestartAck(agentID string, message map[string]any) {
|
|
requestID, _ := message["request_id"].(string)
|
|
status, _ := message["status"].(string)
|
|
errorText, _ := message["error"].(string)
|
|
if requestID == "" || (status != "accepted" && status != "restarting" && status != "failed") {
|
|
return
|
|
}
|
|
var expected string
|
|
if c.db.QueryRow("SELECT request_id FROM agent_restart_operations WHERE agent_id=?", agentID).Scan(&expected) != nil || expected != requestID {
|
|
return
|
|
}
|
|
if status == "failed" {
|
|
detail := limit(sanitizeDisplayText(errorText), 1000)
|
|
if detail == "" {
|
|
detail = "Agent 未能启动替代进程。"
|
|
}
|
|
_, _ = c.db.Exec("UPDATE agent_restart_operations SET status='failed', confirmed_at=?, failure=? WHERE agent_id=?", time.Now().UTC().Format(time.RFC3339Nano), detail, agentID)
|
|
c.recordAudit("remote.restart_confirmed", "agent:"+agentID, "agent:"+agentID, "failed", detail)
|
|
return
|
|
}
|
|
_, _ = c.db.Exec("UPDATE agent_restart_operations SET status='restarting', confirmed_at=?, failure='' WHERE agent_id=?", time.Now().UTC().Format(time.RFC3339Nano), agentID)
|
|
c.recordAudit("remote.restart_confirmed", "agent:"+agentID, "agent:"+agentID, "restarting", "Agent 已确认正在安全结束任务并重启。")
|
|
}
|
|
|
|
func (c *ControlPlane) markRestartReconnected(agentID string) {
|
|
result, err := c.db.Exec("UPDATE agent_restart_operations SET status='reconnected', confirmed_at=?, failure='' WHERE agent_id=? AND status IN ('sent', 'restarting')", time.Now().UTC().Format(time.RFC3339Nano), agentID)
|
|
if err == nil {
|
|
if changed, _ := result.RowsAffected(); changed == 1 {
|
|
c.recordAudit("remote.restart_reconnected", "agent:"+agentID, "agent:"+agentID, "reconnected", "替代 Agent 已重新通过认证并恢复连接。")
|
|
}
|
|
}
|
|
}
|
|
|