fix: agent_version 为空 & 恢复批量卸载
- 后端上报时若 agent_version 为空则不覆盖已有版本(防止旧Agent清空版本号) - 安装/升级后写入版本号 v0.1.0(而非 installed/latest) - DB 存量 online 主机 agent_version 补填 v0.1.0 - 前端恢复批量卸载 Agent 按钮
This commit is contained in:
1 parent
502674d73a
commit
4dd3e3238d
3 files changed
+43
-11
No files matched your search
@@ -96,19 +96,30 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 直接 SQL 更新 asset_servers 监控字段
|
// 直接 SQL 更新 asset_servers 监控字段
|
||||||
|
// 若 AgentVersion 非空则同步写入,否则保留数据库中已有版本号(防止旧版Agent空字段覆盖已设置的版本)
|
||||||
updateSQL := `UPDATE asset_servers SET
|
updateSQL := `UPDATE asset_servers SET
|
||||||
cpu_usage = ?, memory_usage = ?, disk_usage = ?,
|
cpu_usage = ?, memory_usage = ?, disk_usage = ?,
|
||||||
cpu = ?, memory = ?, disk = ?,
|
cpu = ?, memory = ?, disk = ?,
|
||||||
status = ?, agent_status = ?, agent_version = ?,
|
status = ?, agent_status = ?` + func() string {
|
||||||
|
if req.AgentVersion != "" {
|
||||||
|
return `, agent_version = ?`
|
||||||
|
}
|
||||||
|
return ``
|
||||||
|
}() + `,
|
||||||
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
|
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
|
||||||
updated_at = NOW()`
|
updated_at = NOW()`
|
||||||
args := []interface{}{
|
args := []interface{}{
|
||||||
req.CPU.UsagePercent, req.Memory.UsagePercent, diskAvg,
|
req.CPU.UsagePercent, req.Memory.UsagePercent, diskAvg,
|
||||||
req.CPU.CoreCount, memMB, diskGB,
|
req.CPU.CoreCount, memMB, diskGB,
|
||||||
"running", "online", req.AgentVersion,
|
"running", "online",
|
||||||
|
}
|
||||||
|
if req.AgentVersion != "" {
|
||||||
|
args = append(args, req.AgentVersion)
|
||||||
|
}
|
||||||
|
args = append(args,
|
||||||
// CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END
|
// CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END
|
||||||
req.Hostname, matchIP, req.Hostname,
|
req.Hostname, matchIP, req.Hostname,
|
||||||
}
|
)
|
||||||
if req.System != nil {
|
if req.System != nil {
|
||||||
if req.System.OSVersion != "" {
|
if req.System.OSVersion != "" {
|
||||||
updateSQL += ", os = ?"
|
updateSQL += ", os = ?"
|
||||||
|
|||||||
@@ -171,10 +171,10 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
|||||||
installed := contains(verifyOut, "active") || contains(verifyOut, "running")
|
installed := contains(verifyOut, "active") || contains(verifyOut, "running")
|
||||||
|
|
||||||
if installed {
|
if installed {
|
||||||
// 更新服务器 Agent 状态
|
// 更新服务器 Agent 状态(版本号:安装成功后由 Agent 下一次上报自动更新)
|
||||||
h.db.Model(&server).Updates(map[string]interface{}{
|
h.db.Model(&server).Updates(map[string]interface{}{
|
||||||
"agent_status": "online",
|
"agent_status": "online",
|
||||||
"agent_version": "installed",
|
"agent_version": "v0.1.0",
|
||||||
"last_check_at": time.Now(),
|
"last_check_at": time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -224,7 +224,7 @@ func (h *AgentInstallHandler) installOne(server models.Server, serverURL, token
|
|||||||
if installed {
|
if installed {
|
||||||
h.db.Model(&server).Updates(map[string]interface{}{
|
h.db.Model(&server).Updates(map[string]interface{}{
|
||||||
"agent_status": "online",
|
"agent_status": "online",
|
||||||
"agent_version": "installed",
|
"agent_version": "v0.1.0",
|
||||||
"last_check_at": time.Now(),
|
"last_check_at": time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -433,7 +433,7 @@ func (h *AgentInstallHandler) BatchUpgradeAgent(c *gin.Context) {
|
|||||||
|
|
||||||
// 更新版本号
|
// 更新版本号
|
||||||
h.db.Model(&server).Updates(map[string]interface{}{
|
h.db.Model(&server).Updates(map[string]interface{}{
|
||||||
"agent_version": "latest",
|
"agent_version": "v0.1.0",
|
||||||
"last_check_at": time.Now(),
|
"last_check_at": time.Now(),
|
||||||
})
|
})
|
||||||
log.Printf("[BatchUpgrade] %s Agent 升级成功", server.Hostname)
|
log.Printf("[BatchUpgrade] %s Agent 升级成功", server.Hostname)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import {
|
import {
|
||||||
Monitor, Plus, RefreshCw, Maximize2, Minimize2, BarChart3, Clock,
|
Monitor, Plus, RefreshCw, Maximize2, Minimize2, BarChart3, Clock,
|
||||||
Server, Activity, Database, Wifi, WifiOff, ArrowUpDown, ArrowUp, ArrowDown,
|
Server, Activity, Database, Wifi, WifiOff, ArrowUpDown, ArrowUp, ArrowDown,
|
||||||
ExternalLink, Download, Rocket, PackageOpen, Loader2, Info
|
ExternalLink, Download, Rocket, PackageOpen, Loader2, Info, Trash2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -24,6 +24,7 @@ import { http } from '@/services/http';
|
|||||||
import { asList } from '@/components/agent/agentUtils';
|
import { asList } from '@/components/agent/agentUtils';
|
||||||
import { AgentWizard } from '@/components/agent/AgentWizard';
|
import { AgentWizard } from '@/components/agent/AgentWizard';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
// ═══════════ Tab 配置 ═══════════
|
// ═══════════ Tab 配置 ═══════════
|
||||||
@@ -40,6 +41,7 @@ interface AgentHost { server_id: number; hostname: string; ip: string; cpu_usage
|
|||||||
type SortField = 'hostname' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'mem_usage' | 'disk_usage' | 'region' | 'ip';
|
type SortField = 'hostname' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'mem_usage' | 'disk_usage' | 'region' | 'ip';
|
||||||
|
|
||||||
const AgentTab: React.FC = () => {
|
const AgentTab: React.FC = () => {
|
||||||
|
const _confirm = useConfirm();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [hosts, setHosts] = useState<AgentHost[]>([]);
|
const [hosts, setHosts] = useState<AgentHost[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -57,6 +59,20 @@ const AgentTab: React.FC = () => {
|
|||||||
const [batchSubmitting, setBatchSubmitting] = useState(false);
|
const [batchSubmitting, setBatchSubmitting] = useState(false);
|
||||||
// 正在执行操作的主机 ID 集合(版本列 loading)
|
// 正在执行操作的主机 ID 集合(版本列 loading)
|
||||||
const [operating, setOperating] = useState<Set<number>>(new Set());
|
const [operating, setOperating] = useState<Set<number>>(new Set());
|
||||||
|
// 批量卸载
|
||||||
|
const [batchUninstalling, setBatchUninstalling] = useState(false);
|
||||||
|
const handleBatchUninstall = async () => {
|
||||||
|
const ok = await _confirm({ title: '批量卸载 Agent', description: `确定卸载选中的 ${selIds.length} 台主机的 AIOps Agent?卸载将在后台异步执行。`, confirmText: '卸载', variant: 'destructive' });
|
||||||
|
if (!ok) return;
|
||||||
|
setBatchUninstalling(true);
|
||||||
|
try {
|
||||||
|
await agentApi.batchUninstall(selIds);
|
||||||
|
toast.success(`已提交:批量卸载 ${selIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
|
||||||
|
setSelected(new Set());
|
||||||
|
setTimeout(() => fetch(), 30000);
|
||||||
|
} catch { toast.error('提交失败'); }
|
||||||
|
finally { setBatchUninstalling(false); }
|
||||||
|
};
|
||||||
const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize));
|
const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize));
|
||||||
const fetch = async () => { try { setHosts(asList(await http.get('/agent/latest'))); } catch {} finally { setLoading(false); } };
|
const fetch = async () => { try { setHosts(asList(await http.get('/agent/latest'))); } catch {} finally { setLoading(false); } };
|
||||||
useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []);
|
useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []);
|
||||||
@@ -141,9 +157,14 @@ const AgentTab: React.FC = () => {
|
|||||||
<Rocket className="w-3 h-3" />安装 Agent
|
<Rocket className="w-3 h-3" />安装 Agent
|
||||||
</Button>
|
</Button>
|
||||||
{selAny && (
|
{selAny && (
|
||||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-blue-500/30 text-blue-400 hover:bg-blue-500/10" onClick={openBatchDialog}>
|
<>
|
||||||
<PackageOpen className="w-3 h-3" />批量部署 ({selected.size})
|
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-blue-500/30 text-blue-400 hover:bg-blue-500/10" onClick={openBatchDialog}>
|
||||||
</Button>
|
<PackageOpen className="w-3 h-3" />批量部署 ({selected.size})
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={handleBatchUninstall} disabled={batchUninstalling}>
|
||||||
|
<Trash2 className="w-3 h-3" />批量卸载 ({selected.size})
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" variant="ghost" className="h-7 text-xs gap-1" onClick={fetch}><RefreshCw className="w-3 h-3" />刷新</Button>
|
<Button size="sm" variant="ghost" className="h-7 text-xs gap-1" onClick={fetch}><RefreshCw className="w-3 h-3" />刷新</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user