quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 46s
quality / web (push) Successful in 1m16s
quality / go (push) Successful in 2m42s
quality / bird2 (push) Successful in 16s
CD / quality (push) Successful in 5m19s
CD / publish (push) Successful in 7m19s
- Deleted unused components: `DashboardActivityTimeline`, `DashboardFramePanel`, `DashboardModulesGrid`, `DashboardRecentJobsGrid`, and `DashboardRecentRevisionsGrid` to streamline the dashboard. - Updated `DashboardKpiGrid` to improve KPI display logic, including progress indicators and enhanced badge functionality. - Refactored `DashboardNetworkHealth` to provide better status representation based on loading states and network conditions. - Introduced new properties for KPI cards to support progress tracking and improved visual feedback. This cleanup aims to enhance performance and maintainability of the dashboard while providing a better user experience.
138 lines
4.0 KiB
Go
138 lines
4.0 KiB
Go
// Package asnresolve fetches IP prefixes announced by an ASN (control-plane ingest).
|
|
package asnresolve
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"evobgp/internal/httpclient"
|
|
)
|
|
|
|
// DefaultRIPEStatURL is the RIPEstat announced-prefixes data call (no API key).
|
|
const DefaultRIPEStatURL = "https://stat.ripe.net/data/announced-prefixes/data.json"
|
|
|
|
// DefaultASOverviewURL is the RIPEstat as-overview data call (holder / org name, no API key).
|
|
const DefaultASOverviewURL = "https://stat.ripe.net/data/as-overview/data.json"
|
|
|
|
// AnnouncedPrefixes returns currently announced IPv4/IPv6 prefixes for the ASN (best-effort via RIPEstat).
|
|
func AnnouncedPrefixes(ctx context.Context, hc *http.Client, asn int64) ([]netip.Prefix, error) {
|
|
if hc == nil {
|
|
hc = httpclient.New(httpclient.DefaultTimeout)
|
|
}
|
|
base := strings.TrimSpace(os.Getenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL"))
|
|
if base == "" {
|
|
base = DefaultRIPEStatURL
|
|
}
|
|
u := fmt.Sprintf("%s?resource=AS%d", strings.TrimSuffix(base, "?"), asn)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", "evobgp-asnresolve/1.0")
|
|
|
|
resp, err := httpclient.DoWithBreaker(ctx, hc, req, 3)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ripestat fetch AS%d: %w", asn, err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("ripestat AS%d: HTTP %s: %s", asn, resp.Status, truncateForErr(body, 200))
|
|
}
|
|
|
|
var wrap struct {
|
|
Status string `json:"status"`
|
|
Data struct {
|
|
Prefixes []struct {
|
|
Prefix string `json:"prefix"`
|
|
} `json:"prefixes"`
|
|
} `json:"data"`
|
|
Messages [][]string `json:"messages"`
|
|
}
|
|
if err := json.Unmarshal(body, &wrap); err != nil {
|
|
return nil, fmt.Errorf("ripestat AS%d: json: %w", asn, err)
|
|
}
|
|
if wrap.Status != "" && wrap.Status != "ok" {
|
|
return nil, fmt.Errorf("ripestat AS%d: status %q", asn, wrap.Status)
|
|
}
|
|
|
|
var out []netip.Prefix
|
|
for _, row := range wrap.Data.Prefixes {
|
|
p := strings.TrimSpace(row.Prefix)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
pfx, err := netip.ParsePrefix(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, pfx.Masked())
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() })
|
|
return out, nil
|
|
}
|
|
|
|
// ASHolderName returns the holder / organization label for the ASN from RIPEstat as-overview (best-effort).
|
|
func ASHolderName(ctx context.Context, hc *http.Client, asn int64) (string, error) {
|
|
if hc == nil {
|
|
hc = httpclient.New(httpclient.DefaultTimeout)
|
|
}
|
|
base := strings.TrimSpace(os.Getenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL"))
|
|
if base == "" {
|
|
base = DefaultASOverviewURL
|
|
}
|
|
u := fmt.Sprintf("%s?resource=AS%d", strings.TrimSuffix(base, "?"), asn)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", "evobgp-asnresolve/1.0")
|
|
|
|
resp, err := httpclient.DoWithBreaker(ctx, hc, req, 3)
|
|
if err != nil {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: %w", asn, err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: HTTP %s: %s", asn, resp.Status, truncateForErr(body, 200))
|
|
}
|
|
|
|
var wrap struct {
|
|
Status string `json:"status"`
|
|
Data struct {
|
|
Holder string `json:"holder"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &wrap); err != nil {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: json: %w", asn, err)
|
|
}
|
|
if wrap.Status != "" && wrap.Status != "ok" {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: status %q", asn, wrap.Status)
|
|
}
|
|
return strings.TrimSpace(wrap.Data.Holder), nil
|
|
}
|
|
|
|
func truncateForErr(b []byte, n int) string {
|
|
s := string(b)
|
|
if len(s) > n {
|
|
return s[:n] + "…"
|
|
}
|
|
return s
|
|
}
|