Files
aiops/backend/controller/server.go
T
2026-07-09 14:16:56 +08:00

468 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controller
import (
"context"
"encoding/json"
"net/http"
"strconv"
"time"
"ezops/model"
"ezops/pkg/crypto"
ezredis "ezops/pkg/redis"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ServerHandler 服务器处理器
type ServerHandler struct {
db *gorm.DB
cache ezredis.Cache
crypt *crypto.SecretBox
}
// NewServerHandler 创建服务器处理器
func NewServerHandler(db *gorm.DB, masterKey string, cache ezredis.Cache) *ServerHandler {
return &ServerHandler{db: db, crypt: crypto.NewSecretBox(masterKey), cache: cache}
}
// recordServerAudit 记录服务器操作审计日志 // Logan.Li
func (h *ServerHandler) recordServerAudit(c *gin.Context, action, resource string, resourceID string, status int, errMsg string) {
userID, _ := c.Get("userID")
username, _ := c.Get("username")
uid, _ := userID.(uint)
uname, _ := username.(string)
h.db.Create(&model.AuditLog{
UserID: &uid,
Username: uname,
IP: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
Module: "server",
Action: action,
Resource: resource,
ResourceID: resourceID,
Status: status,
Error: errMsg,
})
}
// ListServers 获取服务器列表
// @Summary 获取服务器列表
// @Description 获取所有服务器列表
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Param status query string false "状态过滤"
// @Param region_id query int false "区域ID"
// @Success 200 {object} model.Response{data=model.ListResponse}
// @Router /servers [get]
func (h *ServerHandler) ListServers(c *gin.Context) {
var query model.PageQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
status := c.Query("status")
regionID := c.Query("region_id")
// 构建缓存键(按查询参数区分)
cacheKey := "servers:list:" + query.Keyword + ":" + status + ":" + regionID +
":" + strconv.Itoa(query.Page) + ":" + strconv.Itoa(query.PageSize)
const cacheTTL = 10 * time.Second
// 尝试从 Redis 缓存读取
if h.cache != nil {
if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" {
var resp model.Response
if json.Unmarshal([]byte(cached), &resp) == nil {
c.JSON(http.StatusOK, resp)
return
}
}
}
db := h.db.Model(&model.Server{})
// 关键词搜索
if query.Keyword != "" {
db = db.Where("alias LIKE ? OR cloud_instance_name LIKE ? OR hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ? OR cloud_instance_id LIKE ?",
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
// 状态过滤
if status := c.Query("status"); status != "" {
db = db.Where("status = ?", status)
}
// 区域过滤
if regionID := c.Query("region_id"); regionID != "" {
db = db.Where("region_id = ?", regionID)
}
var total int64
db.Count(&total)
var servers []model.Server
db.Preload("Region").Preload("Cloud").
Offset(query.GetOffset()).Limit(query.GetLimit()).
Order("created_at DESC").
Find(&servers)
// 填充 has_auth 字段(密码和SSH密钥在JSON中隐藏,但前端需知道是否有凭据) // Logan.Li
for i := range servers {
normalizeServerRemoteAccess(&servers[i])
servers[i].HasAuth = servers[i].Password != "" || servers[i].SSHKey != ""
}
resp := model.Response{
Code: 200,
Message: "success",
Data: model.ListResponse{
List: servers,
Total: total,
},
}
// 写入 Redis 缓存(异步,缓存失败不影响响应)
if h.cache != nil {
if data, err := json.Marshal(resp); err == nil {
_ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL)
}
}
c.JSON(http.StatusOK, resp)
}
// GetServer 获取服务器详情
// @Summary 获取服务器详情
// @Description 获取指定服务器的详细信息
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Success 200 {object} model.Response{data=model.Server}
// @Router /servers/{id} [get]
func (h *ServerHandler) GetServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var server model.Server
if err := h.db.Preload("Region").Preload("Cloud").First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
normalizeServerRemoteAccess(&server)
server.HasAuth = server.Password != "" || server.SSHKey != ""
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: server,
})
}
// CreateServer 创建服务器
// @Summary 创建服务器
// @Description 创建新服务器
// @Tags 服务器
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body model.CreateServerRequest true "创建请求"
// @Success 200 {object} model.Response{data=model.Server}
// @Router /servers [post]
func (h *ServerHandler) CreateServer(c *gin.Context) {
var req model.CreateServerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
server := model.Server{
Hostname: req.Hostname,
Alias: req.Alias,
CloudInstanceName: req.CloudInstanceName,
CloudInstanceID: req.CloudInstanceID,
IP: req.IP,
PublicIP: req.PublicIP,
Port: req.Port,
Username: req.Username,
Password: req.Password,
SSHKey: req.SSHKey,
AuthType: req.AuthType,
OS: req.OS,
CPU: req.CPU,
Memory: req.Memory,
Disk: req.Disk,
RegionID: req.RegionID,
CloudID: req.CloudID,
Tags: req.Tags,
Description: req.Description,
Status: "stopped",
}
if req.Status != "" {
server.Status = req.Status
}
if server.Port == 0 {
server.Port = 22
}
if server.Username == "" {
server.Username = "root"
}
if server.AuthType == "" {
server.AuthType = "password"
}
// 关联 SSH 凭证:解密后覆盖认证字段(同 AssignCredential,避免单台 CRUD 忽略 credential_id) // Logan.Li
if req.CredentialID != nil && *req.CredentialID > 0 {
var cred model.Credential
if err := h.db.First(&cred, *req.CredentialID).Error; err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "凭据不存在"})
return
}
plain, err := h.crypt.Decrypt(cred.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"})
return
}
applyCredential(cred, plain, &server)
}
if err := h.db.Create(&server).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "创建服务器失败",
})
return
}
h.recordServerAudit(c, "create", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "创建成功",
Data: server,
})
}
// UpdateServer 更新服务器
// @Summary 更新服务器
// @Description 更新指定服务器信息
// @Tags 服务器
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Param request body model.UpdateServerRequest true "更新请求"
// @Success 200 {object} model.Response{data=model.Server}
// @Router /servers/{id} [put]
func (h *ServerHandler) UpdateServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req model.UpdateServerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var server model.Server
if err := h.db.First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
updates := map[string]interface{}{}
if req.Hostname != "" {
updates["hostname"] = req.Hostname
}
if req.Alias != nil {
updates["alias"] = *req.Alias
}
if req.CloudInstanceName != "" {
updates["cloud_instance_name"] = req.CloudInstanceName
}
if req.CloudInstanceID != "" {
updates["cloud_instance_id"] = req.CloudInstanceID
}
if req.IP != "" {
updates["ip"] = req.IP
}
if req.PublicIP != "" {
updates["public_ip"] = req.PublicIP
}
if req.Port > 0 {
updates["port"] = req.Port
}
if req.Username != "" {
updates["username"] = req.Username
}
if req.Password != "" {
updates["password"] = req.Password
}
if req.SSHKey != "" {
updates["ssh_key"] = req.SSHKey
}
if req.AuthType != "" {
updates["auth_type"] = req.AuthType
}
// 关联 SSH 凭证:解密后覆盖认证字段(applyCredential 填充已 load 的 server,再同步到 updates) // Logan.Li
if req.CredentialID != nil && *req.CredentialID > 0 {
var cred model.Credential
if err := h.db.First(&cred, *req.CredentialID).Error; err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "凭据不存在"})
return
}
plain, err := h.crypt.Decrypt(cred.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"})
return
}
applyCredential(cred, plain, &server)
updates["ssh_key"] = server.SSHKey
updates["password"] = server.Password
updates["auth_type"] = server.AuthType
updates["username"] = server.Username
updates["port"] = server.Port
}
if req.OS != "" {
updates["os"] = req.OS
}
if req.CPU > 0 {
updates["cpu"] = req.CPU
}
if req.Memory > 0 {
updates["memory"] = req.Memory
}
if req.Disk > 0 {
updates["disk"] = req.Disk
}
if req.Status != "" {
updates["status"] = req.Status
}
if req.RegionID != nil {
updates["region_id"] = *req.RegionID
}
if req.CloudID != nil {
updates["cloud_id"] = *req.CloudID
}
if req.Tags != "" {
updates["tags"] = req.Tags
}
if req.Description != "" {
updates["description"] = req.Description
}
if err := h.db.Model(&server).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.Preload("Region").Preload("Cloud").First(&server, id)
h.recordServerAudit(c, "update", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "更新成功",
Data: server,
})
}
// DeleteServer 删除服务器
// @Summary 删除服务器
// @Description 删除指定服务器
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Success 200 {object} model.Response
// @Router /servers/{id} [delete]
func (h *ServerHandler) DeleteServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var server model.Server
if err := h.db.First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
// 硬删除:先清理关联数据,再物理删除服务器记录
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.AgentReport{})
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.MetricsAggregate{})
if err := h.db.Unscoped().Delete(&model.Server{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "删除失败",
})
return
}
h.recordServerAudit(c, "delete", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "服务器已彻底删除",
})
}
// GetServerStats 获取服务器统计
// @Summary 获取服务器统计
// @Description 获取服务器统计数据
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Success 200 {object} model.Response{data=model.ServerStats}
// @Router /servers/stats [get]
func (h *ServerHandler) GetServerStats(c *gin.Context) {
var stats model.ServerStats
h.db.Model(&model.Server{}).Count(&stats.Total)
h.db.Model(&model.Server{}).Where("status = ?", "running").Count(&stats.Running)
h.db.Model(&model.Server{}).Where("status = ?", "stopped").Count(&stats.Stopped)
h.db.Model(&model.Server{}).Where("status = ?", "maintenance").Count(&stats.Maintenance)
h.db.Model(&model.Server{}).Where("status = ?", "error").Count(&stats.Error)
var avgCPU, avgMemory, avgDisk float64
h.db.Model(&model.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
h.db.Model(&model.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
h.db.Model(&model.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk)
stats.AvgCPU = avgCPU
stats.AvgMemory = avgMemory
stats.AvgDisk = avgDisk
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: stats,
})
}