|
|
@@ -8,6 +8,7 @@ import (
|
|
|
"net/http"
|
|
|
"os"
|
|
|
"strings"
|
|
|
+ "sync"
|
|
|
"time"
|
|
|
|
|
|
"quickip/internal/auth"
|
|
|
@@ -19,28 +20,35 @@ import (
|
|
|
configreadersvc "quickip/internal/network/configreader"
|
|
|
interfacesvc "quickip/internal/network/interfaces"
|
|
|
netplansvc "quickip/internal/network/netplan"
|
|
|
- "quickip/internal/systemaction"
|
|
|
validatorsvc "quickip/internal/network/validator"
|
|
|
- verifysvc "quickip/internal/network/verify"
|
|
|
+ "quickip/internal/systemaction"
|
|
|
"quickip/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
|
|
|
- verifySvc *verifysvc.Service
|
|
|
- taskSvc *tasks.Service
|
|
|
- systemSvc *systemaction.Service
|
|
|
+ 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
|
|
|
}
|
|
|
|
|
|
-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, verifySvc *verifysvc.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, verifySvc: verifySvc, taskSvc: taskSvc, systemSvc: systemSvc}
|
|
|
+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 {
|
|
|
@@ -51,6 +59,8 @@ func (s *Server) Run(ctx context.Context) error {
|
|
|
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/apply", auth.Middleware(s.cfg, http.HandlerFunc(s.handleApply)))
|
|
|
+ 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)))
|
|
|
@@ -78,6 +88,50 @@ func (s *Server) Run(ctx context.Context) error {
|
|
|
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()
|
|
|
@@ -295,28 +349,103 @@ func (s *Server) runApplyTask(taskID string, input model.InterfaceConfig, manage
|
|
|
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, "ip", input.IP, "prefix", input.Prefix, "gateway", input.Gateway, "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.taskSvc.Update(taskID, "rolled_back", "rolling_back", "配置失败,已自动回滚。", true)
|
|
|
+ _ = s.interfaceSvc.EnsureMaintenanceAddress()
|
|
|
+ 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", "verifying", "正在验证配置结果。", false)
|
|
|
- if err := s.verifySvc.Verify(input); err != nil {
|
|
|
- _ = s.netplanSvc.Restore(filePath, backupPath)
|
|
|
- _ = s.applySvc.Apply()
|
|
|
- s.taskSvc.Update(taskID, "rolled_back", "rolling_back", "配置失败,已自动回滚。", true)
|
|
|
+ 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) 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.interfaceSvc.EnsureMaintenanceAddress()
|
|
|
+ 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
|
|
|
+}
|
|
|
|
|
|
- s.taskSvc.Update(taskID, "success", "completed", "目标接口配置已成功应用。", false)
|
|
|
+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) {
|