feat(network): implement peer discovery features and UI integration
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 28s
CI / web (push) Successful in 52s
CI / go (push) Successful in 2m22s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m36s

Added functionality for peer discovery, including new API endpoints for listing, approving, and rejecting discovered peers. Updated the network queries and settings to support peer discovery configurations. Enhanced the UI to display discovered peers and integrated related settings in the tenant settings component. Updated OpenAPI documentation to reflect the new endpoints and parameters. This improves the network management capabilities by allowing dynamic peer discovery and management.
This commit is contained in:
Denozordec
2026-07-30 22:31:59 +07:00
parent fd3a217cbe
commit 5fbe9c9b56
29 changed files with 1831 additions and 26 deletions
+7
View File
@@ -70,6 +70,13 @@ type Backend interface {
UpdatePeer(tenantID, id string, patch *PeerPatch) (*BGPPeer, error)
DeletePeer(tenantID, id string) error
// Peer discovery (dynamic BGP quarantine → approve/reject).
ListPeerDiscoveries(tenantID, status string) ([]*BGPPeerDiscovery, error)
GetPeerDiscovery(tenantID, id string) (*BGPPeerDiscovery, error)
UpsertPeerDiscovery(tenantID string, in *PeerDiscoveryUpsert) (*BGPPeerDiscovery, error)
ApprovePeerDiscovery(tenantID, id string, in *PeerDiscoveryApproveInput) (*BGPPeer, *BGPPeerDiscovery, error)
RejectPeerDiscovery(tenantID, id string) (*BGPPeerDiscovery, error)
ListSpeakersForTenant(tenantID string) []*Speaker
GetSpeaker(tenantID, speakerID string) (*Speaker, error)
GetSpeakerAnyTenant(speakerID string) (*Speaker, error)
+3
View File
@@ -33,6 +33,8 @@ type Memory struct {
peers map[string]*BGPPeer
peerDiscoveries map[string]*BGPPeerDiscovery
dohProfiles map[string]*DohProfile
communities map[string]*Community
cdnSources map[string]*CDNSource
@@ -143,6 +145,7 @@ func NewMemory() *Memory {
speakers: make(map[string]*Speaker),
publishedRevision: make(map[string]publishedInfo),
peers: make(map[string]*BGPPeer),
peerDiscoveries: make(map[string]*BGPPeerDiscovery),
dohProfiles: make(map[string]*DohProfile),
communities: make(map[string]*Community),
cdnSources: make(map[string]*CDNSource),
+213
View File
@@ -0,0 +1,213 @@
package store
import (
"sort"
"strings"
"time"
"github.com/google/uuid"
)
func (m *Memory) ListPeerDiscoveries(tenantID, status string) ([]*BGPPeerDiscovery, error) {
m.mu.RLock()
defer m.mu.RUnlock()
status = strings.TrimSpace(strings.ToLower(status))
var out []*BGPPeerDiscovery
for _, d := range m.peerDiscoveries {
if d == nil || d.TenantID != tenantID {
continue
}
if status != "" && !strings.EqualFold(d.Status, status) {
continue
}
out = append(out, copyPeerDiscovery(d))
}
sort.Slice(out, func(i, j int) bool {
return out[i].LastSeenAt.After(out[j].LastSeenAt)
})
return out, nil
}
func (m *Memory) GetPeerDiscovery(tenantID, id string) (*BGPPeerDiscovery, error) {
m.mu.RLock()
defer m.mu.RUnlock()
d, ok := m.peerDiscoveries[id]
if !ok || d.TenantID != tenantID {
return nil, ErrNotFound
}
return copyPeerDiscovery(d), nil
}
func (m *Memory) UpsertPeerDiscovery(tenantID string, in *PeerDiscoveryUpsert) (*BGPPeerDiscovery, error) {
if in == nil {
return nil, ErrInvalidInput
}
neighbor, ok := NormalizePeerNeighborString(in.Neighbor)
if !ok {
return nil, ErrInvalidInput
}
neighborID := strings.TrimSpace(in.NeighborID)
seenAt := in.SeenAt
if seenAt.IsZero() {
seenAt = time.Now().UTC()
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.tenants[tenantID]; !ok {
return nil, ErrTenantScope
}
existing := m.findPeerDiscoveryLocked(tenantID, neighborID, neighbor, in.RemoteASN)
if existing != nil {
if existing.Status == PeerDiscoveryRejected {
return copyPeerDiscovery(existing), nil
}
if existing.Status == PeerDiscoveryApproved {
existing.SessionState = strings.TrimSpace(in.SessionState)
existing.ProtocolName = strings.TrimSpace(in.ProtocolName)
existing.LastSeenAt = seenAt
if sp := strings.TrimSpace(in.SpeakerID); sp != "" {
existing.SpeakerID = sp
}
return copyPeerDiscovery(existing), nil
}
existing.Neighbor = neighbor
existing.RemoteASN = in.RemoteASN
if neighborID != "" {
existing.NeighborID = neighborID
}
existing.ProtocolName = strings.TrimSpace(in.ProtocolName)
existing.SessionState = strings.TrimSpace(in.SessionState)
existing.LastSeenAt = seenAt
if sp := strings.TrimSpace(in.SpeakerID); sp != "" {
existing.SpeakerID = sp
}
return copyPeerDiscovery(existing), nil
}
id := uuid.NewString()
d := &BGPPeerDiscovery{
ID: id,
TenantID: tenantID,
SpeakerID: strings.TrimSpace(in.SpeakerID),
NeighborID: neighborID,
Neighbor: neighbor,
RemoteASN: in.RemoteASN,
ProtocolName: strings.TrimSpace(in.ProtocolName),
SessionState: strings.TrimSpace(in.SessionState),
Status: PeerDiscoveryPending,
FirstSeenAt: seenAt,
LastSeenAt: seenAt,
}
m.peerDiscoveries[id] = d
return copyPeerDiscovery(d), nil
}
func (m *Memory) ApprovePeerDiscovery(tenantID, id string, in *PeerDiscoveryApproveInput) (*BGPPeer, *BGPPeerDiscovery, error) {
m.mu.Lock()
defer m.mu.Unlock()
d, ok := m.peerDiscoveries[id]
if !ok || d.TenantID != tenantID {
return nil, nil, ErrNotFound
}
if d.Status != PeerDiscoveryPending {
return nil, nil, ErrInvalidInput
}
if d.RemoteASN == 0 {
return nil, nil, ErrInvalidInput
}
neighbor, okN := NormalizePeerNeighborString(d.Neighbor)
if !okN {
return nil, nil, ErrInvalidInput
}
name := ""
enabled := true
var speakerID *string
if in != nil {
name = strings.TrimSpace(in.Name)
if in.Enabled != nil {
enabled = *in.Enabled
}
if in.SpeakerID != nil {
v := strings.TrimSpace(*in.SpeakerID)
if v != "" {
speakerID = &v
}
}
}
if name == "" {
if d.NeighborID != "" {
name = "discovered-" + d.NeighborID
} else {
name = "discovered-" + neighbor
}
}
if speakerID == nil && strings.TrimSpace(d.SpeakerID) != "" {
sp := d.SpeakerID
speakerID = &sp
}
peerID := uuid.NewString()
peer := &BGPPeer{
ID: peerID,
TenantID: tenantID,
SpeakerID: speakerID,
Name: name,
Neighbor: neighbor,
RemoteASN: d.RemoteASN,
Enabled: enabled,
SessionState: d.SessionState,
PoliciesJSON: "{}",
}
m.peers[peerID] = peer
d.Status = PeerDiscoveryApproved
d.ApprovedPeerID = peerID
d.LastSeenAt = time.Now().UTC()
peerCopy := *peer
return &peerCopy, copyPeerDiscovery(d), nil
}
func (m *Memory) RejectPeerDiscovery(tenantID, id string) (*BGPPeerDiscovery, error) {
m.mu.Lock()
defer m.mu.Unlock()
d, ok := m.peerDiscoveries[id]
if !ok || d.TenantID != tenantID {
return nil, ErrNotFound
}
if d.Status == PeerDiscoveryApproved {
return nil, ErrInvalidInput
}
d.Status = PeerDiscoveryRejected
d.LastSeenAt = time.Now().UTC()
return copyPeerDiscovery(d), nil
}
func (m *Memory) findPeerDiscoveryLocked(tenantID, neighborID, neighbor string, asn int64) *BGPPeerDiscovery {
for _, d := range m.peerDiscoveries {
if d == nil || d.TenantID != tenantID {
continue
}
if neighborID != "" && d.NeighborID == neighborID {
return d
}
if neighborID == "" && d.NeighborID == "" && d.Neighbor == neighbor && d.RemoteASN == asn {
return d
}
if neighborID != "" && d.NeighborID == "" && d.Neighbor == neighbor && d.RemoteASN == asn {
return d
}
}
return nil
}
func copyPeerDiscovery(d *BGPPeerDiscovery) *BGPPeerDiscovery {
if d == nil {
return nil
}
cp := *d
return &cp
}
+44
View File
@@ -0,0 +1,44 @@
package store
import "time"
// PeerDiscoveryStatus values for bgp_peer_discovery.status.
const (
PeerDiscoveryPending = "pending"
PeerDiscoveryApproved = "approved"
PeerDiscoveryRejected = "rejected"
)
// BGPPeerDiscovery is a live-detected dynamic BGP session awaiting operator action.
type BGPPeerDiscovery struct {
ID string `json:"id"`
TenantID string `json:"tenant_id,omitempty"`
SpeakerID string `json:"speaker_id,omitempty"`
NeighborID string `json:"neighbor_id,omitempty"`
Neighbor string `json:"neighbor"`
RemoteASN int64 `json:"remote_asn"`
ProtocolName string `json:"protocol_name,omitempty"`
SessionState string `json:"session_state,omitempty"`
Status string `json:"status"`
FirstSeenAt time.Time `json:"first_seen_at"`
LastSeenAt time.Time `json:"last_seen_at"`
ApprovedPeerID string `json:"approved_peer_id,omitempty"`
}
// PeerDiscoveryUpsert is input for syncing a live dynamic session into the store.
type PeerDiscoveryUpsert struct {
SpeakerID string
NeighborID string
Neighbor string
RemoteASN int64
ProtocolName string
SessionState string
SeenAt time.Time
}
// PeerDiscoveryApproveInput optional fields when promoting a discovery to BGPPeer.
type PeerDiscoveryApproveInput struct {
Name string
SpeakerID *string
Enabled *bool
}