Add AS peers display to AS detail page
- Added GetASPeers method to database to fetch all peering relationships - Updated AS detail handler to fetch and pass peers to template - Added peers section to AS detail page showing all peer ASNs with their info - Added peer count to the info cards at the top of the page - Shows handle, description, and first/last seen dates for each peer
This commit is contained in:
parent
deeedae180
commit
7aec01c499
@ -1435,6 +1435,65 @@ func (d *Database) GetASDetailsContext(ctx context.Context, asn int) (*ASN, []Li
|
|||||||
return &asnInfo, allPrefixes, nil
|
return &asnInfo, allPrefixes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ASPeer represents a peering relationship with another AS
|
||||||
|
type ASPeer struct {
|
||||||
|
ASN int `json:"asn"`
|
||||||
|
Handle string `json:"handle"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
FirstSeen time.Time `json:"first_seen"`
|
||||||
|
LastSeen time.Time `json:"last_seen"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetASPeers returns all ASes that peer with the given AS
|
||||||
|
func (d *Database) GetASPeers(asn int) ([]ASPeer, error) {
|
||||||
|
return d.GetASPeersContext(context.Background(), asn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetASPeersContext returns all ASes that peer with the given AS with context support
|
||||||
|
func (d *Database) GetASPeersContext(ctx context.Context, asn int) ([]ASPeer, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN p.as_a = ? THEN p.as_b
|
||||||
|
ELSE p.as_a
|
||||||
|
END as peer_asn,
|
||||||
|
COALESCE(a.handle, '') as handle,
|
||||||
|
COALESCE(a.description, '') as description,
|
||||||
|
p.first_seen,
|
||||||
|
p.last_seen
|
||||||
|
FROM peerings p
|
||||||
|
LEFT JOIN asns a ON a.asn = CASE
|
||||||
|
WHEN p.as_a = ? THEN p.as_b
|
||||||
|
ELSE p.as_a
|
||||||
|
END
|
||||||
|
WHERE p.as_a = ? OR p.as_b = ?
|
||||||
|
ORDER BY peer_asn
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := d.db.QueryContext(ctx, query, asn, asn, asn, asn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query AS peers: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var peers []ASPeer
|
||||||
|
for rows.Next() {
|
||||||
|
var peer ASPeer
|
||||||
|
err := rows.Scan(&peer.ASN, &peer.Handle, &peer.Description, &peer.FirstSeen, &peer.LastSeen)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Error("Failed to scan peer row", "error", err, "asn", asn)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error iterating AS peers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetPrefixDetails returns detailed information about a prefix
|
// GetPrefixDetails returns detailed information about a prefix
|
||||||
func (d *Database) GetPrefixDetails(prefix string) ([]LiveRoute, error) {
|
func (d *Database) GetPrefixDetails(prefix string) ([]LiveRoute, error) {
|
||||||
return d.GetPrefixDetailsContext(context.Background(), prefix)
|
return d.GetPrefixDetailsContext(context.Background(), prefix)
|
||||||
|
@ -60,6 +60,8 @@ type Store interface {
|
|||||||
// AS and prefix detail operations
|
// AS and prefix detail operations
|
||||||
GetASDetails(asn int) (*ASN, []LiveRoute, error)
|
GetASDetails(asn int) (*ASN, []LiveRoute, error)
|
||||||
GetASDetailsContext(ctx context.Context, asn int) (*ASN, []LiveRoute, error)
|
GetASDetailsContext(ctx context.Context, asn int) (*ASN, []LiveRoute, error)
|
||||||
|
GetASPeers(asn int) ([]ASPeer, error)
|
||||||
|
GetASPeersContext(ctx context.Context, asn int) ([]ASPeer, error)
|
||||||
GetPrefixDetails(prefix string) ([]LiveRoute, error)
|
GetPrefixDetails(prefix string) ([]LiveRoute, error)
|
||||||
GetPrefixDetailsContext(ctx context.Context, prefix string) ([]LiveRoute, error)
|
GetPrefixDetailsContext(ctx context.Context, prefix string) ([]LiveRoute, error)
|
||||||
GetRandomPrefixesByLength(maskLength, ipVersion, limit int) ([]LiveRoute, error)
|
GetRandomPrefixesByLength(maskLength, ipVersion, limit int) ([]LiveRoute, error)
|
||||||
|
@ -291,6 +291,17 @@ func (m *mockStore) GetRandomPrefixesByLengthContext(ctx context.Context, maskLe
|
|||||||
return m.GetRandomPrefixesByLength(maskLength, ipVersion, limit)
|
return m.GetRandomPrefixesByLength(maskLength, ipVersion, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetASPeers mock implementation
|
||||||
|
func (m *mockStore) GetASPeers(asn int) ([]database.ASPeer, error) {
|
||||||
|
// Return empty peers for now
|
||||||
|
return []database.ASPeer{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetASPeersContext mock implementation with context support
|
||||||
|
func (m *mockStore) GetASPeersContext(ctx context.Context, asn int) ([]database.ASPeer, error) {
|
||||||
|
return m.GetASPeers(asn)
|
||||||
|
}
|
||||||
|
|
||||||
// UpsertLiveRouteBatch mock implementation
|
// UpsertLiveRouteBatch mock implementation
|
||||||
func (m *mockStore) UpsertLiveRouteBatch(routes []*database.LiveRoute) error {
|
func (m *mockStore) UpsertLiveRouteBatch(routes []*database.LiveRoute) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
@ -493,6 +493,14 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get peers
|
||||||
|
peers, err := s.db.GetASPeersContext(r.Context(), asn)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get AS peers", "error", err)
|
||||||
|
// Continue without peers rather than failing the whole request
|
||||||
|
peers = []database.ASPeer{}
|
||||||
|
}
|
||||||
|
|
||||||
// Group prefixes by IP version
|
// Group prefixes by IP version
|
||||||
const ipVersionV4 = 4
|
const ipVersionV4 = 4
|
||||||
var ipv4Prefixes, ipv6Prefixes []database.LiveRoute
|
var ipv4Prefixes, ipv6Prefixes []database.LiveRoute
|
||||||
@ -549,6 +557,8 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
|||||||
TotalCount int
|
TotalCount int
|
||||||
IPv4Count int
|
IPv4Count int
|
||||||
IPv6Count int
|
IPv6Count int
|
||||||
|
Peers []database.ASPeer
|
||||||
|
PeerCount int
|
||||||
}{
|
}{
|
||||||
ASN: asInfo,
|
ASN: asInfo,
|
||||||
IPv4Prefixes: ipv4Prefixes,
|
IPv4Prefixes: ipv4Prefixes,
|
||||||
@ -556,6 +566,8 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
|||||||
TotalCount: len(prefixes),
|
TotalCount: len(prefixes),
|
||||||
IPv4Count: len(ipv4Prefixes),
|
IPv4Count: len(ipv4Prefixes),
|
||||||
IPv6Count: len(ipv6Prefixes),
|
IPv6Count: len(ipv6Prefixes),
|
||||||
|
Peers: peers,
|
||||||
|
PeerCount: len(peers),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if context is still valid before writing response
|
// Check if context is still valid before writing response
|
||||||
|
@ -154,6 +154,10 @@
|
|||||||
<div class="info-label">IPv6 Prefixes</div>
|
<div class="info-label">IPv6 Prefixes</div>
|
||||||
<div class="info-value">{{.IPv6Count}}</div>
|
<div class="info-value">{{.IPv6Count}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-label">Peer ASNs</div>
|
||||||
|
<div class="info-value">{{.PeerCount}}</div>
|
||||||
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<div class="info-label">First Seen</div>
|
<div class="info-label">First Seen</div>
|
||||||
<div class="info-value">{{.ASN.FirstSeen.Format "2006-01-02"}}</div>
|
<div class="info-value">{{.ASN.FirstSeen.Format "2006-01-02"}}</div>
|
||||||
@ -223,6 +227,44 @@
|
|||||||
<p>No prefixes announced by this AS</p>
|
<p>No prefixes announced by this AS</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Peers}}
|
||||||
|
<div class="prefix-section">
|
||||||
|
<div class="prefix-header">
|
||||||
|
<h2>Peer ASNs</h2>
|
||||||
|
<span class="prefix-count">{{.PeerCount}}</span>
|
||||||
|
</div>
|
||||||
|
<table class="prefix-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ASN</th>
|
||||||
|
<th>Handle</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>First Seen</th>
|
||||||
|
<th>Last Seen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Peers}}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/as/{{.ASN}}" class="prefix-link">AS{{.ASN}}</a></td>
|
||||||
|
<td>{{if .Handle}}{{.Handle}}{{else}}-{{end}}</td>
|
||||||
|
<td>{{if .Description}}{{.Description}}{{else}}-{{end}}</td>
|
||||||
|
<td>{{.FirstSeen.Format "2006-01-02"}}</td>
|
||||||
|
<td>{{.LastSeen.Format "2006-01-02"}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="prefix-section">
|
||||||
|
<h2>Peer ASNs</h2>
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No peering relationships found for this AS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user