Przeglądaj źródła

fix(sdwan): allow editing and state toggle of existing group members (v1.8.5)

Proxmox Router 1 dzień temu
rodzic
commit
0126c397f8

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.8.4
+1.8.5

BIN
bin/debtik


BIN
bin/debtik-arm64


+ 3 - 1
internal/api/api.go

@@ -298,7 +298,9 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("GET /api/sdwan/groups/{id}", s.handleGetSDWANGroup)
 	s.mux.HandleFunc("DELETE /api/sdwan/groups/{id}", s.handleDeleteSDWANGroup)
 	s.mux.HandleFunc("POST /api/sdwan/groups/{id}/state", s.handleSetSDWANGroupState)
-	s.mux.HandleFunc("POST /api/sdwan/groups/{id}/members", s.handleAddSDWANMember)
+	s.mux.HandleFunc("POST /api/sdwan/groups/{id}/members", s.handleSaveSDWANMember)
+	s.mux.HandleFunc("PUT /api/sdwan/groups/{id}/members", s.handleSaveSDWANMember)
+	s.mux.HandleFunc("PUT /api/sdwan/groups/{id}/members/{iface}", s.handleSaveSDWANMember)
 	s.mux.HandleFunc("DELETE /api/sdwan/groups/{id}/members/{iface}", s.handleDeleteSDWANMember)
 	s.mux.HandleFunc("GET /api/sdwan/telemetry", s.handleGetSDWANTelemetry)
 

+ 31 - 2
internal/api/sdwan_handlers.go

@@ -88,7 +88,7 @@ func (s *Server) handleSetSDWANGroupState(w http.ResponseWriter, r *http.Request
 	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": id, "disabled": req.Disabled})
 }
 
