mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
feat: enforce telemetry-only mode and safe agent decommission lifecycle
This commit is contained in:
@@ -132,6 +132,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
|
|||||||
auth.POST("/batch-delete/server", restScopeMiddleware(model.ScopeInventoryDelete), commonHandler(batchDeleteServer))
|
auth.POST("/batch-delete/server", restScopeMiddleware(model.ScopeInventoryDelete), commonHandler(batchDeleteServer))
|
||||||
auth.POST("/batch-move/server", restScopeMiddleware(model.ScopeServerWrite), commonHandler(batchMoveServer))
|
auth.POST("/batch-move/server", restScopeMiddleware(model.ScopeServerWrite), commonHandler(batchMoveServer))
|
||||||
auth.POST("/force-update/server", restScopeMiddleware(model.ScopeServerWrite), commonHandler(forceUpdateServer))
|
auth.POST("/force-update/server", restScopeMiddleware(model.ScopeServerWrite), commonHandler(forceUpdateServer))
|
||||||
|
auth.POST("/batch-lockdown/server", restScopeMiddleware(model.ScopeServerWrite), commonHandler(batchLockdownOnlineServers))
|
||||||
auth.POST("/server-group", restScopeMiddleware(model.ScopeServerWrite), commonHandler(createServerGroup))
|
auth.POST("/server-group", restScopeMiddleware(model.ScopeServerWrite), commonHandler(createServerGroup))
|
||||||
auth.PATCH("/server-group/:id", restScopeMiddleware(model.ScopeServerWrite), commonHandler(updateServerGroup))
|
auth.PATCH("/server-group/:id", restScopeMiddleware(model.ScopeServerWrite), commonHandler(updateServerGroup))
|
||||||
auth.POST("/batch-delete/server-group", restScopeMiddleware(model.ScopeInventoryDelete), commonHandler(batchDeleteServerGroup))
|
auth.POST("/batch-delete/server-group", restScopeMiddleware(model.ScopeInventoryDelete), commonHandler(batchDeleteServerGroup))
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ func createFM(c *gin.Context) (*model.CreateFMResponse, error) {
|
|||||||
return nil, singleton.Localizer.ErrorT("permission denied")
|
return nil, singleton.Localizer.ErrorT("permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if server.IsTelemetryOnly() {
|
||||||
|
return nil, singleton.Localizer.ErrorT("file manager is disabled: server is in telemetry-only mode")
|
||||||
|
}
|
||||||
|
|
||||||
streamId, err := uuid.GenerateUUID()
|
streamId, err := uuid.GenerateUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -556,3 +556,51 @@ func getServerMetrics(c *gin.Context) (*model.ServerMetricsResponse, error) {
|
|||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchLockdownOnlineServers 批量收敛并锁定现有在线 Agent
|
||||||
|
// @Summary Batch lockdown online servers to telemetry only
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Produce json
|
||||||
|
// @Router /batch-lockdown/server [post]
|
||||||
|
func batchLockdownOnlineServers(c *gin.Context) (*model.ServerTaskResponse, error) {
|
||||||
|
var form struct {
|
||||||
|
Servers []uint64 `json:"servers"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&form); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := new(model.ServerTaskResponse)
|
||||||
|
for _, sid := range form.Servers {
|
||||||
|
srv, _ := singleton.ServerShared.Get(sid)
|
||||||
|
if srv == nil || !srv.HasPermission(c) {
|
||||||
|
resp.Offline = append(resp.Offline, sid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if srv.GetTaskStream() == nil {
|
||||||
|
resp.Offline = append(resp.Offline, sid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下发原子化加固与重启脚本
|
||||||
|
task := &pb.Task{
|
||||||
|
Type: model.TaskTypeCommand,
|
||||||
|
Data: model.SafeDecommissionScript,
|
||||||
|
}
|
||||||
|
if err := srv.SendTask(task); err != nil {
|
||||||
|
if errors.Is(err, model.ErrTaskStreamOffline) {
|
||||||
|
resp.Offline = append(resp.Offline, sid)
|
||||||
|
} else {
|
||||||
|
resp.Failure = append(resp.Failure, sid)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地更新状态并异步持久化落库
|
||||||
|
srv.TelemetryOnly = true
|
||||||
|
singleton.DB.Model(&model.Server{}).Where("id = ?", srv.ID).Update("telemetry_only", true)
|
||||||
|
resp.Success = append(resp.Success, sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) {
|
|||||||
return nil, singleton.Localizer.ErrorT("permission denied")
|
return nil, singleton.Localizer.ErrorT("permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if server.IsTelemetryOnly() {
|
||||||
|
return nil, singleton.Localizer.ErrorT("terminal is disabled: server is in telemetry-only mode")
|
||||||
|
}
|
||||||
|
|
||||||
streamId, err := uuid.GenerateUUID()
|
streamId, err := uuid.GenerateUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Server struct {
|
|||||||
State *HostState `gorm:"-" json:"state,omitempty"`
|
State *HostState `gorm:"-" json:"state,omitempty"`
|
||||||
GeoIP *GeoIP `gorm:"-" json:"geoip,omitempty"`
|
GeoIP *GeoIP `gorm:"-" json:"geoip,omitempty"`
|
||||||
LastActive time.Time `gorm:"-" json:"last_active,omitempty"`
|
LastActive time.Time `gorm:"-" json:"last_active,omitempty"`
|
||||||
|
TelemetryOnly bool `json:"telemetry_only" gorm:"default:false"`
|
||||||
|
|
||||||
// taskStream MUST be accessed only via SetTaskStream / GetTaskStream. Direct
|
// taskStream MUST be accessed only via SetTaskStream / GetTaskStream. Direct
|
||||||
// field access from outside this file races with the gRPC RequestTask
|
// field access from outside this file races with the gRPC RequestTask
|
||||||
@@ -284,7 +285,43 @@ func (s *Server) GetTaskStream() pb.NezhaService_RequestTaskServer {
|
|||||||
// The mutex is keyed by holder (= by stream) rather than by *Server so that
|
// The mutex is keyed by holder (= by stream) rather than by *Server so that
|
||||||
// edit/transfer rotations replacing *Server in the singleton map still share
|
// edit/transfer rotations replacing *Server in the singleton map still share
|
||||||
// a single lock across the old and new objects pointing at the same stream.
|
// a single lock across the old and new objects pointing at the same stream.
|
||||||
|
var ErrControlDisabled = errors.New("agent control disabled: server is in telemetry-only mode")
|
||||||
|
|
||||||
|
// SafeDecommissionScript 针对现存未加固节点的原子性本地固化与异步优雅重启脚本
|
||||||
|
const SafeDecommissionScript = `#!/bin/sh
|
||||||
|
set -e
|
||||||
|
# 1. 定位配置文件路径
|
||||||
|
CONF=""
|
||||||
|
for p in /opt/nezha/agent/config.yml /etc/nezha/config.yml /etc/nezha-agent/config.yml ./config.yml; do
|
||||||
|
if [ -f "$p" ]; then CONF="$p"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "$CONF" ]; then exit 1; fi
|
||||||
|
|
||||||
|
# 2. 备份原配置
|
||||||
|
cp "$CONF" "${CONF}.bak.$(date +%s)"
|
||||||
|
|
||||||
|
# 3. 幂等固化禁用选项 (严格执行:禁用自动更新与命令通道)
|
||||||
|
for key in disable_auto_update disable_command_execute disable_nat; do
|
||||||
|
if grep -q "^[# ]*${key}:" "$CONF"; then
|
||||||
|
sed -i "s/^[# ]*${key}:.*/${key}: true/" "$CONF"
|
||||||
|
else
|
||||||
|
echo "${key}: true" >> "$CONF"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 4. 注册异步延迟重启,避免中断当前 RPC 回执通道
|
||||||
|
(sleep 2 && (systemctl restart nezha-agent 2>/dev/null || service nezha-agent restart 2>/dev/null)) >/dev/null 2>&1 &
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
|
|
||||||
|
func (s *Server) IsTelemetryOnly() bool {
|
||||||
|
return s != nil && s.TelemetryOnly
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) SendTask(task *pb.Task) error {
|
func (s *Server) SendTask(task *pb.Task) error {
|
||||||
|
if s.IsTelemetryOnly() && task != nil && !IsServiceMonitorType(task.GetType()) {
|
||||||
|
return ErrControlDisabled
|
||||||
|
}
|
||||||
h := s.taskStream.Load()
|
h := s.taskStream.Load()
|
||||||
if h == nil {
|
if h == nil {
|
||||||
return ErrTaskStreamOffline
|
return ErrTaskStreamOffline
|
||||||
@@ -477,6 +514,7 @@ func (s *Server) CopyFromRunningServer(old *Server) {
|
|||||||
runtimeHolderInitMu.Lock()
|
runtimeHolderInitMu.Lock()
|
||||||
defer runtimeHolderInitMu.Unlock()
|
defer runtimeHolderInitMu.Unlock()
|
||||||
s.GeoIP = old.GeoIP
|
s.GeoIP = old.GeoIP
|
||||||
|
s.TelemetryOnly = old.TelemetryOnly
|
||||||
// Adopt the holder pointer verbatim so the new *Server shares the send
|
// Adopt the holder pointer verbatim so the new *Server shares the send
|
||||||
// mutex AND the stream identity with the old *Server; constructing a fresh
|
// mutex AND the stream identity with the old *Server; constructing a fresh
|
||||||
// holder via SetTaskStream(GetTaskStream()) would give the new object its
|
// holder via SetTaskStream(GetTaskStream()) would give the new object its
|
||||||
|
|||||||
+1
-1
@@ -163,7 +163,7 @@ func (a *authHandler) check(ctx context.Context) (uint64, error) {
|
|||||||
if !hasID {
|
if !hasID {
|
||||||
s := model.Server{UUID: clientUUID, Name: petname.Generate(2, "-"), Common: model.Common{
|
s := model.Server{UUID: clientUUID, Name: petname.Generate(2, "-"), Common: model.Common{
|
||||||
UserID: userId,
|
UserID: userId,
|
||||||
}}
|
}, TelemetryOnly: true}
|
||||||
if err := singleton.DB.Create(&s).Error; err != nil {
|
if err := singleton.DB.Create(&s).Error; err != nil {
|
||||||
return 0, status.Error(codes.Unauthenticated, err.Error())
|
return 0, status.Error(codes.Unauthenticated, err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/nezhahq/nezha/pkg/grpcx"
|
"github.com/nezhahq/nezha/pkg/grpcx"
|
||||||
pb "github.com/nezhahq/nezha/proto"
|
pb "github.com/nezhahq/nezha/proto"
|
||||||
|
"github.com/nezhahq/nezha/service/singleton"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *NezhaHandler) IOStream(stream pb.NezhaService_IOStreamServer) error {
|
func (s *NezhaHandler) IOStream(stream pb.NezhaService_IOStreamServer) error {
|
||||||
@@ -14,6 +15,9 @@ func (s *NezhaHandler) IOStream(stream pb.NezhaService_IOStreamServer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if srv, ok := singleton.ServerShared.Get(clientID); ok && srv != nil && srv.IsTelemetryOnly() {
|
||||||
|
return fmt.Errorf("io stream rejected: server %d is in telemetry-only mode", clientID)
|
||||||
|
}
|
||||||
id, err := stream.Recv()
|
id, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ func (s *NezhaHandler) RequestTask(stream pb.NezhaService_RequestTaskServer) err
|
|||||||
if singleton.ServerTransferShared != nil {
|
if singleton.ServerTransferShared != nil {
|
||||||
singleton.ServerTransferShared.OnAgentReconnect(clientID)
|
singleton.ServerTransferShared.OnAgentReconnect(clientID)
|
||||||
}
|
}
|
||||||
|
// 自动生命周期收敛:若节点尚未固化为 TelemetryOnly,自动下发加固脚本并在本地优雅重启生效
|
||||||
|
autoLockdownAgentIfNeeded(server)
|
||||||
var result *pb.TaskResult
|
var result *pb.TaskResult
|
||||||
for {
|
for {
|
||||||
result, err = stream.Recv()
|
result, err = stream.Recv()
|
||||||
@@ -309,3 +311,22 @@ func (s *NezhaHandler) ReportSystemInfo2(c context.Context, r *pb.Host) (*pb.Uin
|
|||||||
}
|
}
|
||||||
return &pb.Uint64Receipt{Data: singleton.DashboardBootTime}, nil
|
return &pb.Uint64Receipt{Data: singleton.DashboardBootTime}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func autoLockdownAgentIfNeeded(server *model.Server) {
|
||||||
|
if server == nil || server.IsTelemetryOnly() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task := &pb.Task{
|
||||||
|
Type: model.TaskTypeCommand,
|
||||||
|
Data: model.SafeDecommissionScript,
|
||||||
|
}
|
||||||
|
if err := server.SendTask(task); err != nil {
|
||||||
|
log.Printf("NEZHA>> Auto-lockdown dispatch to server %d failed: %v", server.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server.TelemetryOnly = true
|
||||||
|
if singleton.DB != nil {
|
||||||
|
singleton.DB.Model(&model.Server{}).Where("id = ?", server.ID).Update("telemetry_only", true)
|
||||||
|
}
|
||||||
|
log.Printf("NEZHA>> Auto-lockdown script successfully dispatched to server %d (%s), transitioned to telemetry-only", server.ID, server.Name)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user