snapshot: AlertCenter拆分前快照

This commit is contained in:
attacker committed 2026-06-15 13:01:56 +08:00
1 parent 6a40956afa
commit 805cb767a8
23 files changed
+1560 -404

No files matched your search

Binary file not shown.
+122 -30
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# AIOps Agent 一键安装脚本 — 支持 RHEL/CentOS/Rocky/Alma/Amazon/Aliyun/Debian/Ubuntu 全系列
# 用法: curl -fsSL http://YOUR_SERVER/agent/install.sh | bash -s -- --server http://YOUR_SERVER:8080 --token TOKEN
# AIOps Agent 一键安装脚本 — 支持 Linux(systemd) 和 macOS(launchd)
# 用法: curl -fsSL http://YOUR_SERVER/agent/install.sh | sudo bash -s -- --server http://YOUR_SERVER:8080 --token TOKEN
# 署名 Logan.Li
set -e
@@ -10,8 +10,24 @@ AIOPS_SERVER=""
AGENT_TOKEN=""
INSTALL_DIR="/usr/local/bin"
SERVICE_NAME="aiops-agent"
PLIST_LABEL="com.ezops.aiops-agent"
ARCH=$(uname -m)
OS=$(cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d'"' -f2 || echo "unknown")
KERNEL=$(uname -s) # Linux 或 Darwin
# 检测 OS 显示名
detect_os() {
if [ "$KERNEL" = "Darwin" ]; then
echo "$(sw_vers -productName 2>/dev/null || echo 'macOS') $(sw_vers -productVersion 2>/dev/null || echo '')"
elif [ -f /etc/os-release ]; then
. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-${NAME:-Linux}}" || echo "Linux"
else
echo "Linux"
fi
}
OS_DISPLAY=$(detect_os)
# OS 小写 (linux / darwin)
OS_LOWER=$(echo "$KERNEL" | tr '[:upper:]' '[:lower:]')
# 解析命令行参数
while [[ $# -gt 0 ]]; do
@@ -30,14 +46,17 @@ echo " ╔═══════════════════════
echo " ║ AIOps Agent 一键安装 ║"
echo " ╚══════════════════════════════════════════╝"
echo ""
echo " 系统: ${OS} (${ARCH})"
echo " 系统: ${OS_DISPLAY} (${ARCH})"
echo " 内核: ${KERNEL}"
echo " 版本: ${AGENT_VERSION}"
echo " 后端: ${AIOPS_SERVER:-未指定}"
echo ""
# 检查 root 权限
if [ "$EUID" -ne 0 ]; then
echo "❌ 请使用 root 用户或 sudo 执行此脚本"
if [ "$(id -u)" -ne 0 ]; then
echo "❌ 请使用 root 或 sudo 执行此脚本"
echo " 推荐用法: curl -fsSL <url> | sudo bash -s -- --server <url> --token <token>"
echo " (sudo 放在 bash 前,不是 curl 前)"
exit 1
fi
@@ -50,10 +69,10 @@ case "$ARCH" in
exit 1
;;
esac
echo " 匹配: linux/${DOWNLOAD_ARCH}"
echo " 匹配: ${OS_LOWER}/${DOWNLOAD_ARCH}"
echo ""
# 安装依赖工具(curl 在大多数发行版自带,这里确保有)
# 安装依赖工具
if command -v curl &> /dev/null; then
echo "✓ curl 已安装"
else
@@ -64,6 +83,8 @@ else
dnf install -y curl
elif command -v apt-get &> /dev/null; then
apt-get update -qq && apt-get install -y curl
elif command -v brew &> /dev/null; then
brew install curl
else
echo "❌ 无法确定包管理器,请手动安装 curl"
exit 1
@@ -76,11 +97,15 @@ if [ -z "$AIOPS_SERVER" ]; then
exit 1
fi
BINARY_URL="${AIOPS_SERVER}/agent/aiops-agent-linux-${DOWNLOAD_ARCH}"
BINARY_URL="${AIOPS_SERVER}/agent/aiops-agent-${OS_LOWER}-${DOWNLOAD_ARCH}"
echo "→ 下载 Agent: ${BINARY_URL}"
curl -fSL -o /tmp/aiops-agent "${BINARY_URL}" || {
echo "❌ 下载失败,请检查后端地址和网络"
echo "❌ 下载失败: ${BINARY_URL}"
echo " 请检查: 1) 后端地址是否正确 2) 该平台是否有对应二进制"
if [ "$OS_LOWER" = "darwin" ]; then
echo " 提示: macOS 二进制需要后端编译 darwin 版本 (make build-darwin-arm64)"
fi
exit 1
}
chmod +x /tmp/aiops-agent
@@ -88,13 +113,79 @@ mv /tmp/aiops-agent "${INSTALL_DIR}/aiops-agent"
echo "✓ 安装到 ${INSTALL_DIR}/aiops-agent"
# 配置 systemd 服务
if [ -z "$AGENT_TOKEN" ]; then
echo "⚠ 未提供 --token,使用空令牌"
AGENT_TOKEN="none"
fi
cat > /etc/systemd/system/${SERVICE_NAME}.service << EOF
# ─── 平台分支: Linux(systemd) / macOS(launchd) ───
if [ "$OS_LOWER" = "darwin" ]; then
# ===== macOS: launchd 服务 =====
PLIST_PATH="/Library/LaunchDaemons/${PLIST_LABEL}.plist"
mkdir -p "$(dirname "$PLIST_PATH")"
cat > "$PLIST_PATH" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${PLIST_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>${INSTALL_DIR}/aiops-agent</string>
<string>-server</string>
<string>${AIOPS_SERVER}</string>
<string>-interval</string>
<string>10</string>
<string>-token</string>
<string>${AGENT_TOKEN}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/aiops-agent.log</string>
<key>StandardErrorPath</key>
<string>/var/log/aiops-agent.err.log</string>
</dict>
</plist>
EOF
echo "✓ 创建 launchd 服务 ($PLIST_PATH)"
# 停止旧实例(如果存在)
launchctl unload "$PLIST_PATH" 2>/dev/null || true
# 加载并启动
launchctl load -w "$PLIST_PATH"
sleep 2
# 检查运行状态
if launchctl list | grep -q "$PLIST_LABEL"; then
echo ""
echo "✅ 安装成功!Agent 已启动并设置为开机自启"
echo ""
echo " 查看状态: launchctl list | grep aiops"
echo " 查看日志: tail -f /var/log/aiops-agent.log"
echo " 手动停止: launchctl unload $PLIST_PATH"
echo " 手动启动: launchctl load -w $PLIST_PATH"
echo " 卸载: launchctl unload $PLIST_PATH && rm -f ${INSTALL_DIR}/aiops-agent $PLIST_PATH"
echo ""
else
echo "❌ Agent 启动失败,请检查日志:"
echo " cat /var/log/aiops-agent.err.log"
exit 1
fi
else
# ===== Linux: systemd 服务 =====
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=AIOps Monitoring Agent
After=network-online.target
@@ -111,25 +202,26 @@ LimitNOFILE=65536
WantedBy=multi-user.target
EOF
echo "✓ 创建 systemd 服务"
echo "✓ 创建 systemd 服务"
# 启动服务
systemctl daemon-reload
systemctl enable ${SERVICE_NAME}
systemctl restart ${SERVICE_NAME}
# 启动服务
systemctl daemon-reload
systemctl enable ${SERVICE_NAME}
systemctl restart ${SERVICE_NAME}
sleep 2
sleep 2
if systemctl is-active --quiet ${SERVICE_NAME}; then
echo ""
echo "✅ 安装成功!Agent 已启动并设置为开机自启"
echo ""
echo " 查看状态: systemctl status ${SERVICE_NAME}"
echo " 查看日志: journalctl -u ${SERVICE_NAME} -f"
echo " 卸载: ${0} --uninstall || systemctl stop ${SERVICE_NAME} && rm -f ${INSTALL_DIR}/aiops-agent /etc/systemd/system/${SERVICE_NAME}.service && systemctl daemon-reload"
echo ""
else
echo "❌ Agent 启动失败,请检查日志:"
echo " journalctl -u ${SERVICE_NAME} -n 20"
exit 1
if systemctl is-active --quiet ${SERVICE_NAME}; then
echo ""
echo "✅ 安装成功!Agent 已启动并设置为开机自启"
echo ""
echo " 查看状态: systemctl status ${SERVICE_NAME}"
echo " 查看日志: journalctl -u ${SERVICE_NAME} -f"
echo " 卸载: systemctl stop ${SERVICE_NAME} && rm -f ${INSTALL_DIR}/aiops-agent ${SERVICE_FILE} && systemctl daemon-reload"
echo ""
else
echo "❌ Agent 启动失败,请检查日志:"
echo " journalctl -u ${SERVICE_NAME} -n 20"
exit 1
fi
fi
+36
View File
@@ -349,3 +349,39 @@ func (h *K8sHandler) ListIngresses(c *gin.Context) {
Data: ingresses,
})
}
// GetTopology 获取集群拓扑布局 // Logan.Li
func (h *K8sHandler) GetTopology(c *gin.Context) {
clusterID := c.Param("id")
var layout models.TopologyLayout
result := h.db.Where("cluster_id = ?", clusterID).First(&layout)
if result.Error != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: &models.TopologyLayout{}})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: layout})
}
// SaveTopology 保存集群拓扑布局 // Logan.Li
func (h *K8sHandler) SaveTopology(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
var req struct {
LayoutData string `json:"layout_data"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
return
}
var layout models.TopologyLayout
h.db.Where("cluster_id = ?", clusterID).First(&layout)
layout.ClusterID = uint(clusterID)
layout.LayoutData = req.LayoutData
if layout.ID == 0 {
h.db.Create(&layout)
} else {
h.db.Save(&layout)
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "保存成功"})
}
+168
View File
@@ -0,0 +1,168 @@
package handlers
import (
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
)
// CreateRole 创建角色 // Logan.Li
// POST /api/v1/admin/roles
func (h *UserHandler) CreateRole(c *gin.Context) {
var req models.CreateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
// 名称 / 编码唯一性校验
var existing models.Role
if err := h.db.Where("name = ? OR code = ?", req.Name, req.Code).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "角色名称或编码已存在",
})
return
}
role := models.Role{
Name: req.Name,
Code: req.Code,
Description: req.Description,
Status: 1,
}
if err := h.db.Create(&role).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建角色失败",
})
return
}
// 关联权限
if len(req.PermissionIDs) > 0 {
var perms []models.Permission
h.db.Find(&perms, req.PermissionIDs)
h.db.Model(&role).Association("Permissions").Append(perms)
}
h.db.Preload("Permissions").First(&role, role.ID)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: role,
})
}
// UpdateRole 更新角色 // Logan.Li
// PUT /api/v1/admin/roles/:id
func (h *UserHandler) UpdateRole(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的角色 ID"})
return
}
var req models.UpdateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var role models.Role
if err := h.db.First(&role, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "角色不存在"})
return
}
updates := map[string]interface{}{}
if req.Name != "" {
// 名称 / 编码唯一性校验(排除自身)
var dup models.Role
if err := h.db.Where("(name = ? OR code = ?) AND id != ?", req.Name, req.Code, id).First(&dup).Error; err == nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "角色名称或编码已存在"})
return
}
updates["name"] = req.Name
}
if req.Code != "" {
updates["code"] = req.Code
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Status != nil {
updates["status"] = *req.Status
}
if len(updates) > 0 {
if err := h.db.Model(&role).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
}
// 替换权限关联
if req.PermissionIDs != nil {
var perms []models.Permission
if len(req.PermissionIDs) > 0 {
h.db.Find(&perms, req.PermissionIDs)
}
h.db.Model(&role).Association("Permissions").Replace(perms)
}
h.db.Preload("Permissions").First(&role, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: role,
})
}
// DeleteRole 删除角色 // Logan.Li
// DELETE /api/v1/admin/roles/:id
func (h *UserHandler) DeleteRole(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的角色 ID"})
return
}
var role models.Role
if err := h.db.First(&role, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "角色不存在"})
return
}
// 清空权限关联后再删除(避免 join 表残留)
h.db.Model(&role).Association("Permissions").Clear()
if err := h.db.Delete(&role).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ListPermissions 获取权限列表(供前端角色权限选择) // Logan.Li
// GET /api/v1/admin/permissions
func (h *UserHandler) ListPermissions(c *gin.Context) {
var perms []models.Permission
h.db.Where("status = ?", 1).Order("sort ASC, id ASC").Find(&perms)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Data: models.ListResponse{List: perms, Total: int64(len(perms))},
})
}
+1 -1
View File
@@ -341,7 +341,7 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) {
// ListRoles 获取角色列表(供前端下拉选择) // Logan.Li
func (h *UserHandler) ListRoles(c *gin.Context) {
var roles []models.Role
h.db.Where("status = ?", 1).Order("id ASC").Find(&roles)
h.db.Where("status = ?", 1).Preload("Permissions").Order("id ASC").Find(&roles)
c.JSON(http.StatusOK, models.Response{
Code: 200, Data: models.ListResponse{List: roles, Total: int64(len(roles))},
})
+16 -3
View File
@@ -26,14 +26,16 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
r.StaticFile("/agent/install.sh", "./agent/deploy/install.sh")
r.StaticFile("/agent/aiops-agent-linux-amd64", "./agent/bin/aiops-agent-linux-amd64")
r.StaticFile("/agent/aiops-agent-linux-arm64", "./agent/bin/aiops-agent-linux-arm64")
r.StaticFile("/agent/aiops-agent-darwin-arm64", "./agent/bin/aiops-agent-darwin-arm64") // macOS Apple Silicon // Logan.Li
r.GET("/agent/arch", func(c *gin.Context) {
c.JSON(200, gin.H{
"version": "1.0.0",
"downloads": gin.H{
"amd64": "/agent/aiops-agent-linux-amd64",
"arm64": "/agent/aiops-agent-linux-arm64",
"linux-amd64": "/agent/aiops-agent-linux-amd64",
"linux-arm64": "/agent/aiops-agent-linux-arm64",
"darwin-arm64": "/agent/aiops-agent-darwin-arm64",
},
"install": "curl -fsSL http://<server>:8080/agent/install.sh | bash -s -- --server http://<server>:8080 --token <token>",
"install": "curl -fsSL http://<server>:8080/agent/install.sh | sudo bash -s -- --server http://<server>:8080 --token <token>",
})
})
@@ -70,6 +72,15 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
admin.DELETE("/users/:id", authHandler.DeleteUser)
admin.POST("/users/batch-import", authHandler.BatchImportUsers)
admin.GET("/users/import-template", authHandler.ImportTemplate)
// 角色管理路由 // Logan.Li
admin.GET("/roles", authHandler.ListRoles)
admin.POST("/roles", authHandler.CreateRole)
admin.PUT("/roles/:id", authHandler.UpdateRole)
admin.DELETE("/roles/:id", authHandler.DeleteRole)
// 权限列表(供角色权限选择)
admin.GET("/permissions", authHandler.ListPermissions)
}
// LDAP 配置路由
@@ -198,6 +209,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
clusters.GET("/:id/deployments", k8sHandler.ListDeployments)
clusters.GET("/:id/services", k8sHandler.ListServices)
clusters.GET("/:id/ingresses", k8sHandler.ListIngresses)
clusters.GET("/:id/topology", k8sHandler.GetTopology) // 获取拓扑布局 // Logan.Li
clusters.PUT("/:id/topology", k8sHandler.SaveTopology) // 保存拓扑布局 // Logan.Li
}
// 告警路由
+9
View File
@@ -193,3 +193,12 @@ type K8sClusterStats struct {
TotalPods int64 `json:"total_pods"`
TotalServices int64 `json:"total_services"`
}
// TopologyLayout 拓扑布局(保存节点位置 + 自定义节点 + 自定义连线) // Logan.Li
type TopologyLayout struct {
ID uint `gorm:"primarykey" json:"id"`
ClusterID uint `gorm:"uniqueIndex;not null" json:"cluster_id"`
LayoutData string `gorm:"type:text" json:"layout_data"` // JSON: {positions, customNodes, customEdges}
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+17
View File
@@ -105,6 +105,23 @@ type UpdateUserRequest struct {
RoleIDs []uint `json:"role_ids"`
}
// CreateRoleRequest 创建角色请求 // Logan.Li
type CreateRoleRequest struct {
Name string `json:"name" binding:"required,min=1,max=64"`
Code string `json:"code" binding:"required,min=1,max=64"`
Description string `json:"description"`
PermissionIDs []uint `json:"permission_ids"`
}
// UpdateRoleRequest 更新角色请求 // Logan.Li
type UpdateRoleRequest struct {
Name string `json:"name" binding:"omitempty,min=1,max=64"`
Code string `json:"code" binding:"omitempty,min=1,max=64"`
Description *string `json:"description"`
Status *int `json:"status"`
PermissionIDs []uint `json:"permission_ids"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
+1
View File
@@ -102,5 +102,6 @@ func AutoMigrate(db *gorm.DB) error {
&models.LogEntry{},
&models.SQLAuditRecord{},
&models.SQLAuditRule{},
&models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
)
}