|
|
@@ -0,0 +1,305 @@
|
|
|
+package httpserver
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "net/http"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "quickip/internal/auth"
|
|
|
+ "quickip/internal/config"
|
|
|
+ "quickip/internal/deviceinfo"
|
|
|
+ "quickip/internal/logger"
|
|
|
+ "quickip/internal/model"
|
|
|
+ applyexecsvc "quickip/internal/network/applyexec"
|
|
|
+ configreadersvc "quickip/internal/network/configreader"
|
|
|
+ interfacesvc "quickip/internal/network/interfaces"
|
|
|
+ netplansvc "quickip/internal/network/netplan"
|
|
|
+ validatorsvc "quickip/internal/network/validator"
|
|
|
+ verifysvc "quickip/internal/network/verify"
|
|
|
+ "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
|
|
|
+}
|
|
|
+
|
|
|
+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) *Server {
|
|
|
+ return &Server{cfg: cfg, log: log, deviceSvc: deviceSvc, interfaceSvc: interfaceSvc, configSvc: configSvc, validatorSvc: validatorSvc, netplanSvc: netplanSvc, applySvc: applySvc, verifySvc: verifySvc, taskSvc: taskSvc}
|
|
|
+}
|
|
|
+
|
|
|
+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/apply", auth.Middleware(s.cfg, http.HandlerFunc(s.handleApply)))
|
|
|
+ mux.Handle("/api/network/rollback", auth.Middleware(s.cfg, http.HandlerFunc(s.handleRollback)))
|
|
|
+ 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) 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": "运行中", "agent_version": s.cfg.AgentVersion}})
|
|
|
+}
|
|
|
+
|
|
|
+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)
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ 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) handleRollback(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.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": {"请求体格式不正确。"}}})
|
|
|
+ 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 + ".quickip.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) 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
|
|
|
+ }
|
|
|
+ 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
|
|
|
+ }
|
|
|
+
|
|
|
+ s.taskSvc.Update(taskID, "running", "applying", "正在应用 netplan 配置。", false)
|
|
|
+ if err := s.applySvc.Apply(); err != nil {
|
|
|
+ _ = s.netplanSvc.Restore(filePath, backupPath)
|
|
|
+ _ = s.applySvc.Apply()
|
|
|
+ s.taskSvc.Update(taskID, "rolled_back", "rolling_back", "配置失败,已自动回滚。", 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)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ s.taskSvc.Update(taskID, "success", "completed", "目标接口配置已成功应用。", 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)
|
|
|
+}
|