功能:扩展云管资源运维、账号授权及监控管理

This commit is contained in:
songyiming committed 2026-11-10 17:52:52 +08:00
1 parent eef485306f
commit 6134d49c16
87 files changed
+29498 -126

No files matched your search

+86 -8
View File
@@ -12,6 +12,8 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"vdi-cloud/platform/internal/config"
"vdi-cloud/platform/internal/model"
@@ -20,10 +22,13 @@ import (
)
type Server struct {
Config config.Config
DB *sqlx.DB
PVE *proxmox.Client
Router *gin.Engine
Config config.Config
DB *sqlx.DB
PVE *proxmox.Client
Router *gin.Engine
computeMu sync.Mutex
computeRevision string
computeRuntime atomic.Pointer[proxmox.Client]
}
type claims struct {
AdminID string `json:"adminId"`
@@ -34,6 +39,7 @@ func New(cfg config.Config, db *sqlx.DB, pve *proxmox.Client) *Server {
gin.SetMode(gin.ReleaseMode)
s := &Server{Config: cfg, DB: db, PVE: pve}
r := gin.New()
_ = r.SetTrustedProxies(cfg.TrustedProxies)
r.Use(gin.Recovery(), func(c *gin.Context) {
c.Set("requestId", store.ID("req_"))
c.Header("X-Request-ID", c.GetString("requestId"))
@@ -56,32 +62,90 @@ func New(cfg config.Config, db *sqlx.DB, pve *proxmox.Client) *Server {
t := r.Group("/api/v1/terminal", s.deviceAuth)
t.POST("/heartbeat", s.heartbeat)
t.GET("/config", s.deviceConfig)
t.POST("/config/ack", s.policyAck)
t.GET("/desktops", s.deviceDesktops)
t.GET("/commands", s.deviceCommands)
t.POST("/commands/:id/ack", s.deviceAck)
t.POST("/connect-ticket", s.createTicket)
t.POST("/connect-ticket/consume", s.consumeTicket)
t.POST("/session", s.deviceSession)
a := r.Group("/api/v1", s.adminAuth)
a := r.Group("/api/v1", s.adminAuth, s.refreshComputeRequest)
a.GET("/auth/me", func(c *gin.Context) { c.JSON(200, c.MustGet("admin")) })
a.POST("/auth/logout", s.logout)
a.POST("/auth/password", s.changeOwnPassword)
a.GET("/administrators", s.allow("super_admin"), s.administratorList)
a.POST("/administrators", s.allow("super_admin"), s.createAdministrator)
a.PUT("/administrators/:id", s.allow("super_admin"), s.updateAdministrator)
a.POST("/administrators/:id/password", s.allow("super_admin"), s.resetAdministratorPassword)
a.GET("/overview", s.overview)
a.GET("/sessions/history", s.sessionHistory)
a.GET("/monitor/series", s.resourceSeries)
a.GET("/monitor/rules", s.alertRules)
a.POST("/monitor/rules", s.allow("super_admin"), s.saveAlertRule)
a.PUT("/monitor/rules/:id", s.allow("super_admin"), s.saveAlertRule)
a.GET("/monitor/alerts", s.alerts)
a.POST("/monitor/alerts/:id/acknowledge", s.allow("super_admin", "operator"), s.acknowledgeAlert)
a.POST("/imports/preview", s.allow("super_admin"), s.previewImport)
a.POST("/imports/commit", s.allow("super_admin"), s.commitImport)
a.GET("/settings", s.settings)
a.GET("/settings/pve", s.allow("super_admin"), s.getPVEConnection)
a.POST("/settings/pve/check", s.allow("super_admin"), s.checkPVEConnection)
a.PUT("/settings/pve", s.allow("super_admin"), s.savePVEConnection)
a.GET("/desktops/:id/hardware", s.desktopHardware)
a.GET("/desktops/:id/snapshots", s.snapshotView)
a.POST("/desktops/:id/snapshots/check", s.allow("super_admin"), s.checkSnapshot)
a.POST("/desktops/:id/snapshots/actions", s.allow("super_admin"), s.queueSnapshot)
a.POST("/tasks/:id/snapshot-reconcile", s.allow("super_admin"), s.reconcileSnapshot)
a.POST("/desktops/:id/hardware/check", s.allow("super_admin"), s.checkHardware)
a.POST("/desktops/:id/hardware", s.allow("super_admin"), s.queueHardware)
a.GET("/resources", s.resources)
a.GET("/resources/gpus", s.gpuInventory)
a.GET("/resources/vms", s.discoverVMs)
a.GET("/resources/templates", s.discoverTemplates)
a.GET("/resources/storage", s.storageList)
a.GET("/templates", s.templateList)
a.POST("/templates", s.allow("super_admin"), s.registerTemplate)
a.PATCH("/templates/:id", s.allow("super_admin"), s.updateTemplate)
a.POST("/templates/:id/validate", s.allow("super_admin"), s.validateTemplate)
a.GET("/pools", s.poolList)
a.POST("/pools", s.allow("super_admin"), s.savePool)
a.PUT("/pools/:id", s.allow("super_admin"), s.savePool)
a.PUT("/pools/:id/members", s.allow("super_admin"), s.poolMembers)
a.POST("/desktops/clone/check", s.allow("super_admin"), s.checkClone)
a.POST("/desktops/clone", s.allow("super_admin"), s.cloneDesktop)
a.POST("/resources/sync", s.allow("super_admin"), s.syncResources)
a.GET("/desktop-batches", s.powerBatches)
a.GET("/desktop-batches/:id", s.getPowerBatch)
a.POST("/desktop-batches/check", s.allow("super_admin", "operator"), s.checkPowerBatch)
a.POST("/desktop-batches", s.allow("super_admin", "operator"), s.queuePowerBatch)
a.POST("/tasks/:id/batch-power-reconcile", s.allow("super_admin", "operator"), s.reconcileBatchPower)
a.GET("/desktops", s.desktops)
a.POST("/desktops/import", s.allow("super_admin"), s.importDesktop)
a.GET("/desktops/:id", s.desktop)
a.PUT("/desktops/:id/connection", s.allow("super_admin"), s.configureDesktop)
a.POST("/desktops/:id/actions", s.allow("super_admin", "operator"), s.desktopAction)
a.GET("/terminals", s.terminals)
a.GET("/terminals/:id/metrics", s.terminalMetrics)
a.GET("/terminal-policy-templates", s.policyTemplates)
a.POST("/terminal-policy-templates", s.allow("super_admin"), s.createPolicyTemplate)
a.GET("/terminal-policy-states", s.policyStates)
a.GET("/terminal-policy-deployments", s.policyDeployments)
a.POST("/terminal-policy-deployments", s.allow("super_admin"), s.deployPolicy)
a.GET("/terminal-policy-deployments/:id/results", s.policyResults)
a.POST("/terminals", s.allow("super_admin"), s.enroll)
a.PUT("/terminals/:id", s.allow("super_admin"), s.updateTerminal)
a.POST("/terminals/:id/disable", s.allow("super_admin"), s.disableTerminal)
a.POST("/terminals/:id/reactivate", s.allow("super_admin"), s.reactivateTerminal)
a.POST("/terminals/:id/commands", s.allow("super_admin", "operator"), s.terminalCommand)
a.GET("/users", s.users)
a.POST("/users", s.allow("super_admin"), s.createUser)
a.PUT("/users/:id", s.allow("super_admin"), s.updateUser)
a.POST("/users/batch-bindings", s.allow("super_admin"), s.batchBindings)
a.PUT("/users/:id/bindings", s.allow("super_admin"), s.bindings)
a.GET("/tasks", s.tasks)
a.GET("/tasks/:id", s.task)
a.POST("/tasks/:id/hardware-reconcile", s.allow("super_admin"), s.reconcileHardware)
a.POST("/tasks/:id/reconcile", s.allow("super_admin"), s.reconcileProvision)
a.GET("/tasks/:id/diagnostics", s.allow("super_admin", "operator"), s.taskDiagnostics)
a.GET("/audit-logs", s.audits)
r.NoRoute(func(c *gin.Context) { s.fail(c, 404, "not_found", "接口不存在") })
@@ -139,7 +203,7 @@ func (s *Server) adminAuth(c *gin.Context) {
return
}
var a model.Administrator
err = s.DB.GetContext(c.Request.Context(), &a, `SELECT a.* FROM administrators a JOIN admin_sessions x ON x.admin_id=a.id WHERE x.id=$1 AND a.id=$2 AND x.expires_at>now()`, cl.ID, cl.AdminID)
err = s.DB.GetContext(c.Request.Context(), &a, `SELECT a.* FROM administrators a JOIN admin_sessions x ON x.admin_id=a.id WHERE x.id=$1 AND a.id=$2 AND a.enabled AND x.expires_at>now()`, cl.ID, cl.AdminID)
if err != nil {
s.fail(c, 401, "unauthorized", "登录已失效,请重新登录")
return
@@ -156,8 +220,18 @@ func (s *Server) login(c *gin.Context) {
if !s.decode(c, &input) {
return
}
if !s.allowLoginAttempt(c, input.Username) {
return
}
var a model.Administrator
err := s.DB.GetContext(c.Request.Context(), &a, "SELECT * FROM administrators WHERE username=$1", input.Username)
tx, err := s.DB.BeginTxx(c.Request.Context(), nil)
if err != nil {
s.dbError(c, err)
return
}
defer tx.Rollback()
// Serialize session creation against account disable, role changes and password reset.
err = tx.GetContext(c.Request.Context(), &a, "SELECT * FROM administrators WHERE username=$1 AND enabled FOR SHARE", input.Username)
if err != nil || bcrypt.CompareHashAndPassword([]byte(a.PasswordHash), []byte(input.Password)) != nil {
s.fail(c, 401, "invalid_credentials", "用户名或密码错误")
return
@@ -170,7 +244,11 @@ func (s *Server) login(c *gin.Context) {
s.dbError(c, err)
return
}
if _, err = s.DB.ExecContext(c.Request.Context(), "INSERT INTO admin_sessions VALUES($1,$2,$3)", id, a.ID, expires); err != nil {
if _, err = tx.ExecContext(c.Request.Context(), "INSERT INTO admin_sessions VALUES($1,$2,$3)", id, a.ID, expires); err != nil {
s.dbError(c, err)
return
}
if err = tx.Commit(); err != nil {
s.dbError(c, err)
return
}