feat: edit server config online (#980)

* feat: edit server config online

* clean

* refactor

* generate template

* fix deadlocks

* fix
This commit is contained in:
UUBulb 2025-01-31 13:33:53 +08:00 committed by GitHub
parent 82d40d49fd
commit 7e8985a599
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 227 additions and 86 deletions

View File

@ -110,6 +110,8 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
auth.GET("/server", listHandler(listServer))
auth.PATCH("/server/:id", commonHandler(updateServer))
auth.GET("/server/:id/config", commonHandler(getServerConfig))
auth.POST("/server/:id/config", commonHandler(setServerConfig))
auth.POST("/batch-delete/server", commonHandler(batchDeleteServer))
auth.POST("/force-update/server", commonHandler(forceUpdateServer))

View File

@ -2,6 +2,7 @@ package controller
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
@ -213,3 +214,99 @@ func forceUpdateServer(c *gin.Context) (*model.ForceUpdateResponse, error) {
return forceUpdateResp, nil
}
// Get server config
// @Summary Get server config
// @Security BearerAuth
// @Schemes
// @Description Get server config
// @Tags auth required
// @Produce json
// @Success 200 {object} model.CommonResponse[string]
// @Router /server/{id}/config [get]
func getServerConfig(c *gin.Context) (string, error) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return "", err
}
singleton.ServerLock.RLock()
s, ok := singleton.ServerList[id]
if !ok || s.TaskStream == nil {
singleton.ServerLock.RUnlock()
return "", nil
}
singleton.ServerLock.RUnlock()
if !s.HasPermission(c) {
return "", singleton.Localizer.ErrorT("permission denied")
}
if err := s.TaskStream.Send(&pb.Task{
Type: model.TaskTypeReportConfig,
}); err != nil {
return "", err
}
timeout := time.NewTimer(time.Second * 10)
select {
case <-timeout.C:
return "", singleton.Localizer.ErrorT("operation timeout")
case data := <-s.ConfigCache:
timeout.Stop()
switch data := data.(type) {
case string:
return data, nil
case error:
return "", singleton.Localizer.ErrorT("get server config failed: %v", data)
}
}
return "", singleton.Localizer.ErrorT("get server config failed")
}
// Set server config
// @Summary Set server config
// @Security BearerAuth
// @Schemes
// @Description Set server config
// @Tags auth required
// @Accept json
// @param request body string true "config"
// @Produce json
// @Success 200 {object} model.CommonResponse[any]
// @Router /server/{id}/config [post]
func setServerConfig(c *gin.Context) (any, error) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return "", err
}
var configRaw string
if err := c.ShouldBindJSON(&configRaw); err != nil {
return nil, err
}
singleton.ServerLock.RLock()
s, ok := singleton.ServerList[id]
if !ok || s.TaskStream == nil {
singleton.ServerLock.RUnlock()
return "", nil
}
singleton.ServerLock.RUnlock()
if !s.HasPermission(c) {
return "", singleton.Localizer.ErrorT("permission denied")
}
if err := s.TaskStream.Send(&pb.Task{
Type: model.TaskTypeApplyConfig,
Data: configRaw,
}); err != nil {
return "", err
}
return nil, nil
}

View File

