Kaynağa Gözat

feat(bgp): parse and display live BGP peer session uptime, bump version to 1.3.10

Proxmox Router 1 gün önce
ebeveyn
işleme
ebf3409f24

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.3.9
+1.3.10

+ 56 - 0
internal/dynamicrouting/bgp_test.go

@@ -3,6 +3,7 @@ package dynamicrouting
 import (
 	"strings"
 	"testing"
+	"time"
 )
 
 func TestBGPConfigGeneration(t *testing.T) {
@@ -51,3 +52,58 @@ func TestBGPConfigGeneration(t *testing.T) {
 
 	_ = sb
 }
+
+func TestParseBirdSinceToUptime(t *testing.T) {
+	refTime := time.Date(2026, 11, 2, 12, 0, 0, 0, time.Local)
+
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "ISO long same day",
+			input:    "2026-11-02 10:06:15",
+			expected: "01h 53m 45s",
+		},
+		{
+			name:     "ISO long with milliseconds",
+			input:    "2026-11-02 10:06:15.123",
+			expected: "01h 53m 44s",
+		},
+		{
+			name:     "ISO long previous day",
+			input:    "2026-10-31 13:15:43",
+			expected: "1d 22h 44m 17s",
+		},
+		{
+			name:     "Date only",
+			input:    "2026-10-31",
+			expected: "2d 12h 00m 00s",
+		},
+		{
+			name:     "Time only with ms",
+			input:    "10:06:15.631",
+			expected: "01h 53m 45s",
+		},
+		{
+			name:     "Empty",
+			input:    "",
+			expected: "",
+		},
+		{
+			name:     "Invalid",
+			input:    "not-a-date",
+			expected: "",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			got := parseBirdSinceToUptime(tc.input, refTime)
+			if got != tc.expected {
+				t.Errorf("%s: got %q, want %q", tc.name, got, tc.expected)
+			}
+		})
+	}
+}

+ 71 - 5
internal/dynamicrouting/manager.go

@@ -202,6 +202,9 @@ func (m *Manager) applyBirdConfig() error {
 
 	sb.WriteString("# /etc/bird/bird.conf\n")
 	sb.WriteString("# Generated dynamically by Proxmox Router (BIRD 2 Engine)\n\n")
+	sb.WriteString("timeformat protocol iso long;\n")
+	sb.WriteString("timeformat base iso long;\n")
+	sb.WriteString("timeformat log iso long;\n")
 	sb.WriteString("log syslog all;\n")
 	sb.WriteString(fmt.Sprintf("router id %s;\n\n", routerID))
 
@@ -818,13 +821,23 @@ func (m *Manager) ListBGPPeers() []BGPPeer {
 		lines := strings.Split(string(out), "\n")
 
 		var cur *livePeerData
+		now := time.Now()
 		for _, line := range lines {
 			trimmed := strings.TrimSpace(line)
-			if strings.Contains(line, " BGP ") {
-				cur = &livePeerData{}
+			// A protocol header line in BIRD is unindented and has "BGP" in column 2
+			if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
 				fields := strings.Fields(line)
-				if len(fields) >= 5 {
-					cur.Uptime = fields[4]
+				if len(fields) >= 2 && fields[1] == "BGP" {
+					cur = &livePeerData{}
+					var sinceStr string
+					if len(fields) >= 6 && strings.Contains(fields[4], "-") && strings.Contains(fields[5], ":") {
+						sinceStr = fields[4] + " " + fields[5]
+					} else if len(fields) >= 5 {
+						sinceStr = fields[4]
+					}
+					cur.Uptime = parseBirdSinceToUptime(sinceStr, now)
+				} else {
+					cur = nil
 				}
 			}
 			if cur == nil {
@@ -863,7 +876,11 @@ func (m *Manager) ListBGPPeers() []BGPPeer {
 		for i := range peers {
 			if live, ok := liveMap[peers[i].RemoteAddress]; ok {
 				peers[i].State = live.State
-				peers[i].Uptime = live.Uptime
+				if live.State == "Established" {
+					peers[i].Uptime = live.Uptime
+				} else {
+					peers[i].Uptime = ""
+				}
 				peers[i].PrefixReceived = live.PfxRcd
 				peers[i].PrefixSent = live.PfxSnt
 				if peers[i].LocalAS == 0 && live.LocalAS > 0 {
@@ -871,8 +888,10 @@ func (m *Manager) ListBGPPeers() []BGPPeer {
 				}
 			} else if peers[i].Disabled {
 				peers[i].State = "Disabled"
+				peers[i].Uptime = ""
 			} else {
 				peers[i].State = "Idle"
+				peers[i].Uptime = ""
 			}
 		}
 	}
@@ -880,6 +899,53 @@ func (m *Manager) ListBGPPeers() []BGPPeer {
 	return peers
 }
 
+func parseBirdSinceToUptime(sinceStr string, now time.Time) string {
+	sinceStr = strings.TrimSpace(sinceStr)
+	if sinceStr == "" {
+		return ""
+	}
+	// Case 1: "2006-01-02 15:04:05" (ISO long)
+	if t, err := time.ParseInLocation("2006-01-02 15:04:05", sinceStr, time.Local); err == nil {
+		return formatDuration(now.Sub(t))
+	}
+	// Case 2: "2006-01-02 15:04:05.999" (ISO long with milliseconds)
+	if t, err := time.ParseInLocation("2006-01-02 15:04:05.999", sinceStr, time.Local); err == nil {
+		return formatDuration(now.Sub(t))
+	}
+	// Case 3: "2006-01-02" (date only)
+	if t, err := time.ParseInLocation("2006-01-02", sinceStr, time.Local); err == nil {
+		return formatDuration(now.Sub(t))
+	}
+	// Case 4: "15:04:05" or "15:04:05.123" (time only, happened today)
+	timeOnly := sinceStr
+	if idx := strings.IndexByte(timeOnly, '.'); idx != -1 {
+		timeOnly = timeOnly[:idx]
+	}
+	if t, err := time.ParseInLocation("15:04:05", timeOnly, time.Local); err == nil {
+		tToday := time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), t.Second(), 0, time.Local)
+		if tToday.After(now) {
+			tToday = tToday.AddDate(0, 0, -1)
+		}
+		return formatDuration(now.Sub(tToday))
+	}
+	return ""
+}
+
+func formatDuration(d time.Duration) string {
+	if d < 0 {
+		d = 0
+	}
+	sec := int64(d.Seconds())
+	days := sec / 86400
+	hours := (sec % 86400) / 3600
+	mins := (sec % 3600) / 60
+	secs := sec % 60
+	if days > 0 {
+		return fmt.Sprintf("%dd %02dh %02dm %02ds", days, hours, mins, secs)
+	}
+	return fmt.Sprintf("%02dh %02dm %02ds", hours, mins, secs)
+}
+
 func (m *Manager) SaveBGPPeer(peer *BGPPeer) error {
 	m.mu.Lock()
 	defer m.mu.Unlock()