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/configs", auth.Middleware(s.cfg, http.HandlerFunc(s.handleConfigs))) 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) handleConfigs(w http.ResponseWriter, _ *http.Request) { interfaces, err := s.interfaceSvc.List() if err != nil { writeJSON(w, http.StatusInternalServerError, model.APIResponse{Code: 4001, Message: "系统执行失败", Data: map[string]string{"error": err.Error()}}) return } result := model.InterfaceConfigsResponse{ Configs: []model.InterfaceConfig{}, Errors: []model.InterfaceConfigError{}, } for _, item := range interfaces.Interfaces { data, err := s.configSvc.Read(item.SystemName) if err != nil { result.Errors = append(result.Errors, model.InterfaceConfigError{Interface: item.SystemName, Message: err.Error()}) continue } result.Configs = append(result.Configs, data) } message := "成功" if len(result.Errors) > 0 { message = "部分接口配置读取失败" } writeJSON(w, http.StatusOK, model.APIResponse{Code: 0, Message: message, Data: result}) } 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, ",") }