@ -32,12 +32,20 @@ type Server struct {
GeoIP *GeoIP `gorm:"-" json:"geoip,omitempty"`
LastActive time.Time `gorm:"-" json:"last_active,omitempty"`
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
ConfigCache chan any `gorm:"-" json:"-"`
PrevTransferInSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的入站使用量
PrevTransferOutSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的出站使用量
}
func InitServer(s *Server) {
s.Host = &Host{}
s.State = &HostState{}
s.GeoIP = &GeoIP{}
s.ConfigCache = make(chan any, 1)
}
func (s *Server) CopyFromRunningServer(old *Server) {
s.Host = old.Host
s.State = old.State

View File

@ -24,6 +24,8 @@ const (
TaskTypeNAT
TaskTypeReportHostInfoDeprecated
TaskTypeFM
TaskTypeReportConfig
TaskTypeApplyConfig
)
type TerminalTask struct {
@ -127,5 +129,12 @@ func (m *Service) AfterFind(tx *gorm.DB) error {
// IsServiceSentinelNeeded 判断该任务类型是否需要进行服务监控 需要则返回true
func IsServiceSentinelNeeded(t uint64) bool {
return t != TaskTypeCommand && t != TaskTypeTerminalGRPC && t != TaskTypeUpgrade && t != TaskTypeKeepalive
switch t {
case TaskTypeCommand, TaskTypeTerminalGRPC, TaskTypeUpgrade,
TaskTypeKeepalive, TaskTypeNAT, TaskTypeFM,
TaskTypeReportConfig, TaskTypeApplyConfig:
return false
default:
return true
}
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-31 21:25+0800\n"
"POT-Creation-Date: 2025-01-30 21:58+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -25,28 +25,28 @@ msgstr ""
#: cmd/dashboard/controller/alertrule.go:108
#: cmd/dashboard/controller/alertrule.go:156
#: cmd/dashboard/controller/alertrule.go:176
#: cmd/dashboard/controller/controller.go:216
#: cmd/dashboard/controller/controller.go:226
#: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124
#: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195
#: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131
#: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43
#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:110
#: cmd/dashboard/controller/nat.go:121 cmd/dashboard/controller/nat.go:160
#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:111
#: cmd/dashboard/controller/nat.go:122 cmd/dashboard/controller/nat.go:162
#: cmd/dashboard/controller/notification.go:112
#: cmd/dashboard/controller/notification.go:166
#: cmd/dashboard/controller/notification_group.go:76
#: cmd/dashboard/controller/notification_group.go:152
#: cmd/dashboard/controller/notification_group.go:164
#: cmd/dashboard/controller/notification_group.go:233
#: cmd/dashboard/controller/server.go:65 cmd/dashboard/controller/server.go:77
#: cmd/dashboard/controller/server.go:128
#: cmd/dashboard/controller/server.go:192
#: cmd/dashboard/controller/server.go:66 cmd/dashboard/controller/server.go:78
#: cmd/dashboard/controller/server.go:137
#: cmd/dashboard/controller/server.go:201
#: cmd/dashboard/controller/server_group.go:75
#: cmd/dashboard/controller/server_group.go:150
#: cmd/dashboard/controller/server_group.go:229
#: cmd/dashboard/controller/service.go:273
#: cmd/dashboard/controller/service.go:344
#: cmd/dashboard/controller/service.go:371
#: cmd/dashboard/controller/service.go:271
#: cmd/dashboard/controller/service.go:342
#: cmd/dashboard/controller/service.go:369
#: cmd/dashboard/controller/terminal.go:41
msgid "permission denied"
msgstr ""
@ -71,15 +71,15 @@ msgstr ""
msgid "need to configure at least a single rule"
msgstr ""
#: cmd/dashboard/controller/controller.go:210
#: cmd/dashboard/controller/oauth2.go:152
#: cmd/dashboard/controller/controller.go:220
#: cmd/dashboard/controller/oauth2.go:153
#: cmd/dashboard/controller/server_group.go:162
#: cmd/dashboard/controller/service.go:96 cmd/dashboard/controller/user.go:27
#: cmd/dashboard/controller/service.go:97 cmd/dashboard/controller/user.go:27
#: cmd/dashboard/controller/user.go:63
msgid "unauthorized"
msgstr ""
#: cmd/dashboard/controller/controller.go:233
#: cmd/dashboard/controller/controller.go:243
msgid "database error"
msgstr ""
@ -100,7 +100,7 @@ msgstr ""
msgid "error parsing %s: %v"
msgstr ""
#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:117
#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:118
#, c-format
msgid "profile id %d does not exist"
msgstr ""
@ -147,32 +147,44 @@ msgstr ""
msgid "code is required"
msgstr ""
#: cmd/dashboard/controller/oauth2.go:174
#: cmd/dashboard/controller/oauth2.go:175
msgid "oauth2 user not binded yet"
msgstr ""
#: cmd/dashboard/controller/oauth2.go:215
#: cmd/dashboard/controller/oauth2.go:221
#: cmd/dashboard/controller/oauth2.go:226
#: cmd/dashboard/controller/oauth2.go:217
#: cmd/dashboard/controller/oauth2.go:223
#: cmd/dashboard/controller/oauth2.go:228
msgid "invalid state key"
msgstr ""
#: cmd/dashboard/controller/server.go:73
#: cmd/dashboard/controller/server.go:74
#, c-format
msgid "server id %d does not exist"
msgstr ""
#: cmd/dashboard/controller/server.go:250
msgid "operation timeout"
msgstr ""
#: cmd/dashboard/controller/server.go:257
msgid "get server config failed: %v"
msgstr ""
#: cmd/dashboard/controller/server.go:261
msgid "get server config failed"
msgstr ""
#: cmd/dashboard/controller/server_group.go:92
#: cmd/dashboard/controller/server_group.go:172
msgid "have invalid server id"
msgstr ""
#: cmd/dashboard/controller/service.go:89
#: cmd/dashboard/controller/service.go:90
#: cmd/dashboard/controller/service.go:165
msgid "server not found"
msgstr ""
#: cmd/dashboard/controller/service.go:269
#: cmd/dashboard/controller/service.go:267
#, c-format
msgid "service id %d does not exist"
msgstr ""
@ -185,19 +197,19 @@ msgstr ""
msgid "you don't have any oauth2 bindings"
msgstr ""
#: cmd/dashboard/controller/user.go:130
#: cmd/dashboard/controller/user.go:131
msgid "password length must be greater than 6"
msgstr ""
#: cmd/dashboard/controller/user.go:133
#: cmd/dashboard/controller/user.go:134
msgid "username can't be empty"
msgstr ""
#: cmd/dashboard/controller/user.go:136
#: cmd/dashboard/controller/user.go:137
msgid "invalid role"
msgstr ""
#: cmd/dashboard/controller/user.go:175
#: cmd/dashboard/controller/user.go:176
msgid "can't delete yourself"
msgstr ""
@ -213,23 +225,23 @@ msgstr ""
msgid "timeout: agent connection not established"
msgstr ""
#: service/rpc/nezha.go:69
#: service/rpc/nezha.go:71
msgid "Scheduled Task Executed Successfully"
msgstr ""
#: service/rpc/nezha.go:73
#: service/rpc/nezha.go:75
msgid "Scheduled Task Executed Failed"
msgstr ""
#: service/rpc/nezha.go:245
#: service/rpc/nezha.go:274
msgid "IP Changed"
msgstr ""
#: service/singleton/alertsentinel.go:170
#: service/singleton/alertsentinel.go:169
msgid "Incident"
msgstr ""
#: service/singleton/alertsentinel.go:180
#: service/singleton/alertsentinel.go:179
msgid "Resolved"
msgstr ""

View File

@ -1,23 +1,21 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# English translations for PACKAGE package.
# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# Automatically generated, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-31 21:25+0800\n"
"PO-Revision-Date: 2024-12-31 15:18+0000\n"
"Last-Translator: UUBulb <uub@kuzu.uk>\n"
"Language-Team: English (United States) <https://hosted.weblate.org/projects/"
"nezha/nezha-dashboard/en_US/>\n"
"POT-Creation-Date: 2025-01-30 14:31+0800\n"
"PO-Revision-Date: 2025-01-30 14:31+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10-dev\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: cmd/dashboard/controller/alertrule.go:104
#, c-format
@ -27,28 +25,28 @@ msgstr "alert id %d does not exist"
#: cmd/dashboard/controller/alertrule.go:108
#: cmd/dashboard/controller/alertrule.go:156
#: cmd/dashboard/controller/alertrule.go:176
#: cmd/dashboard/controller/controller.go:216
#: cmd/dashboard/controller/controller.go:226
#: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124
#: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195
#: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131
#: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43
#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:110
#: cmd/dashboard/controller/nat.go:121 cmd/dashboard/controller/nat.go:160
#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:111
#: cmd/dashboard/controller/nat.go:122 cmd/dashboard/controller/nat.go:162
#: cmd/dashboard/controller/notification.go:112
#: cmd/dashboard/controller/notification.go:166
#: cmd/dashboard/controller/notification_group.go:76
#: cmd/dashboard/controller/notification_group.go:152
#: cmd/dashboard/controller/notification_group.go:164
#: cmd/dashboard/controller/notification_group.go:233
#: cmd/dashboard/controller/server.go:65 cmd/dashboard/controller/server.go:77
#: cmd/dashboard/controller/server.go:128
#: cmd/dashboard/controller/server.go:192
#: cmd/dashboard/controller/server.go:66 cmd/dashboard/controller/server.go:78
#: cmd/dashboard/controller/server.go:137
#: cmd/dashboard/controller/server.go:201
#: cmd/dashboard/controller/server_group.go:75
#: cmd/dashboard/controller/server_group.go:150
#: cmd/dashboard/controller/server_group.go:229
#: cmd/dashboard/controller/service.go:273
#: cmd/dashboard/controller/service.go:344
#: cmd/dashboard/controller/service.go:371
#: cmd/dashboard/controller/service.go:271
#: cmd/dashboard/controller/service.go:342
#: cmd/dashboard/controller/service.go:369
#: cmd/dashboard/controller/terminal.go:41
msgid "permission denied"
msgstr "permission denied"
@ -73,15 +71,15 @@ msgstr "cycle_start is a future value"
msgid "need to configure at least a single rule"
msgstr "need to configure at least a single rule"
#: cmd/dashboard/controller/controller.go:210
#: cmd/dashboard/controller/oauth2.go:152
#: cmd/dashboard/controller/controller.go:220
#: cmd/dashboard/controller/oauth2.go:153
#: cmd/dashboard/controller/server_group.go:162
#: cmd/dashboard/controller/service.go:96 cmd/dashboard/controller/user.go:27
#: cmd/dashboard/controller/service.go:97 cmd/dashboard/controller/user.go:27
#: cmd/dashboard/controller/user.go:63
msgid "unauthorized"
msgstr "unauthorized"
#: cmd/dashboard/controller/controller.go:233
#: cmd/dashboard/controller/controller.go:243
msgid "database error"
msgstr "database error"
@ -102,7 +100,7 @@ msgstr "the retry count must be an integer between 1 and 10"
msgid "error parsing %s: %v"
msgstr "error parsing %s: %v"
#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:117
#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:118
#, c-format
msgid "profile id %d does not exist"
msgstr "profile id %d does not exist"
@ -149,32 +147,36 @@ msgstr "operation not permitted"
msgid "code is required"
msgstr "code is required"
#: cmd/dashboard/controller/oauth2.go:174
#: cmd/dashboard/controller/oauth2.go:175
msgid "oauth2 user not binded yet"
msgstr "oauth2 user not binded yet"
#: cmd/dashboard/controller/oauth2.go:215
#: cmd/dashboard/controller/oauth2.go:221
#: cmd/dashboard/controller/oauth2.go:226
#: cmd/dashboard/controller/oauth2.go:217
#: cmd/dashboard/controller/oauth2.go:223
#: cmd/dashboard/controller/oauth2.go:228
msgid "invalid state key"
msgstr "invalid state key"
#: cmd/dashboard/controller/server.go:73
#: cmd/dashboard/controller/server.go:74
#, c-format
msgid "server id %d does not exist"
msgstr "server id %d does not exist"
#: cmd/dashboard/controller/server.go:251
msgid "operation timeout"
msgstr "operation timeout"
#: cmd/dashboard/controller/server_group.go:92
#: cmd/dashboard/controller/server_group.go:172
msgid "have invalid server id"
msgstr "have invalid server id"
#: cmd/dashboard/controller/service.go:89
#: cmd/dashboard/controller/service.go:90
#: cmd/dashboard/controller/service.go:165
msgid "server not found"
msgstr "server not found"
#: cmd/dashboard/controller/service.go:269
#: cmd/dashboard/controller/service.go:267
#, c-format
msgid "service id %d does not exist"
msgstr "service id %d does not exist"
@ -187,19 +189,19 @@ msgstr "incorrect password"
msgid "you don't have any oauth2 bindings"
msgstr "you don't have any oauth2 bindings"
#: cmd/dashboard/controller/user.go:130
#: cmd/dashboard/controller/user.go:131
msgid "password length must be greater than 6"
msgstr "password length must be greater than 6"
#: cmd/dashboard/controller/user.go:133
#: cmd/dashboard/controller/user.go:134
msgid "username can't be empty"
msgstr "username can't be empty"
#: cmd/dashboard/controller/user.go:136
#: cmd/dashboard/controller/user.go:137
msgid "invalid role"
msgstr "invalid role"
#: cmd/dashboard/controller/user.go:175
#: cmd/dashboard/controller/user.go:176
msgid "can't delete yourself"
msgstr "can't delete yourself"
@ -215,23 +217,23 @@ msgstr "timeout: user connection not established"
msgid "timeout: agent connection not established"
msgstr "timeout: agent connection not established"
#: service/rpc/nezha.go:69
#: service/rpc/nezha.go:70
msgid "Scheduled Task Executed Successfully"
msgstr "Scheduled Task Executed Successfully"
#: service/rpc/nezha.go:73
#: service/rpc/nezha.go:74
msgid "Scheduled Task Executed Failed"
msgstr "Scheduled Task Executed Failed"
#: service/rpc/nezha.go:245
#: service/rpc/nezha.go:268
msgid "IP Changed"
msgstr "IP Changed"
#: service/singleton/alertsentinel.go:170
#: service/singleton/alertsentinel.go:169
msgid "Incident"
msgstr "Incident"
#: service/singleton/alertsentinel.go:180
#: service/singleton/alertsentinel.go:179
msgid "Resolved"
msgstr "Resolved"

View File

@ -67,9 +67,7 @@ func (a *authHandler) Check(ctx context.Context) (uint64, error) {
if err := singleton.DB.Create(&s).Error; err != nil {
return 0, status.Error(codes.Unauthenticated, err.Error())
}
s.Host = &model.Host{}
s.State = &model.HostState{}
s.GeoIP = &model.GeoIP{}
model.InitServer(&s)
singleton.ServerLock.Lock()
singleton.ServerList[s.ID] = &s

View File

@ -2,6 +2,7 @@ package rpc
import (
"context"
"errors"
"fmt"
"log"
"net"
@ -54,7 +55,8 @@ func (s *NezhaHandler) RequestTask(stream pb.NezhaService_RequestTaskServer) err
log.Printf("NEZHA>> RequestTask error: %v, clientID: %d\n", err, clientID)
return nil
}
if result.GetType() == model.TaskTypeCommand {
switch result.GetType() {
case model.TaskTypeCommand:
// 处理上报的计划任务
singleton.CronLock.RLock()
cr := singleton.Crons[result.GetId()]
@ -78,11 +80,24 @@ func (s *NezhaHandler) RequestTask(stream pb.NezhaService_RequestTaskServer) err
LastResult: result.GetSuccessful(),
})
}
} else if model.IsServiceSentinelNeeded(result.GetType()) {
singleton.ServiceSentinelShared.Dispatch(singleton.ReportData{
Data: result,
Reporter: clientID,
})
case model.TaskTypeReportConfig:
singleton.ServerLock.RLock()
if len(singleton.ServerList[clientID].ConfigCache) < 1 {
if !result.GetSuccessful() {
singleton.ServerList[clientID].ConfigCache <- errors.New(result.Data)
singleton.ServerLock.RUnlock()
continue
}
singleton.ServerList[clientID].ConfigCache <- result.Data
}
singleton.ServerLock.RUnlock()
default:
if model.IsServiceSentinelNeeded(result.GetType()) {
singleton.ServiceSentinelShared.Dispatch(singleton.ReportData{
Data: result,
Reporter: clientID,
})
}
}
}
}

View File

@ -31,9 +31,7 @@ func loadServers() {
DB.Find(&servers)
for _, s := range servers {
innerS := s
innerS.Host = &model.Host{}
innerS.State = &model.HostState{}
innerS.GeoIP = new(model.GeoIP)
model.InitServer(&innerS)
ServerList[innerS.ID] = &innerS
ServerUUIDToID[innerS.UUID] = innerS.ID
}