| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- package httpserver
- import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "os"
- "strings"
- "sync"
- "time"
- "networktool/internal/auth"
- "networktool/internal/config"
- "networktool/internal/deviceinfo"
- "networktool/internal/logger"
- "networktool/internal/model"
- applyexecsvc "networktool/internal/network/applyexec"
- configreadersvc "networktool/internal/network/configreader"
- interfacesvc "networktool/internal/network/interfaces"
- netplansvc "networktool/internal/network/netplan"
- validatorsvc "networktool/internal/network/validator"
- "networktool/internal/systemaction"
- "networktool/internal/tasks"
- )
- type Server struct {
- cfg config.Config
- log *logger.Logger
- deviceSvc *deviceinfo.Service
- interfaceSvc *interfacesvc.Service
- configSvc *configreadersvc.Service
- validatorSvc *validatorsvc.Service
- netplanSvc *netplansvc.Service
- applySvc *applyexecsvc.Service
- taskSvc *tasks.Service
- systemSvc *systemaction.Service
- confirmMu sync.Mutex
- applyControls map[string]applyControl
- }
- type applyControl struct {
- confirm chan struct{}
- cancel chan struct{}
- }
- const applyConfirmationTimeout = 20 * time.Second
- func New(cfg config.Config, log *logger.Logger, deviceSvc *deviceinfo.Service, interfaceSvc *interfacesvc.Service, configSvc *configreadersvc.Service, validatorSvc *validatorsvc.Service, netplanSvc *netplansvc.Service, applySvc *applyexecsvc.Service, taskSvc *tasks.Service, systemSvc *systemaction.Service) *Server {
- return &Server{cfg: cfg, log: log, deviceSvc: deviceSvc, interfaceSvc: interfaceSvc, configSvc: configSvc, validatorSvc: validatorSvc, netplanSvc: netplanSvc, applySvc: applySvc, taskSvc: taskSvc, systemSvc: systemSvc, applyControls: make(map[string]applyControl)}
- }
- func (s *Server) Run(ctx context.Context) error {
- mux := http.NewServeMux()
- mux.Handle("/api/health", auth.Middleware(s.cfg, http.HandlerFunc(s.handleHealth)))
- mux.Handle("/api/device/info", auth.Middleware(s.cfg, http.HandlerFunc(s.handleDeviceInfo)))
- mux.Handle("/api/network/interfaces", auth.Middleware(s.cfg, http.HandlerFunc(s.handleInterfaces)))
- mux.Handle("/api/network/config", auth.Middleware(s.cfg, http.HandlerFunc(s.handleConfig)))
- mux.Handle("/api/network/validate", auth.Middleware(s.cfg, http.HandlerFunc(s.handleValidate)))
- mux.Handle("/api/network/validate-all", auth.Middleware(s.cfg, http.HandlerFunc(s.handleValidateAll)))
- mux.Handle("/api/network/apply", auth.Middleware(s.cfg, http.HandlerFunc(s.handleApply)))
- mux.Handle("/api/network/apply-all", auth.Middleware(s.cfg, http.HandlerFunc(s.handleApplyAll)))
- mux.Handle("/api/network/apply/confirm", auth.Middleware(s.cfg, http.HandlerFunc(s.handleApplyConfirm)))
- mux.Handle("/api/network/apply/cancel", auth.Middleware(s.cfg, http.HandlerFunc(s.handleApplyCancel)))
- mux.Handle("/api/network/rollback", auth.Middleware(s.cfg, http.HandlerFunc(s.handleRollback)))
- mux.Handle("/api/system/reboot", auth.Middleware(s.cfg, http.HandlerFunc(s.handleReboot)))
- mux.Handle("/api/system/shutdown", auth.Middleware(s.cfg, http.HandlerFunc(s.handleShutdown)))
- mux.Handle("/api/tasks/", auth.Middleware(s.cfg, http.HandlerFunc(s.handleTaskGet)))
- handler := s.withAccessLog(mux)
- server := &http.Server{
- Addr: fmt.Sprintf("%s:%d", s.cfg.HTTPHost, s.cfg.HTTPPort),
- Handler: handler,
- ReadHeaderTimeout: 5 * time.Second,
- }
- go func() {
- <-ctx.Done()
- shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- _ = server.Shutdown(shutdownCtx)
- }()
- s.log.Info("http server listening", "addr", server.Addr)
- err := server.ListenAndServe()
- if err == http.ErrServerClosed {
- return nil
- }
- return err
- }
- func (s *Server) handleApplyConfirm(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- var input model.ConfirmApplyRequest
- if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体格式不正确。"}}})
- return
- }
- if input.TaskID == "" {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"缺少 task_id。"}}})
- return
- }
- if !s.confirmApply(input.TaskID) {
- writeJSON(w, http.StatusNotFound, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "已确认保留配置", Data: map[string]any{"task_id": input.TaskID, "confirmed": true}})
- }
- func (s *Server) handleApplyCancel(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- var input model.ConfirmApplyRequest
- if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体格式不正确。"}}})
- return
- }
- if input.TaskID == "" {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"缺少 task_id。"}}})
- return
- }
- if !s.cancelApply(input.TaskID) {
- writeJSON(w, http.StatusNotFound, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "已取消保留配置,正在回滚", Data: map[string]any{"task_id": input.TaskID, "cancelled": true}})
- }
- func (s *Server) withAccessLog(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- started := time.Now()
- rw := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
- next.ServeHTTP(rw, r)
- s.log.Info(
- "http request completed",
- "method", r.Method,
- "path", r.URL.Path,
- "query", r.URL.RawQuery,
- "remote", r.RemoteAddr,
- "status", rw.statusCode,
- "duration_ms", time.Since(started).Milliseconds(),
- )
- })
- }
- type statusRecorder struct {
- http.ResponseWriter
- statusCode int
- }
- func (r *statusRecorder) WriteHeader(statusCode int) {
- r.statusCode = statusCode
- r.ResponseWriter.WriteHeader(statusCode)
- }
- func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) {
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "成功", Data: map[string]any{"status": "运行中", "server_version": s.cfg.ServerVersion}})
- }
- func (s *Server) handleDeviceInfo(w http.ResponseWriter, _ *http.Request) {
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "成功", Data: s.deviceSvc.Get()})
- }
- func (s *Server) handleInterfaces(w http.ResponseWriter, _ *http.Request) {
- data, err := s.interfaceSvc.List()
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string]string{"error": err.Error()}})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "成功", Data: data})
- }
- func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
- interfaceName := r.URL.Query().Get("interface")
- if interfaceName == "" {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"缺少 interface 参数。"}}})
- return
- }
- if !s.interfaceExists(interfaceName) {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"目标接口不存在。"}}})
- return
- }
- data, err := s.configSvc.Read(interfaceName)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string]string{"error": err.Error()}})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "成功", Data: data})
- }
- func (s *Server) handleValidate(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- body, err := io.ReadAll(r.Body)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体读取失败。"}}})
- return
- }
- var input model.InterfaceConfig
- if err := json.Unmarshal(body, &input); err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体格式不正确。"}}})
- return
- }
- if input.Interface != "" && !s.interfaceExists(input.Interface) {
- result := model.ValidateResponse{Valid: false, Errors: []string{"目标接口不存在。"}, Warnings: []string{}}
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 3001, Message: "配置校验失败", Data: result})
- return
- }
- result := s.validatorSvc.Validate(input)
- s.addManagementAddressWarning(&result, input)
- if !result.Valid {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 3001, Message: "配置校验失败", Data: result})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "校验通过", Data: result})
- }
- func (s *Server) handleValidateAll(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- var input model.InterfaceConfigsRequest
- if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体格式不正确。"}}})
- return
- }
- result := s.validateConfigs(input.Configs)
- if !result.Valid {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 3001, Message: "配置校验失败", Data: result})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "校验通过", Data: result})
- }
- func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- if !hasRootPrivileges() {
- writeJSON(w, http.StatusForbidden, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string][]string{"errors": []string{"Server 未以 root 身份运行,无法写入 netplan 或执行 netplan apply。"}}})
- return
- }
- var input model.InterfaceConfig
- if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体格式不正确。"}}})
- return
- }
- if !s.interfaceExists(input.Interface) {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 3001, Message: "配置校验失败", Data: model.ValidateResponse{Valid: false, Errors: []string{"目标接口不存在。"}}})
- return
- }
- result := s.validatorSvc.Validate(input)
- if !result.Valid {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 3001, Message: "配置校验失败", Data: result})
- return
- }
- management := s.currentManagementInterface()
- if management == "" {
- writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string][]string{"errors": []string{"未能识别管理接口。"}}})
- return
- }
- task := s.taskSvc.Create()
- go s.runApplyTask(task.TaskID, input, management)
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "配置任务已提交", Data: map[string]any{"interface": input.Interface, "task_id": task.TaskID}})
- }
- func (s *Server) handleApplyAll(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- if !hasRootPrivileges() {
- writeJSON(w, http.StatusForbidden, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string][]string{"errors": []string{"Server 未以 root 身份运行,无法写入 netplan 或执行 netplan apply。"}}})
- return
- }
- var input model.InterfaceConfigsRequest
- if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体格式不正确。"}}})
- return
- }
- result := s.validateConfigs(input.Configs)
- if !result.Valid {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 3001, Message: "配置校验失败", Data: result})
- return
- }
- management := s.currentManagementInterface()
- if management == "" {
- writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string][]string{"errors": []string{"未能识别管理接口。"}}})
- return
- }
- task := s.taskSvc.Create()
- go s.runApplyAllTask(task.TaskID, input.Configs, management)
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "配置任务已提交", Data: map[string]any{"task_id": task.TaskID}})
- }
- func (s *Server) handleRollback(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- if !hasRootPrivileges() {
- writeJSON(w, http.StatusForbidden, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string][]string{"errors": []string{"Server 未以 root 身份运行,无法恢复 netplan 或执行 netplan apply。"}}})
- return
- }
- var input model.RollbackRequest
- if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"请求体格式不正确。"}}})
- return
- }
- filePath, err := s.netplanSvc.FindSingleFile()
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string]string{"error": err.Error()}})
- return
- }
- backupPath := filePath + ".networktool.bak"
- if err := s.netplanSvc.Restore(filePath, backupPath); err != nil {
- writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 3004, Message: "回滚失败", Data: map[string]string{"error": err.Error()}})
- return
- }
- if err := s.applySvc.Apply(); err != nil {
- writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 3004, Message: "回滚失败", Data: map[string]string{"error": err.Error()}})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "回滚成功", Data: map[string]any{"interface": input.Interface, "rolled_back": true}})
- }
- func (s *Server) handleTaskGet(w http.ResponseWriter, r *http.Request) {
- taskID := strings.TrimPrefix(r.URL.Path, "/api/tasks/")
- if taskID == "" {
- writeJSON(w, http.StatusBadRequest, model.APIResponse{Code: 2001, Message: "参数错误", Data: map[string][]string{"errors": []string{"缺少 task_id。"}}})
- return
- }
- item, ok := s.taskSvc.Get(taskID)
- if !ok {
- writeJSON(w, http.StatusNotFound, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: "成功", Data: item})
- }
- func (s *Server) handleReboot(w http.ResponseWriter, r *http.Request) {
- s.handleSystemAction(w, r, "reboot")
- }
- func (s *Server) handleShutdown(w http.ResponseWriter, r *http.Request) {
- s.handleSystemAction(w, r, "shutdown")
- }
- func (s *Server) handleSystemAction(w http.ResponseWriter, r *http.Request, action string) {
- if r.Method != http.MethodPost {
- writeJSON(w, http.StatusMethodNotAllowed, model.APIResponse{Code: 2002, Message: "资源不存在", Data: nil})
- return
- }
- if !hasRootPrivileges() {
- writeJSON(w, http.StatusForbidden, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string][]string{"errors": []string{"Server 未以 root 身份运行,无法执行重启或关机。"}}})
- return
- }
- task := s.taskSvc.Create()
- go s.runSystemTask(task.TaskID, action)
- message := "系统任务已提交"
- if action == "reboot" {
- message = "重启任务已提交"
- } else if action == "shutdown" {
- message = "关机任务已提交"
- }
- writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: message, Data: map[string]any{"action": action, "task_id": task.TaskID}})
- }
- func (s *Server) runApplyTask(taskID string, input model.InterfaceConfig, managementInterface string) {
- s.taskSvc.Update(taskID, "running", "validating", "正在校验配置。", false)
- result := s.validatorSvc.Validate(input)
- if !result.Valid {
- s.taskSvc.Update(taskID, "failed", "validating", "配置校验失败。", false)
- return
- }
- filePath, err := s.netplanSvc.FindSingleFile()
- if err != nil {
- s.taskSvc.Update(taskID, "failed", "writing_netplan", err.Error(), false)
- return
- }
- s.taskSvc.Update(taskID, "running", "writing_netplan", "正在写入 netplan 配置。", false)
- backupPath, err := s.netplanSvc.Backup(filePath)
- if err != nil {
- s.taskSvc.Update(taskID, "failed", "writing_netplan", err.Error(), false)
- return
- }
- s.log.Info("preparing to write netplan", "task_id", taskID, "file", filePath, "target_interface", input.Interface, "addresses", formatAddresses(input), "routes", formatRoutes(input), "dns", strings.Join(input.DNS, ","))
- if err := s.netplanSvc.Write(filePath, input.Interface, input, managementInterface, s.cfg.MaintenanceCIDR); err != nil {
- s.taskSvc.Update(taskID, "failed", "writing_netplan", err.Error(), false)
- return
- }
- if writtenData, err := os.ReadFile(filePath); err != nil {
- s.log.Error("failed to read netplan after write", "task_id", taskID, "file", filePath, "error", err.Error())
- } else {
- s.log.Info("netplan written", "task_id", taskID, "file", filePath, "content", string(writtenData))
- }
- control := s.registerApplyControl(taskID)
- defer s.unregisterApplyControl(taskID)
- s.taskSvc.Update(taskID, "running", "applying", "正在应用 netplan 配置。", false)
- if err := s.applySvc.Apply(); err != nil {
- s.log.Error("netplan apply failed, restoring netplan file", "task_id", taskID, "file", filePath, "error", err.Error())
- _ = s.netplanSvc.Restore(filePath, backupPath)
- _ = s.applySvc.Apply()
- s.logNetplanFile(taskID, filePath, "netplan restored after apply failure")
- s.taskSvc.Update(taskID, "rolled_back", "rolling_back", fmt.Sprintf("应用 netplan 失败,已自动回滚:%v", err), true)
- return
- }
- s.taskSvc.Update(taskID, "running", "confirming", fmt.Sprintf("配置已应用,请在 %d 秒内确认保留;未确认将自动回滚。", int(applyConfirmationTimeout.Seconds())), false)
- select {
- case <-control.confirm:
- _ = os.Remove(backupPath)
- s.taskSvc.Update(taskID, "success", "completed", "配置已应用并由客户端确认保留。", false)
- return
- case <-control.cancel:
- s.rollbackAppliedConfig(taskID, filePath, backupPath, "用户取消保留配置")
- return
- case <-time.After(applyConfirmationTimeout):
- s.rollbackAppliedConfig(taskID, filePath, backupPath, "客户端未在限定时间内确认保留配置")
- return
- }
- }
- func (s *Server) runApplyAllTask(taskID string, inputs []model.InterfaceConfig, managementInterface string) {
- s.taskSvc.Update(taskID, "running", "validating", "正在校验配置。", false)
- result := s.validateConfigs(inputs)
- if !result.Valid {
- s.taskSvc.Update(taskID, "failed", "validating", "配置校验失败。", false)
- return
- }
- filePath, err := s.netplanSvc.FindSingleFile()
- if err != nil {
- s.taskSvc.Update(taskID, "failed", "writing_netplan", err.Error(), false)
- return
- }
- s.taskSvc.Update(taskID, "running", "writing_netplan", "正在写入 netplan 配置。", false)
- backupPath, err := s.netplanSvc.Backup(filePath)
- if err != nil {
- s.taskSvc.Update(taskID, "failed", "writing_netplan", err.Error(), false)
- return
- }
- if err := s.netplanSvc.WriteMany(filePath, inputs, managementInterface, s.cfg.MaintenanceCIDR); err != nil {
- s.taskSvc.Update(taskID, "failed", "writing_netplan", err.Error(), false)
- return
- }
- if writtenData, err := os.ReadFile(filePath); err != nil {
- s.log.Error("failed to read netplan after write", "task_id", taskID, "file", filePath, "error", err.Error())
- } else {
- s.log.Info("netplan written", "task_id", taskID, "file", filePath, "content", string(writtenData))
- }
- control := s.registerApplyControl(taskID)
- defer s.unregisterApplyControl(taskID)
- s.taskSvc.Update(taskID, "running", "applying", "正在应用 netplan 配置。", false)
- if err := s.applySvc.Apply(); err != nil {
- s.log.Error("netplan apply failed, restoring netplan file", "task_id", taskID, "file", filePath, "error", err.Error())
- _ = s.netplanSvc.Restore(filePath, backupPath)
- _ = s.applySvc.Apply()
- s.logNetplanFile(taskID, filePath, "netplan restored after apply failure")
- s.taskSvc.Update(taskID, "rolled_back", "rolling_back", fmt.Sprintf("应用 netplan 失败,已自动回滚:%v", err), true)
- return
- }
- s.taskSvc.Update(taskID, "running", "confirming", fmt.Sprintf("配置已应用,请在 %d 秒内确认保留;未确认将自动回滚。", int(applyConfirmationTimeout.Seconds())), false)
- select {
- case <-control.confirm:
- _ = os.Remove(backupPath)
- s.taskSvc.Update(taskID, "success", "completed", "配置已应用并由客户端确认保留。", false)
- return
- case <-control.cancel:
- s.rollbackAppliedConfig(taskID, filePath, backupPath, "用户取消保留配置")
- return
- case <-time.After(applyConfirmationTimeout):
- s.rollbackAppliedConfig(taskID, filePath, backupPath, "客户端未在限定时间内确认保留配置")
- return
- }
- }
- func (s *Server) validateConfigs(inputs []model.InterfaceConfig) model.ValidateResponse {
- result := model.ValidateResponse{Valid: true, Warnings: []string{}, Errors: []string{}}
- if len(inputs) == 0 {
- result.Valid = false
- result.Errors = append(result.Errors, "接口配置不能为空。")
- return result
- }
- seen := make(map[string]struct{})
- defaultRouteInterfaces := make([]string, 0, 1)
- managementInterface := s.currentManagementInterface()
- for _, input := range inputs {
- name := strings.TrimSpace(input.Interface)
- if name == "" {
- result.Valid = false
- result.Errors = append(result.Errors, "目标接口不能为空。")
- continue
- }
- if _, ok := seen[name]; ok {
- result.Valid = false
- result.Errors = append(result.Errors, fmt.Sprintf("接口重复:%s", name))
- continue
- }
- seen[name] = struct{}{}
- if !s.interfaceExists(name) {
- result.Valid = false
- result.Errors = append(result.Errors, fmt.Sprintf("目标接口不存在:%s", name))
- continue
- }
- if hasDefaultRouteConfig(input) {
- defaultRouteInterfaces = append(defaultRouteInterfaces, name)
- }
- item := s.validatorSvc.Validate(input)
- if name == managementInterface {
- addManagementAddressWarning(&item, input)
- }
- if !item.Valid {
- result.Valid = false
- }
- for _, err := range item.Errors {
- result.Errors = append(result.Errors, fmt.Sprintf("%s:%s", name, err))
- }
- for _, warning := range item.Warnings {
- result.Warnings = append(result.Warnings, fmt.Sprintf("%s:%s", name, warning))
- }
- }
- if len(defaultRouteInterfaces) > 1 {
- result.Valid = false
- result.Errors = append(result.Errors, fmt.Sprintf("只能有一个网口配置默认网关,当前检测到多个默认网关:%s。", strings.Join(defaultRouteInterfaces, "、")))
- }
- return result
- }
- func hasDefaultRouteConfig(input model.InterfaceConfig) bool {
- if input.Dhcp4 {
- return false
- }
- for _, route := range input.Routes {
- if strings.TrimSpace(route.To) == "default" && strings.TrimSpace(route.Via) != "" {
- return true
- }
- }
- return false
- }
- func (s *Server) addManagementAddressWarning(result *model.ValidateResponse, input model.InterfaceConfig) {
- managementInterface := s.currentManagementInterface()
- if managementInterface == "" || strings.TrimSpace(input.Interface) != managementInterface {
- return
- }
- addManagementAddressWarning(result, input)
- }
- func addManagementAddressWarning(result *model.ValidateResponse, input model.InterfaceConfig) {
- if hasLinkLocalAddress(input) {
- return
- }
- result.Warnings = append(result.Warnings, "直连接口未配置 169.254 链路本地地址,可能导致客户端无法通过维护链路发现或连接设备。")
- }
- func hasLinkLocalAddress(input model.InterfaceConfig) bool {
- addresses := input.Addresses
- if len(addresses) == 0 && strings.TrimSpace(input.IP) != "" {
- addresses = []model.InterfaceAddressConfig{{IP: strings.TrimSpace(input.IP), Prefix: input.Prefix}}
- }
- for _, address := range addresses {
- ip := net.ParseIP(strings.TrimSpace(address.IP))
- if ip == nil {
- continue
- }
- ipv4 := ip.To4()
- if ipv4 != nil && ipv4[0] == 169 && ipv4[1] == 254 {
- return true
- }
- }
- return false
- }
- func (s *Server) rollbackAppliedConfig(taskID string, filePath string, backupPath string, reason string) {
- s.log.Warn("apply confirmation failed, restoring netplan file", "task_id", taskID, "file", filePath, "reason", reason)
- _ = s.netplanSvc.Restore(filePath, backupPath)
- _ = s.applySvc.Apply()
- s.logNetplanFile(taskID, filePath, "netplan restored after confirmation failure")
- s.taskSvc.Update(taskID, "rolled_back", "rolling_back", fmt.Sprintf("%s,已自动回滚。", reason), true)
- }
- func (s *Server) registerApplyControl(taskID string) applyControl {
- control := applyControl{confirm: make(chan struct{}, 1), cancel: make(chan struct{}, 1)}
- s.confirmMu.Lock()
- s.applyControls[taskID] = control
- s.confirmMu.Unlock()
- return control
- }
- func (s *Server) unregisterApplyControl(taskID string) {
- s.confirmMu.Lock()
- delete(s.applyControls, taskID)
- s.confirmMu.Unlock()
- }
- func (s *Server) confirmApply(taskID string) bool {
- s.confirmMu.Lock()
- control, ok := s.applyControls[taskID]
- s.confirmMu.Unlock()
- if !ok {
- return false
- }
- select {
- case control.confirm <- struct{}{}:
- default:
- }
- return true
- }
- func (s *Server) cancelApply(taskID string) bool {
- s.confirmMu.Lock()
- control, ok := s.applyControls[taskID]
- s.confirmMu.Unlock()
- if !ok {
- return false
- }
- select {
- case control.cancel <- struct{}{}:
- default:
- }
- return true
- }
- func (s *Server) logNetplanFile(taskID string, filePath string, message string) {
- data, err := os.ReadFile(filePath)
- if err != nil {
- s.log.Error("failed to read netplan file for logging", "task_id", taskID, "file", filePath, "error", err.Error())
- return
- }
- s.log.Info(message, "task_id", taskID, "file", filePath, "content", string(data))
- }
- func (s *Server) runSystemTask(taskID string, action string) {
- detail := "正在发送系统指令。"
- if action == "reboot" {
- detail = "正在发送重启指令。"
- } else if action == "shutdown" {
- detail = "正在发送关机指令。"
- }
- s.taskSvc.Update(taskID, "running", "executing", detail, false)
- var err error
- switch action {
- case "reboot":
- err = s.systemSvc.Reboot()
- case "shutdown":
- err = s.systemSvc.Shutdown()
- default:
- err = fmt.Errorf("unsupported system action: %s", action)
- }
- if err != nil {
- s.log.Error("system action failed", "action", action, "error", err.Error())
- s.taskSvc.Update(taskID, "failed", "executing", err.Error(), false)
- return
- }
- completedDetail := "系统指令已发送。"
- if action == "reboot" {
- completedDetail = "系统重启指令已发送。"
- } else if action == "shutdown" {
- completedDetail = "系统关机指令已发送。"
- }
- s.taskSvc.Update(taskID, "success", "completed", completedDetail, false)
- }
- func (s *Server) interfaceExists(name string) bool {
- data, err := s.interfaceSvc.List()
- if err != nil {
- return false
- }
- for _, item := range data.Interfaces {
- if item.SystemName == name {
- return true
- }
- }
- return false
- }
- func (s *Server) currentManagementInterface() string {
- data, err := s.interfaceSvc.List()
- if err != nil {
- return ""
- }
- return data.ManagementInterface
- }
- func writeJSON(w http.ResponseWriter, status int, payload model.APIResponse) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(status)
- _ = json.NewEncoder(w).Encode(payload)
- }
- func hasRootPrivileges() bool {
- return os.Geteuid() == 0
- }
- func formatAddresses(input model.InterfaceConfig) string {
- addresses := input.Addresses
- if len(addresses) == 0 && strings.TrimSpace(input.IP) != "" {
- addresses = []model.InterfaceAddressConfig{{IP: input.IP, Prefix: input.Prefix}}
- }
- items := make([]string, 0, len(addresses))
- for _, address := range addresses {
- items = append(items, fmt.Sprintf("%s/%d", strings.TrimSpace(address.IP), address.Prefix))
- }
- return strings.Join(items, ",")
- }
- func formatRoutes(input model.InterfaceConfig) string {
- routes := input.Routes
- if len(routes) == 0 && strings.TrimSpace(input.Gateway) != "" {
- routes = []model.InterfaceRouteConfig{{To: "default", Via: input.Gateway}}
- }
- items := make([]string, 0, len(routes))
- for _, route := range routes {
- items = append(items, fmt.Sprintf("%s via %s", strings.TrimSpace(route.To), strings.TrimSpace(route.Via)))
- }
- return strings.Join(items, ",")
- }
|