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"]) + } +}