-func (s *Server) handleAddSDWANMember(w http.ResponseWriter, r *http.Request) {
+func (s *Server) handleSaveSDWANMember(w http.ResponseWriter, r *http.Request) {
 	if s.sdwanMgr == nil {
 		writeError(w, http.StatusServiceUnavailable, "sd-wan subsystem unavailable")
 		return
@@ -100,10 +100,38 @@ func (s *Server) handleAddSDWANMember(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	if mem.Interface == "" {
+		if iface := r.PathValue("iface"); iface != "" {
+			mem.Interface = iface
+		}
+	}
+	if mem.Interface == "" && mem.ID == "" {
 		writeError(w, http.StatusBadRequest, "interface is required")
 		return
 	}
-	if err := s.sdwanMgr.AddMember(id, &mem); err != nil {
+
+	isUpdate := r.Method == http.MethodPut
+	if !isUpdate {
+		if g, ok := s.sdwanMgr.Get(id); ok && g != nil {
+			for _, existing := range g.Members {
+				if (mem.ID != "" && existing.ID == mem.ID) || (mem.Interface != "" && existing.Interface == mem.Interface) {
+					isUpdate = true
+					if mem.ID == "" {
+						mem.ID = existing.ID
+					}
+					break
+				}
+			}
+		}
+	}
+
+	var err error
+	if isUpdate {
+		err = s.sdwanMgr.UpdateMember(id, &mem)
+	} else {
+		err = s.sdwanMgr.AddMember(id, &mem)
+	}
+
+	if err != nil {
 		writeError(w, http.StatusBadRequest, err.Error())
 		return
 	}
@@ -113,6 +141,7 @@ func (s *Server) handleAddSDWANMember(w http.ResponseWriter, r *http.Request) {
 		"interface": mem.Interface,
 		"weight":    mem.Weight,
 		"priority":  mem.Priority,
+		"disabled":  mem.Disabled,
 	})
 }
 

+ 25 - 0
internal/api/sdwan_handlers_test.go

@@ -96,6 +96,31 @@ func TestSDWANAPIEndpoints(t *testing.T) {
 		t.Fatalf("POST /api/sdwan/groups/%s/members status = %d; body = %s", groupID, rec.Code, rec.Body.String())
 	}
 
+	// 4a. PUT /api/sdwan/groups/{id}/members (Update member via PUT)
+	memPayload["weight"] = 75
+	body, _ = json.Marshal(memPayload)
+	req = httptest.NewRequest(http.MethodPut, "/api/sdwan/groups/"+groupID+"/members", bytes.NewReader(body))
+	rec = httptest.NewRecorder()
+	srv.mux.ServeHTTP(rec, req)
+	if rec.Code != http.StatusOK {
+		t.Fatalf("PUT /api/sdwan/groups/%s/members status = %d; body = %s", groupID, rec.Code, rec.Body.String())
+	}
+
+	// 4b. POST /api/sdwan/groups/{id}/members (Update member via POST when already exists)
+	memPayload["weight"] = 85
+	body, _ = json.Marshal(memPayload)
+	req = httptest.NewRequest(http.MethodPost, "/api/sdwan/groups/"+groupID+"/members", bytes.NewReader(body))
+	rec = httptest.NewRecorder()
+	srv.mux.ServeHTTP(rec, req)
+	if rec.Code != http.StatusOK {
+		t.Fatalf("POST (update) /api/sdwan/groups/%s/members status = %d; body = %s", groupID, rec.Code, rec.Body.String())
+	}
+
+	gAfterUpdate, _ := sdwanMgr.Get(groupID)
+	if len(gAfterUpdate.Members) != 1 || gAfterUpdate.Members[0].Weight != 85 {
+		t.Fatalf("expected member weight 85, got %+v", gAfterUpdate.Members)
+	}
+
 	// 5. GET /api/sdwan/telemetry
 	req = httptest.NewRequest(http.MethodGet, "/api/sdwan/telemetry", nil)
 	rec = httptest.NewRecorder()

+ 27 - 1
internal/sdwan/manager.go

@@ -295,7 +295,7 @@ func (m *Manager) UpdateMember(groupIDOrName string, mem *SDWANMember) error {
 
 	idx := -1
 	for i, existing := range g.Members {
-		if existing.ID == mem.ID || existing.Interface == mem.Interface {
+		if (mem.ID != "" && existing.ID == mem.ID) || (mem.Interface != "" && existing.Interface == mem.Interface) {
 			idx = i
 			break
 		}
@@ -305,8 +305,34 @@ func (m *Manager) UpdateMember(groupIDOrName string, mem *SDWANMember) error {
 		return fmt.Errorf("member %s not found in group %s", mem.Interface, g.Name)
 	}
 
+	// Preserve existing ID & Interface if not specified
+	if mem.ID == "" {
+		mem.ID = g.Members[idx].ID
+	}
+	if mem.Interface == "" {
+		mem.Interface = g.Members[idx].Interface
+	}
+	if mem.Weight <= 0 {
+		mem.Weight = g.Members[idx].Weight
+	}
+	if mem.Priority <= 0 {
+		mem.Priority = g.Members[idx].Priority
+	}
+	if mem.TargetProbe == "" {
+		mem.TargetProbe = g.Members[idx].TargetProbe
+	}
+	if mem.MaxLatency <= 0 {
+		mem.MaxLatency = g.Members[idx].MaxLatency
+	}
+	if mem.MaxLoss <= 0 {
+		mem.MaxLoss = g.Members[idx].MaxLoss
+	}
+
 	// Preserve live telemetry
 	mem.Status = g.Members[idx].Status
+	if mem.Status == "" {
+		mem.Status = StatusHealthy
+	}
 	mem.LiveLatency = g.Members[idx].LiveLatency
 	mem.PacketLoss = g.Members[idx].PacketLoss
 	mem.Jitter = g.Members[idx].Jitter

+ 16 - 4
internal/web/static/app.js

@@ -19353,8 +19353,19 @@ async function saveSDWANMember(event) {
   event.preventDefault();
 
   const id = document.getElementById('sdwan-member-id').value;
-  const groupId = document.getElementById('sdwan-member-group-id').value;
-  const iface = document.getElementById('sdwan-member-interface').value;
+  let groupId = document.getElementById('sdwan-member-group-id').value;
+  let iface = document.getElementById('sdwan-member-interface').value;
+
+  // In edit mode with disabled selects, fallback to existing member data if needed
+  if (id) {
+    const existingGroup = (state.sdwanGroups || []).find(g => (g.members || []).some(m => m.id === id));
+    if (existingGroup) {
+      if (!groupId) groupId = existingGroup.id;
+      const mem = (existingGroup.members || []).find(m => m.id === id);
+      if (mem && !iface) iface = mem.interface;
+    }
+  }
+
   const gateway = document.getElementById('sdwan-member-gateway').value.trim();
   const weight = parseInt(document.getElementById('sdwan-member-weight').value, 10) || 50;
   const priority = parseInt(document.getElementById('sdwan-member-priority').value, 10) || 1;
@@ -19376,8 +19387,9 @@ async function saveSDWANMember(event) {
   };
 
   try {
+    const method = id ? 'PUT' : 'POST';
     const res = await fetch(`${API_BASE}/sdwan/groups/${groupId}/members`, {
-      method: 'POST',
+      method,
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify(payload)
     });
@@ -19408,7 +19420,7 @@ async function toggleSDWANMemberState(groupId, memberId, currentState) {
 
   try {
     const res = await fetch(`${API_BASE}/sdwan/groups/${groupId}/members`, {
-      method: 'POST',
+      method: 'PUT',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify(payload)
     });