feat: Agent一键安装(loading/成功/失败状态无弹框) + 批量安装API
This commit is contained in:
1 parent
48b2d2281b
commit
2fc2a2ae07
5 files changed
+213
-5
No files matched your search
@@ -144,3 +144,113 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// installOne 安装单个服务器的 Agent(内部方法)
|
||||||
|
func (h *AgentInstallHandler) installOne(server models.Server, serverURL, token string) (bool, string, string, error) {
|
||||||
|
client, err := sshDial(server)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", "", fmt.Errorf("SSH连接失败: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if server.Status != "running" {
|
||||||
|
return false, "", "", fmt.Errorf("服务器未运行")
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd := fmt.Sprintf(
|
||||||
|
`curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token %s`,
|
||||||
|
serverURL, serverURL, token,
|
||||||
|
)
|
||||||
|
|
||||||
|
installOut, err := runSSHCommand(client, installCmd)
|
||||||
|
if err != nil {
|
||||||
|
return false, installOut, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'")
|
||||||
|
installed := contains(verifyOut, "active") || contains(verifyOut, "running")
|
||||||
|
|
||||||
|
if installed {
|
||||||
|
h.db.Model(&server).Updates(map[string]interface{}{
|
||||||
|
"agent_status": "online",
|
||||||
|
"agent_version": "installed",
|
||||||
|
"last_check_at": time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return installed, installOut, verifyOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchInstallAgent 批量安装 Agent
|
||||||
|
// POST /servers/install-agent
|
||||||
|
func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
ServerIDs []uint `json:"server_ids"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.ServerIDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供要安装的服务器 ID 列表"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverURL := fmt.Sprintf("http://%s", c.Request.Host)
|
||||||
|
token := c.Query("token")
|
||||||
|
|
||||||
|
type InstallResult struct {
|
||||||
|
ServerID uint `json:"server_id"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Installed bool `json:"installed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]InstallResult, 0, len(req.ServerIDs))
|
||||||
|
successCount := 0
|
||||||
|
|
||||||
|
for _, sid := range req.ServerIDs {
|
||||||
|
var server models.Server
|
||||||
|
if err := h.db.First(&server, sid).Error; err != nil {
|
||||||
|
results = append(results, InstallResult{
|
||||||
|
ServerID: sid,
|
||||||
|
Success: false,
|
||||||
|
Message: "服务器不存在",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, _, _, err := h.installOne(server, serverURL, token)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, InstallResult{
|
||||||
|
ServerID: server.ID,
|
||||||
|
Hostname: server.Hostname,
|
||||||
|
IP: server.IP,
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
successCount++
|
||||||
|
msg := "安装成功"
|
||||||
|
if !installed {
|
||||||
|
msg = "安装命令已执行,Agent 服务未检测到运行"
|
||||||
|
}
|
||||||
|
results = append(results, InstallResult{
|
||||||
|
ServerID: server.ID,
|
||||||
|
Hostname: server.Hostname,
|
||||||
|
IP: server.IP,
|
||||||
|
Success: true,
|
||||||
|
Installed: installed,
|
||||||
|
Message: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: fmt.Sprintf("完成 %d/%d 台服务器", successCount, len(req.ServerIDs)),
|
||||||
|
Data: gin.H{
|
||||||
|
"total": len(req.ServerIDs),
|
||||||
|
"success": successCount,
|
||||||
|
"results": results,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -141,6 +141,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
// Agent 远程安装 // Logan.Li
|
// Agent 远程安装 // Logan.Li
|
||||||
agentInstallHandler := handlers.NewAgentInstallHandler(db)
|
agentInstallHandler := handlers.NewAgentInstallHandler(db)
|
||||||
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
|
servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent)
|
||||||
|
servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 云账号路由
|
// 云账号路由
|
||||||
|
|||||||
@@ -73,8 +73,75 @@ export function ServerManager() {
|
|||||||
|
|
||||||
// Agent 安装向导状态
|
// Agent 安装向导状态
|
||||||
const [agentWizardOpen, setAgentWizardOpen] = useState(false);
|
const [agentWizardOpen, setAgentWizardOpen] = useState(false);
|
||||||
const openInstallAgent = (server: any) => {
|
// Agent 安装状态: { [serverId]: 'installing' | 'success' | 'failed' }
|
||||||
setAgentWizardOpen(true);
|
const [installingIds, setInstallingIds] = useState<Set<number>>(new Set());
|
||||||
|
const [installResults, setInstallResults] = useState<Map<number, { success: boolean; message: string }>>(new Map());
|
||||||
|
|
||||||
|
const openInstallAgent = async (server: any) => {
|
||||||
|
const token = useUserStore.getState().token;
|
||||||
|
if (!token) { toast.error('请先登录'); return; }
|
||||||
|
|
||||||
|
setInstallingIds(prev => new Set(prev).add(server.id));
|
||||||
|
try {
|
||||||
|
const res: any = await serverApi.installAgent(server.id, token);
|
||||||
|
const installed = res?.installed || false;
|
||||||
|
setInstallResults(prev => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(server.id, {
|
||||||
|
success: installed,
|
||||||
|
message: installed ? '安装成功 ✓' : '命令已执行,Agent 未检测到运行'
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
if (installed) toast.success(`${server.hostname} Agent 安装成功`);
|
||||||
|
else toast.warning(`${server.hostname}: Agent 安装命令已执行`);
|
||||||
|
} catch (e: any) {
|
||||||
|
setInstallResults(prev => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(server.id, { success: false, message: e?.message || '安装失败' });
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast.error(`${server.hostname} 安装失败: ${e?.message || '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setInstallingIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(server.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchInstallAgents = async () => {
|
||||||
|
const token = useUserStore.getState().token;
|
||||||
|
if (!token) { toast.error('请先登录'); return; }
|
||||||
|
const ids = selectedServers.map(id => Number(id));
|
||||||
|
|
||||||
|
setInstallingIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
ids.forEach(id => next.add(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await serverApi.batchInstallAgent(ids, token);
|
||||||
|
const results = res?.results || [];
|
||||||
|
setInstallResults(prev => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
results.forEach((r: any) => {
|
||||||
|
next.set(r.server_id, { success: r.installed, message: r.message });
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast.success(`批量安装完成: ${res?.success || 0}/${ids.length} 成功`);
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(`批量安装失败: ${e?.message || '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
setInstallingIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
ids.forEach(id => next.delete(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openTerminal = (server: any) => {
|
const openTerminal = (server: any) => {
|
||||||
@@ -360,6 +427,15 @@ export function ServerManager() {
|
|||||||
{selectedServers.length > 0 && (
|
{selectedServers.length > 0 && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-[var(--dg-text-secondary)]">已选择 {selectedServers.length} 项</span>
|
<span className="text-sm text-[var(--dg-text-secondary)]">已选择 {selectedServers.length} 项</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBatchInstallAgents}
|
||||||
|
className="border-[var(--dg-cyan)]/30 text-[var(--dg-cyan)] hover:bg-[var(--dg-cyan)]/10"
|
||||||
|
>
|
||||||
|
<Cpu className="h-4 w-4 mr-2" />
|
||||||
|
批量安装Agent
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -421,6 +497,8 @@ export function ServerManager() {
|
|||||||
onOpenEdit={openEditForm}
|
onOpenEdit={openEditForm}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onInstallAgent={openInstallAgent}
|
onInstallAgent={openInstallAgent}
|
||||||
|
installing={installingIds.has(server.id)}
|
||||||
|
installResult={installResults.get(server.id) || null}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Cpu, Edit, MoreHorizontal, Server as ServerIcon, Terminal, Trash2 } from "lucide-react";
|
import { Cpu, Edit, Loader2, MoreHorizontal, Server as ServerIcon, Terminal, Trash2, CheckCircle, XCircle } from "lucide-react";
|
||||||
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
import { getStatusBadge, getTags, getUsageColor } from './serverHelpers';
|
||||||
|
|
||||||
interface ServerTableRowProps {
|
interface ServerTableRowProps {
|
||||||
@@ -23,6 +23,8 @@ interface ServerTableRowProps {
|
|||||||
onOpenEdit: (server: any) => void;
|
onOpenEdit: (server: any) => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
onInstallAgent: (server: any) => void;
|
onInstallAgent: (server: any) => void;
|
||||||
|
installing?: boolean;
|
||||||
|
installResult?: { success: boolean; message: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServerTableRow({
|
export function ServerTableRow({
|
||||||
@@ -33,6 +35,8 @@ export function ServerTableRow({
|
|||||||
onOpenEdit,
|
onOpenEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onInstallAgent,
|
onInstallAgent,
|
||||||
|
installing = false,
|
||||||
|
installResult = null,
|
||||||
}: ServerTableRowProps) {
|
}: ServerTableRowProps) {
|
||||||
const cpuUsage = Number(server.cpu_usage || 0);
|
const cpuUsage = Number(server.cpu_usage || 0);
|
||||||
const memoryUsage = Number(server.memory_usage || 0);
|
const memoryUsage = Number(server.memory_usage || 0);
|
||||||
@@ -154,9 +158,20 @@ export function ServerTableRow({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
className="text-[var(--dg-text-secondary)] focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)] cursor-pointer"
|
||||||
onClick={() => onInstallAgent(server)}
|
onClick={() => onInstallAgent(server)}
|
||||||
|
disabled={installing}
|
||||||
>
|
>
|
||||||
<Cpu className="h-4 w-4 mr-2" />
|
{installing ? (
|
||||||
安装Agent
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
) : installResult ? (
|
||||||
|
installResult.success ? (
|
||||||
|
<CheckCircle className="h-4 w-4 mr-2 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-4 w-4 mr-2 text-red-400" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Cpu className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{installing ? '安装中...' : installResult ? installResult.message : '安装Agent'}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ export const serverApi = {
|
|||||||
installAgent: async (id: number | string, token: string) => {
|
installAgent: async (id: number | string, token: string) => {
|
||||||
return http.post<{ installed: boolean; arch: string; output: string }>(`/servers/${id}/install-agent?token=${encodeURIComponent(token)}`);
|
return http.post<{ installed: boolean; arch: string; output: string }>(`/servers/${id}/install-agent?token=${encodeURIComponent(token)}`);
|
||||||
},
|
},
|
||||||
|
// 批量远程安装 Agent
|
||||||
|
batchInstallAgent: async (ids: number[], token: string) => {
|
||||||
|
return http.post<{ total: number; success: number; results: any[] }>(`/servers/install-agent?token=${encodeURIComponent(token)}`, { server_ids: ids });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== K8s 集群 API ====================
|
// ==================== K8s 集群 API ====================
|
||||||
|
|||||||
Reference in new issue
Block a user