From 8a50ba44b34d0b35c39e0ce13851cf7aae64d7be Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 9 Apr 2026 15:39:01 +0700 Subject: [PATCH] feat: add live BGP protocol state tracking to peer listing Enhanced the peer listing API to include live BGP session states by integrating a new function to retrieve protocol states from the BIRD socket. Added parsing logic for BGP protocol states and updated the peer JSON response to include session state information. Introduced unit tests for the new parsing functionality to ensure accuracy in state retrieval. --- internal/httpapi/routes.go | 76 ++++++++++++++++++++++++++++++++- internal/httpapi/server_test.go | 16 +++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/internal/httpapi/routes.go b/internal/httpapi/routes.go index 4135c71..c39562f 100644 --- a/internal/httpapi/routes.go +++ b/internal/httpapi/routes.go @@ -319,15 +319,89 @@ func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) { return } peers := s.store.ListPeers(a.TenantID) + liveStates := s.liveBGPProtocolStates(r.Context()) items := make([]map[string]any, 0, len(peers)) for _, p := range peers { - items = append(items, peerJSON(p)) + row := peerJSON(p) + if st, ok := liveStates[peerProtocolNameForID(p.ID)]; ok && strings.TrimSpace(st) != "" { + row["session_state"] = strings.TrimSpace(st) + } + items = append(items, row) } writeJSON(w, http.StatusOK, map[string]any{ "items": items, "next_cursor": nil, "has_more": false, }) } +func (s *Server) liveBGPProtocolStates(ctx context.Context) map[string]string { + sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET")) + if sock == "" { + return map[string]string{} + } + out, err := birdfmt.ShowProtocols(ctx, sock, strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN"))) + if err != nil { + return map[string]string{} + } + return parseBGPProtocolStates(out) +} + +// parseBGPProtocolStates parses `birdc show protocols all` summary rows into protocol_name -> state. +func parseBGPProtocolStates(output string) map[string]string { + out := make(map[string]string) + for _, raw := range strings.Split(output, "\n") { + line := strings.TrimSpace(raw) + if line == "" { + continue + } + low := strings.ToLower(line) + if strings.HasPrefix(low, "bird ") || strings.HasPrefix(low, "name ") || strings.HasPrefix(low, "table ") { + continue + } + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + if !strings.EqualFold(fields[1], "BGP") { + continue + } + state := extractBGPSessionState(line) + if state == "" { + state = fields[3] + } + out[fields[0]] = state + } + return out +} + +func extractBGPSessionState(line string) string { + known := []string{ + "Established", + "Idle", + "Connect", + "Active", + "OpenSent", + "OpenConfirm", + } + for _, st := range known { + if strings.Contains(line, st) { + return st + } + } + return "" +} + +// peerProtocolNameForID must stay in sync with pipeline peer protocol naming. +func peerProtocolNameForID(peerID string) string { + s := strings.ReplaceAll(strings.TrimSpace(peerID), "-", "") + if len(s) > 16 { + s = s[:16] + } + if s == "" { + s = "x" + } + return "evobgp_p_" + s +} + func (s *Server) handleListSpeakers(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok { diff --git a/internal/httpapi/server_test.go b/internal/httpapi/server_test.go index c80ccbc..8971a7d 100644 --- a/internal/httpapi/server_test.go +++ b/internal/httpapi/server_test.go @@ -341,3 +341,19 @@ func waitJob(t *testing.T, client *http.Client, base, token, jobID string) { } t.Fatalf("job %s did not complete", jobID) } + +func TestParseBGPProtocolStates(t *testing.T) { + raw := `BIRD 2.16.2 ready. +name proto table state since info +device1 Device --- up 2026-04-09 +evobgp_p_abcdef01 BGP master up 2026-04-09 Established +evobgp_p_01234567 BGP master start 2026-04-09 Connect +` + got := parseBGPProtocolStates(raw) + if got["evobgp_p_abcdef01"] != "Established" { + t.Fatalf("expected protocol state Established, got %q", got["evobgp_p_abcdef01"]) + } + if got["evobgp_p_01234567"] != "Connect" { + t.Fatalf("expected protocol state Connect, got %q", got["evobgp_p_01234567"]) + } +}