Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <[email protected]>
365 lines
9.5 KiB
Go
365 lines
9.5 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/birdfmt"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
type speakerBGPLive struct {
|
|
SpeakerID string
|
|
Label string
|
|
Sessions []birdfmt.BGPSession
|
|
Error string
|
|
Polled bool
|
|
ObservedAt time.Time
|
|
ReceivedAt time.Time
|
|
LastSuccessAt time.Time
|
|
Source string
|
|
ObservationOverride *liveObservation
|
|
}
|
|
|
|
type liveObservation struct {
|
|
ObservedAt *string `json:"observed_at"`
|
|
ReceivedAt *string `json:"received_at"`
|
|
LastSuccessAt *string `json:"last_success_at"`
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
Source string `json:"source"`
|
|
Quality string `json:"quality"`
|
|
ErrorCode string `json:"error_code,omitempty"`
|
|
}
|
|
|
|
func liveTime(t time.Time) *string {
|
|
if t.IsZero() {
|
|
return nil
|
|
}
|
|
v := t.UTC().Format(time.RFC3339Nano)
|
|
return &v
|
|
}
|
|
|
|
func (v speakerBGPLive) observed() bool {
|
|
return v.Polled || v.Error != "" || len(v.Sessions) > 0
|
|
}
|
|
|
|
func (v speakerBGPLive) successful() bool {
|
|
return v.observed() && v.Error == ""
|
|
}
|
|
|
|
func (v speakerBGPLive) observation() liveObservation {
|
|
if v.ObservationOverride != nil {
|
|
return *v.ObservationOverride
|
|
}
|
|
o := liveObservation{ObservedAt: liveTime(v.ObservedAt), ReceivedAt: liveTime(v.ReceivedAt), LastSuccessAt: liveTime(v.LastSuccessAt), Source: v.Source, Quality: "unknown"}
|
|
if o.Source == "" {
|
|
o.Source = "none"
|
|
}
|
|
switch {
|
|
case v.successful():
|
|
o.Quality = "ok"
|
|
case v.Error != "" && !v.LastSuccessAt.IsZero():
|
|
o.Quality = "stale"
|
|
o.ErrorCode = liveErrorCode(v.Error)
|
|
case v.Error != "":
|
|
o.Quality = "unreachable"
|
|
o.ErrorCode = liveErrorCode(v.Error)
|
|
default:
|
|
o.ErrorCode = "not_polled"
|
|
}
|
|
return o
|
|
}
|
|
|
|
func liveErrorCode(err string) string {
|
|
if strings.Contains(strings.ToLower(err), "not configured") || strings.Contains(strings.ToLower(err), "missing") {
|
|
return "not_configured"
|
|
}
|
|
return "poll_failed"
|
|
}
|
|
|
|
type peerSessionOnSpeaker struct {
|
|
SpeakerID string `json:"speaker_id"`
|
|
Label string `json:"label"`
|
|
State string `json:"state"`
|
|
PollError string `json:"poll_error,omitempty"`
|
|
Observation liveObservation `json:"observation"`
|
|
}
|
|
|
|
type liveSpeakerPoll struct {
|
|
SpeakerID string `json:"speaker_id"`
|
|
Label string `json:"label"`
|
|
OK bool `json:"ok"`
|
|
SessionCount *int `json:"session_count"`
|
|
PollError string `json:"poll_error,omitempty"`
|
|
Observation liveObservation `json:"observation"`
|
|
}
|
|
|
|
func speakerDisplayLabel(sp *store.Speaker) string {
|
|
if sp == nil {
|
|
return ""
|
|
}
|
|
meta := store.ParseSpeakerMeta(sp.MetaJSON)
|
|
host := strings.TrimSpace(meta.AgentDomain)
|
|
if host == "" {
|
|
host = strings.TrimSpace(sp.Endpoint)
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(sp.Role), "master") {
|
|
if host != "" {
|
|
return "CP · " + host
|
|
}
|
|
return "CP (master)"
|
|
}
|
|
if host != "" {
|
|
return host
|
|
}
|
|
return sp.ID
|
|
}
|
|
|
|
func liveSpeakerPollJSON(views []speakerBGPLive) []liveSpeakerPoll {
|
|
out := make([]liveSpeakerPoll, 0, len(views))
|
|
for _, v := range views {
|
|
var sessionCount *int
|
|
if v.successful() {
|
|
count := len(v.Sessions)
|
|
sessionCount = &count
|
|
}
|
|
out = append(out, liveSpeakerPoll{
|
|
SpeakerID: v.SpeakerID,
|
|
Label: v.Label,
|
|
OK: v.successful(),
|
|
SessionCount: sessionCount,
|
|
PollError: v.Error,
|
|
Observation: v.observation(),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func findPeerSession(sessions []birdfmt.BGPSession, protoName string, neighbor netip.Addr, hasNeighbor bool) *birdfmt.BGPSession {
|
|
for i := range sessions {
|
|
if peerSessionMatches(sessions[i], protoName, neighbor, hasNeighbor) {
|
|
return &sessions[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func matchPeerOnSpeakers(peer *store.BGPPeer, views []speakerBGPLive) (
|
|
bestState string,
|
|
connectedID string,
|
|
connectedLabel string,
|
|
establishedOn []peerSessionOnSpeaker,
|
|
on []peerSessionOnSpeaker,
|
|
mismatch bool,
|
|
) {
|
|
if peer == nil {
|
|
return "", "", "", nil, nil, false
|
|
}
|
|
neighbor, hasNeighbor := store.ParsePeerNeighbor(peer.Neighbor)
|
|
protoName := birdfmt.PeerProtocolName(peer.ID)
|
|
|
|
for _, v := range views {
|
|
if !v.successful() {
|
|
on = append(on, peerSessionOnSpeaker{
|
|
SpeakerID: v.SpeakerID,
|
|
Label: v.Label,
|
|
PollError: v.Error,
|
|
Observation: v.observation(),
|
|
})
|
|
continue
|
|
}
|
|
sess := findPeerSession(v.Sessions, protoName, neighbor, hasNeighbor)
|
|
if sess == nil {
|
|
on = append(on, peerSessionOnSpeaker{
|
|
SpeakerID: v.SpeakerID,
|
|
Label: v.Label,
|
|
State: "absent",
|
|
Observation: v.observation(),
|
|
})
|
|
continue
|
|
}
|
|
hit := peerSessionOnSpeaker{
|
|
SpeakerID: v.SpeakerID,
|
|
Label: v.Label,
|
|
State: sess.State,
|
|
Observation: v.observation(),
|
|
}
|
|
on = append(on, hit)
|
|
if strings.EqualFold(strings.TrimSpace(sess.State), "Established") {
|
|
establishedOn = append(establishedOn, hit)
|
|
}
|
|
if bestState == "" || sessionStateRank(sess.State) > sessionStateRank(bestState) {
|
|
bestState = sess.State
|
|
}
|
|
}
|
|
|
|
if len(establishedOn) > 0 {
|
|
bestState = "Established"
|
|
labels := make([]string, 0, len(establishedOn))
|
|
for _, e := range establishedOn {
|
|
labels = append(labels, e.Label)
|
|
}
|
|
connectedLabel = strings.Join(labels, ", ")
|
|
if len(establishedOn) == 1 {
|
|
connectedID = establishedOn[0].SpeakerID
|
|
}
|
|
} else if len(on) == 1 && on[0].PollError == "" && on[0].State != "" {
|
|
connectedID = on[0].SpeakerID
|
|
connectedLabel = on[0].Label
|
|
}
|
|
|
|
if peer.SpeakerID != nil && strings.TrimSpace(*peer.SpeakerID) != "" && len(establishedOn) > 0 {
|
|
want := strings.TrimSpace(*peer.SpeakerID)
|
|
found := false
|
|
for _, e := range establishedOn {
|
|
if strings.EqualFold(strings.TrimSpace(e.SpeakerID), want) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
mismatch = !found
|
|
}
|
|
return bestState, connectedID, connectedLabel, establishedOn, on, mismatch
|
|
}
|
|
|
|
func peerSessionMatches(sess birdfmt.BGPSession, protoName string, neighbor netip.Addr, hasNeighbor bool) bool {
|
|
if strings.EqualFold(strings.TrimSpace(sess.Name), protoName) {
|
|
return true
|
|
}
|
|
if !hasNeighbor || strings.TrimSpace(sess.Neighbor) == "" {
|
|
return false
|
|
}
|
|
peerAddr, ok := store.ParsePeerNeighbor(sess.Neighbor)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return peerAddr == neighbor
|
|
}
|
|
|
|
func sessionStateRank(state string) int {
|
|
switch strings.ToLower(strings.TrimSpace(state)) {
|
|
case "established":
|
|
return 100
|
|
case "openconfirm", "opensent":
|
|
return 80
|
|
case "active", "connect":
|
|
return 60
|
|
case "idle":
|
|
return 20
|
|
default:
|
|
return 10
|
|
}
|
|
}
|
|
|
|
func applyPeerLiveFields(row map[string]any, peer *store.BGPPeer, views []speakerBGPLive) {
|
|
expected := expectedPeerViews(peer, views)
|
|
state, _, _, _, _, _ := matchPeerOnSpeakers(peer, expected)
|
|
_, connID, connLabel, establishedOn, on, mismatch := matchPeerOnSpeakers(peer, views)
|
|
row["session_on_speakers"] = on
|
|
row["established_on_speakers"] = establishedOn
|
|
row["session_conflict"] = false
|
|
row["session_mismatch"] = mismatch
|
|
row["last_known_session_state"] = row["session_state"]
|
|
row["session_state"] = nil
|
|
if state != "" {
|
|
row["session_state"] = state
|
|
}
|
|
if state == "" && len(expected) > 0 && allSpeakerPollsSuccessful(expected) {
|
|
row["session_state"] = "absent"
|
|
}
|
|
row["live_observation"] = peerLiveObservation(expected)
|
|
if connLabel != "" {
|
|
row["connected_speaker_label"] = connLabel
|
|
}
|
|
row["connected_speaker_id"] = peerLiveSpeakerIDOrNull(connID)
|
|
}
|
|
|
|
func expectedPeerViews(peer *store.BGPPeer, views []speakerBGPLive) []speakerBGPLive {
|
|
if peer == nil || peer.SpeakerID == nil || strings.TrimSpace(*peer.SpeakerID) == "" {
|
|
return views
|
|
}
|
|
want := strings.TrimSpace(*peer.SpeakerID)
|
|
expected := make([]speakerBGPLive, 0, 1)
|
|
for _, v := range views {
|
|
if strings.EqualFold(v.SpeakerID, want) {
|
|
expected = append(expected, v)
|
|
}
|
|
}
|
|
return expected
|
|
}
|
|
|
|
func allSpeakerPollsSuccessful(views []speakerBGPLive) bool {
|
|
for _, v := range views {
|
|
if !v.successful() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func peerLiveObservation(views []speakerBGPLive) liveObservation {
|
|
if len(views) == 0 {
|
|
return speakerBGPLive{}.observation()
|
|
}
|
|
result := liveObservation{Source: views[0].Source, Quality: "unknown"}
|
|
if result.Source == "" {
|
|
result.Source = "none"
|
|
}
|
|
var latestObserved, latestReceived, latestSuccess time.Time
|
|
allOK, allFailed, anyStale := true, true, false
|
|
for _, v := range views {
|
|
o := v.observation()
|
|
if v.ObservedAt.After(latestObserved) {
|
|
latestObserved = v.ObservedAt
|
|
}
|
|
if v.ReceivedAt.After(latestReceived) {
|
|
latestReceived = v.ReceivedAt
|
|
}
|
|
if v.LastSuccessAt.After(latestSuccess) {
|
|
latestSuccess = v.LastSuccessAt
|
|
}
|
|
allOK = allOK && v.successful()
|
|
allFailed = allFailed && o.Quality == "unreachable"
|
|
anyStale = anyStale || o.Quality == "stale"
|
|
if result.ErrorCode == "" && o.ErrorCode != "" {
|
|
result.ErrorCode = o.ErrorCode
|
|
}
|
|
if o.ExpiresAt != nil {
|
|
expires, err := time.Parse(time.RFC3339Nano, *o.ExpiresAt)
|
|
if err == nil {
|
|
previous := time.Time{}
|
|
if result.ExpiresAt != nil {
|
|
previous, _ = time.Parse(time.RFC3339Nano, *result.ExpiresAt)
|
|
}
|
|
if previous.IsZero() || expires.Before(previous) {
|
|
result.ExpiresAt = o.ExpiresAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
result.ObservedAt = liveTime(latestObserved)
|
|
result.ReceivedAt = liveTime(latestReceived)
|
|
result.LastSuccessAt = liveTime(latestSuccess)
|
|
if len(views) > 1 {
|
|
result.Source = "mixed"
|
|
}
|
|
switch {
|
|
case allOK:
|
|
result.Quality = "ok"
|
|
result.ErrorCode = ""
|
|
case anyStale:
|
|
result.Quality = "stale"
|
|
case allFailed:
|
|
result.Quality = "unreachable"
|
|
}
|
|
return result
|
|
}
|
|
|
|
func peerLiveSpeakerIDOrNull(id string) any {
|
|
if strings.TrimSpace(id) == "" {
|
|
return nil
|
|
}
|
|
return id
|
|
}
|