feat: enhance RefreshModule to support legacy ASN resolution and improve prefix handling. Introduce sorting of ASNs and manage announced prefixes with polite pauses for better API interaction. Update buildPreviewFragments to streamline route rendering for BIRD configurations.
CI / changes (push) Successful in 10s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 35s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m27s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m16s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 19s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m17s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m48s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m49s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m28s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m35s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m15s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m27s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m23s

This commit is contained in:
Denozordec committed 2026-04-06 00:01:16 +07:00
1 parent 1969a00151
commit e1aaa18377
9 files changed
+625 -68

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
package birdfmt
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
)
var standardPairRE = regexp.MustCompile(`^\s*(\d{1,5}):(\d{1,5})\s*$`)
// RouteCommunityAttrs returns BIRD 2 lines (indented with 4 spaces) for use inside a static `route … { … }` block.
// Empty string means no community attributes.
func RouteCommunityAttrs(kind, name, valueJSON string) (string, error) {
name = strings.TrimSpace(name)
raw := strings.TrimSpace(valueJSON)
if raw == "" {
raw = "{}"
}
if s, ok := parseLargeFromJSONLoose(raw); ok {
return s, nil
}
_ = kind // reserved for future kinds (e.g. extended communities)
if m := standardPairRE.FindStringSubmatch(name); len(m) == 3 {
return standardAddLine(m[1], m[2])
}
var obj map[string]any
if json.Unmarshal([]byte(raw), &obj) == nil {
if v, ok := obj["standard"].(string); ok {
if m := standardPairRE.FindStringSubmatch(v); len(m) == 3 {
return standardAddLine(m[1], m[2])
}
}
if arr, ok := obj["standard"].([]any); ok && len(arr) == 2 {
a, aok := numberToUint32String(arr[0])
b, bok := numberToUint32String(arr[1])
if aok && bok {
return standardAddLine(a, b)
}
}
}
var strVal string
if json.Unmarshal([]byte(raw), &strVal) == nil {
if m := standardPairRE.FindStringSubmatch(strVal); len(m) == 3 {
return standardAddLine(m[1], m[2])
}
}
return "", nil
}
func standardAddLine(a, b string) (string, error) {
ai, err1 := strconv.ParseUint(strings.TrimSpace(a), 10, 16)
bi, err2 := strconv.ParseUint(strings.TrimSpace(b), 10, 16)
if err1 != nil || err2 != nil {
return "", fmt.Errorf("birdfmt: standard community parts must be 0..65535 (%s:%s)", a, b)
}
return fmt.Sprintf(" bgp_community.add((%d,%d));", ai, bi), nil
}
func parseLargeFromJSONLoose(raw string) (string, bool) {
var obj map[string]any
if json.Unmarshal([]byte(raw), &obj) != nil {
return "", false
}
arr, ok := obj["large"].([]any)
if !ok || len(arr) == 0 {
return "", false
}
var b strings.Builder
for _, row := range arr {
triple, ok := row.([]any)
if !ok || len(triple) != 3 {
continue
}
a, ok1 := numberToUint32String(triple[0])
bb, ok2 := numberToUint32String(triple[1])
c, ok3 := numberToUint32String(triple[2])
if !ok1 || !ok2 || !ok3 {
continue
}
ai, _ := strconv.ParseUint(a, 10, 32)
bi, _ := strconv.ParseUint(bb, 10, 32)
ci, _ := strconv.ParseUint(c, 10, 32)
fmt.Fprintf(&b, " bgp_large_community.add((%d,%d,%d));\n", ai, bi, ci)
}
s := strings.TrimRight(b.String(), "\r\n")
return s, s != ""
}
func numberToUint32String(v any) (string, bool) {
switch x := v.(type) {
case float64:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case json.Number:
n, err := x.Int64()
if err != nil || n < 0 || n > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(n), 10), true
case int:
if x < 0 || x > 4294967295 {
return "", false
}
return strconv.FormatUint(uint64(x), 10), true
case string:
n, err := strconv.ParseUint(strings.TrimSpace(x), 10, 32)
if err != nil {
return "", false
}
return strconv.FormatUint(n, 10), true
default:
return "", false
}
}
+23
View File
@@ -0,0 +1,23 @@
package birdfmt
import "testing"
func TestRouteCommunityAttrs_StandardName(t *testing.T) {
s, err := RouteCommunityAttrs("", "65000:120", "{}")
if err != nil {
t.Fatal(err)
}
if s != " bgp_community.add((65000,120));" {
t.Fatalf("got %q", s)
}
}
func TestRouteCommunityAttrs_LargeJSON(t *testing.T) {
s, err := RouteCommunityAttrs("large", "x", `{"large":[[65000,1,2]]}`)
if err != nil {
t.Fatal(err)
}
if s != " bgp_large_community.add((65000,1,2));" {
t.Fatalf("got %q", s)
}
}
+137
View File
@@ -0,0 +1,137 @@
package birdfmt
import (
"net/netip"
"sort"
"strings"
)
// StaticRoute is one static route line in BIRD (optionally with a per-route body).
type StaticRoute struct {
Prefix netip.Prefix
RouteBody string // optional lines inside `route P unreachable { ... }`
}
// RenderStaticIPv4Routes renders a BIRD 2 protocol static block for IPv4.
func RenderStaticIPv4Routes(protocolName string, routes []StaticRoute) string {
if protocolName == "" {
protocolName = "evobgp_prefixes_v4"
}
type key struct {
p string
b string
}
uniq := make(map[key]StaticRoute)
for _, r := range routes {
if !r.Prefix.Addr().Is4() {
continue
}
p := r.Prefix.Masked()
body := strings.TrimSpace(r.RouteBody)
uniq[key{p.String(), body}] = StaticRoute{Prefix: p, RouteBody: body}
}
keys := make([]key, 0, len(uniq))
for k := range uniq {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].p != keys[j].p {
return keys[i].p < keys[j].p
}
return keys[i].b < keys[j].b
})
var b strings.Builder
b.WriteString("protocol static ")
b.WriteString(protocolName)
b.WriteString(" {\n ipv4;\n")
for _, k := range keys {
r := uniq[k]
p := r.Prefix.String()
if r.RouteBody == "" {
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable;\n")
continue
}
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable {\n")
for _, line := range strings.Split(r.RouteBody, "\n") {
line = strings.TrimRight(line, "\r")
if strings.TrimSpace(line) == "" {
continue
}
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
b.WriteString(" ")
}
b.WriteString(line)
b.WriteByte('\n')
}
b.WriteString(" };\n")
}
b.WriteString("}\n")
return b.String()
}
// RenderStaticIPv6Routes renders a BIRD 2 protocol static block for IPv6.
func RenderStaticIPv6Routes(protocolName string, routes []StaticRoute) string {
if protocolName == "" {
protocolName = "evobgp_prefixes_v6"
}
type key struct {
p string
b string
}
uniq := make(map[key]StaticRoute)
for _, r := range routes {
if !r.Prefix.Addr().Is6() {
continue
}
p := r.Prefix.Masked()
body := strings.TrimSpace(r.RouteBody)
uniq[key{p.String(), body}] = StaticRoute{Prefix: p, RouteBody: body}
}
keys := make([]key, 0, len(uniq))
for k := range uniq {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].p != keys[j].p {
return keys[i].p < keys[j].p
}
return keys[i].b < keys[j].b
})
var b strings.Builder
b.WriteString("protocol static ")
b.WriteString(protocolName)
b.WriteString(" {\n ipv6;\n")
for _, k := range keys {
r := uniq[k]
p := r.Prefix.String()
if r.RouteBody == "" {
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable;\n")
continue
}
b.WriteString(" route ")
b.WriteString(p)
b.WriteString(" unreachable {\n")
for _, line := range strings.Split(r.RouteBody, "\n") {
line = strings.TrimRight(line, "\r")
if strings.TrimSpace(line) == "" {
continue
}
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
b.WriteString(" ")
}
b.WriteString(line)
b.WriteByte('\n')
}
b.WriteString(" };\n")
}
b.WriteString("}\n")
return b.String()
}
+20
View File
@@ -0,0 +1,20 @@
package birdfmt
import (
"net/netip"
"strings"
"testing"
)
func TestRenderStaticIPv4Routes_WithCommunity(t *testing.T) {
p := netip.MustParsePrefix("203.0.113.0/24")
s := RenderStaticIPv4Routes("t", []StaticRoute{
{Prefix: p, RouteBody: " bgp_community.add((65000,1));"},
})
if !strings.Contains(s, "route 203.0.113.0/24 unreachable {") {
t.Fatalf("missing block: %s", s)
}
if !strings.Contains(s, "bgp_community.add((65000,1))") {
t.Fatalf("missing community: %s", s)
}
}