feat: enhance BGP template and peer configuration handling. Introduce policies_json parameter for local IP and ASN overrides in BGP speakers. Implement BGP template rendering and update peer configuration to utilize templates, improving flexibility in BGP setup. Add tests for new rendering logic and ensure proper integration with existing configurations.
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 27s
CI / go (push) Successful in 23s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m26s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m33s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m21s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m24s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m25s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s

This commit is contained in:
Denozordec
2026-04-06 10:18:51 +07:00
parent c958d5af0d
commit be8e7b5742
11 changed files with 279 additions and 51 deletions
+10 -1
View File
@@ -577,6 +577,13 @@ components:
bgp_speaker_id: bgp_speaker_id:
type: ["string", "null"] type: ["string", "null"]
description: "`null` - политика для всех спикеров." description: "`null` - политика для всех спикеров."
policies_json:
type: string
description: >
JSON-объект (строка). Поля `local_ipv4`, `local_ipv6`, `local_asn` переопределяют
глобальные `bird_local_ipv4` / `bird_local_ipv6` / `bird_local_asn` тенанта.
Если эффективный локальный адрес или ASN пира отличается от значений в шаблоне BIRD,
в сгенерированный блок `protocol bgp … from bgp_template` добавляется строка `local … as …`.
additionalProperties: true additionalProperties: true
BgpSpeaker: BgpSpeaker:
@@ -1744,7 +1751,9 @@ paths:
patch: patch:
tags: [Peers] tags: [Peers]
summary: Обновить пира summary: Обновить пира
description: Политики, neighbor, ASN, привязка к `bgp_speaker_id` или `null` для всех спикеров. description: >
Политики (`policies_json`: `local_ipv4`, `local_ipv6`, `local_asn`), neighbor, ASN,
привязка к `bgp_speaker_id` или `null` для всех спикеров.
operationId: patchPeer operationId: patchPeer
parameters: parameters:
- $ref: "#/components/parameters/IdempotencyKey" - $ref: "#/components/parameters/IdempotencyKey"
+110
View File
@@ -5,6 +5,116 @@ import (
"strings" "strings"
) )
// BGP template names referenced by generated peers (BIRD 2).
const (
BGPTemplateNameV4 = "bgp_template"
BGPTemplateNameV6 = "bgp_template_v6"
)
// BGPTemplatesOptions holds tenant defaults for template bgp bgp_template (+ v6 mirror).
type BGPTemplatesOptions struct {
LocalIPv4 string
LocalIPv6 string
LocalASN uint32
ExportFilterV4 string
ExportFilterV6 string
}
// RenderBGPTemplates renders two template bgp blocks (IPv4 and IPv6 AFI).
func RenderBGPTemplates(opts BGPTemplatesOptions) (string, error) {
if strings.TrimSpace(opts.LocalIPv4) == "" || strings.TrimSpace(opts.LocalIPv6) == "" {
return "", fmt.Errorf("birdfmt: template local IPv4 and IPv6 are required")
}
if opts.LocalASN == 0 {
return "", fmt.Errorf("birdfmt: template local ASN must be non-zero")
}
exp4 := "all"
if strings.TrimSpace(opts.ExportFilterV4) != "" {
exp4 = "filter " + strings.TrimSpace(opts.ExportFilterV4)
}
exp6 := "all"
if strings.TrimSpace(opts.ExportFilterV6) != "" {
exp6 = "filter " + strings.TrimSpace(opts.ExportFilterV6)
}
var b strings.Builder
fmt.Fprintf(&b, "template bgp %s {\n", BGPTemplateNameV4)
b.WriteString(" local ")
b.WriteString(strings.TrimSpace(opts.LocalIPv4))
fmt.Fprintf(&b, " as %d;\n", opts.LocalASN)
b.WriteString(" ipv4 {\n")
b.WriteString(" import all;\n")
b.WriteString(" export ")
b.WriteString(exp4)
b.WriteString(";\n")
b.WriteString(" };\n")
b.WriteString("}\n\n")
fmt.Fprintf(&b, "template bgp %s {\n", BGPTemplateNameV6)
b.WriteString(" local ")
b.WriteString(strings.TrimSpace(opts.LocalIPv6))
fmt.Fprintf(&b, " as %d;\n", opts.LocalASN)
b.WriteString(" ipv6 {\n")
b.WriteString(" import all;\n")
b.WriteString(" export ")
b.WriteString(exp6)
b.WriteString(";\n")
b.WriteString(" };\n")
b.WriteString("}\n")
return b.String(), nil
}
// BGPPeerFromTemplateOptions describes protocol bgp NAME from TEMPLATE { … }.
type BGPPeerFromTemplateOptions struct {
ProtocolName string
TemplateName string
NeighborIP string
NeighborASN uint32
SourceAddress string
// If set, emits "local … as …" before neighbor (overrides template local/ASN for this peer).
OverrideLocalIP string
OverrideLocalASN uint32
}
// RenderProtocolBGPFromTemplate renders protocol bgp … from TEMPLATE { neighbor; multihop; source address; passive; }.
func RenderProtocolBGPFromTemplate(opts BGPPeerFromTemplateOptions) (string, error) {
if strings.TrimSpace(opts.ProtocolName) == "" {
return "", fmt.Errorf("birdfmt: protocol name is required")
}
if strings.TrimSpace(opts.TemplateName) == "" {
return "", fmt.Errorf("birdfmt: template name is required")
}
if strings.TrimSpace(opts.NeighborIP) == "" || strings.TrimSpace(opts.SourceAddress) == "" {
return "", fmt.Errorf("birdfmt: neighbor and source address are required")
}
if opts.NeighborASN == 0 {
return "", fmt.Errorf("birdfmt: neighbor ASN must be non-zero")
}
if opts.OverrideLocalIP != "" && opts.OverrideLocalASN == 0 {
return "", fmt.Errorf("birdfmt: override local ASN is required when override local IP is set")
}
var b strings.Builder
b.WriteString("protocol bgp ")
b.WriteString(strings.TrimSpace(opts.ProtocolName))
b.WriteString(" from ")
b.WriteString(strings.TrimSpace(opts.TemplateName))
b.WriteString(" {\n")
if ip := strings.TrimSpace(opts.OverrideLocalIP); ip != "" {
b.WriteString(" local ")
b.WriteString(ip)
fmt.Fprintf(&b, " as %d;\n", opts.OverrideLocalASN)
}
b.WriteString(" neighbor ")
b.WriteString(strings.TrimSpace(opts.NeighborIP))
fmt.Fprintf(&b, " as %d;\n", opts.NeighborASN)
b.WriteString(" multihop;\n")
b.WriteString(" source address ")
b.WriteString(strings.TrimSpace(opts.SourceAddress))
b.WriteString(";\n")
b.WriteString(" passive;\n")
b.WriteString("}\n")
return b.String(), nil
}
// BGPPeerIPv4Options describes a single BGP session (BIRD 2, IPv4 AF). // BGPPeerIPv4Options describes a single BGP session (BIRD 2, IPv4 AF).
type BGPPeerIPv4Options struct { type BGPPeerIPv4Options struct {
ProtocolName string // e.g. evobgp_peer_uplink ProtocolName string // e.g. evobgp_peer_uplink
+47
View File
@@ -27,3 +27,50 @@ func TestRenderProtocolBGPIPv4_ExportAll(t *testing.T) {
t.Fatal(got) t.Fatal(got)
} }
} }
func TestRenderBGPTemplates_Validation(t *testing.T) {
_, err := RenderBGPTemplates(BGPTemplatesOptions{})
if err == nil {
t.Fatal("expected error")
}
}
func TestRenderProtocolBGPFromTemplate_MultihopPassive(t *testing.T) {
got, err := RenderProtocolBGPFromTemplate(BGPPeerFromTemplateOptions{
ProtocolName: "evobgp_p_x",
TemplateName: BGPTemplateNameV4,
NeighborIP: "94.142.140.141",
NeighborASN: 65002,
SourceAddress: "77.232.38.173",
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, "from bgp_template") {
t.Fatal(got)
}
if !strings.Contains(got, "multihop;") || !strings.Contains(got, "passive;") {
t.Fatal(got)
}
if !strings.Contains(got, "source address 77.232.38.173;") {
t.Fatal(got)
}
}
func TestRenderProtocolBGPFromTemplate_OverrideLocal(t *testing.T) {
got, err := RenderProtocolBGPFromTemplate(BGPPeerFromTemplateOptions{
ProtocolName: "p",
TemplateName: BGPTemplateNameV4,
NeighborIP: "192.0.2.2",
NeighborASN: 2,
SourceAddress: "10.0.0.1",
OverrideLocalIP: "10.0.0.1",
OverrideLocalASN: 65099,
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, "local 10.0.0.1 as 65099;") {
t.Fatal(got)
}
}
+4 -3
View File
@@ -1,13 +1,14 @@
// Package birdfmt builds BIRD 2 configuration text: main bird.conf skeleton, include // Package birdfmt builds BIRD 2 configuration text: main bird.conf skeleton, include
// fragments under bird.d/, export filters, static route protocols, and minimal BGP peer // fragments under bird.d/, export filters, static route protocols, BGP templates, and
// blocks. Runtime helpers parse-check configs (bird -p) and reload (birdc configure). // BGP peer protocols (from template or standalone). Runtime helpers parse-check configs
// (bird -p) and reload (birdc configure).
// //
// # Layout // # Layout
// //
// Operator keeps a stable bird.conf (or generated skeleton) next to EvoBGP fragments: // Operator keeps a stable bird.conf (or generated skeleton) next to EvoBGP fragments:
// //
// bird.conf — router id, protocol device, protocol direct, include lines // bird.conf — router id, protocol device, protocol direct, include lines
// bird.d/evobgp_*.conf — generated prefixes, filters, peers (names from constants) // bird.d/evobgp_*.conf — generated prefixes, filters, BGP templates, peers (names from constants)
// //
// Include order should list filter definitions before protocols that reference them // Include order should list filter definitions before protocols that reference them
// (see StandardIncludeFragments). // (see StandardIncludeFragments).
+8 -6
View File
@@ -9,11 +9,12 @@ import (
const ( const (
DirBirdD = "bird.d" DirBirdD = "bird.d"
FragmentPrefixesV4 = "evobgp_prefixes_v4.conf" FragmentPrefixesV4 = "evobgp_prefixes_v4.conf"
FragmentPrefixesV6 = "evobgp_prefixes_v6.conf" FragmentPrefixesV6 = "evobgp_prefixes_v6.conf"
FragmentFiltersV4 = "evobgp_filters_v4.conf" FragmentFiltersV4 = "evobgp_filters_v4.conf"
FragmentFiltersV6 = "evobgp_filters_v6.conf" FragmentFiltersV6 = "evobgp_filters_v6.conf"
FragmentPeers = "evobgp_peers.conf" FragmentBGPTemplate = "evobgp_bgp_template.conf"
FragmentPeers = "evobgp_peers.conf"
) )
// FragmentIncludePath returns a POSIX include path such as bird.d/evobgp_prefixes_v4.conf. // FragmentIncludePath returns a POSIX include path such as bird.d/evobgp_prefixes_v4.conf.
@@ -24,11 +25,12 @@ func FragmentIncludePath(fragmentBaseName string) string {
return DirBirdD + "/" + fragmentBaseName return DirBirdD + "/" + fragmentBaseName
} }
// StandardIncludeFragments is the recommended order: filters before peers that reference them. // StandardIncludeFragments is the recommended order: filters before BGP templates and peers.
func StandardIncludeFragments() []string { func StandardIncludeFragments() []string {
return []string{ return []string{
FragmentIncludePath(FragmentFiltersV4), FragmentIncludePath(FragmentFiltersV4),
FragmentIncludePath(FragmentFiltersV6), FragmentIncludePath(FragmentFiltersV6),
FragmentIncludePath(FragmentBGPTemplate),
FragmentIncludePath(FragmentPrefixesV4), FragmentIncludePath(FragmentPrefixesV4),
FragmentIncludePath(FragmentPrefixesV6), FragmentIncludePath(FragmentPrefixesV6),
FragmentIncludePath(FragmentPeers), FragmentIncludePath(FragmentPeers),
+18 -7
View File
@@ -29,13 +29,24 @@ func TestStandardLayout_GeneratorMatchesFixtures(t *testing.T) {
staticV6 := RenderStaticIPv6Protocol("evobgp_prefixes_v6", nil) staticV6 := RenderStaticIPv6Protocol("evobgp_prefixes_v6", nil)
assertFileEquals(t, "testdata/scenarios/standard_layout/bird.d/evobgp_prefixes_v6.conf", staticV6) assertFileEquals(t, "testdata/scenarios/standard_layout/bird.d/evobgp_prefixes_v6.conf", staticV6)
peer, err := RenderProtocolBGPIPv4(BGPPeerIPv4Options{ tpl, err := RenderBGPTemplates(BGPTemplatesOptions{
ProtocolName: "evobgp_peer_ci", LocalIPv4: "192.0.2.1",
LocalIP: "192.0.2.1", LocalIPv6: "2001:db8::1",
LocalASN: 65001, LocalASN: 65001,
NeighborIP: "192.0.2.2", ExportFilterV4: "evobgp_export_v4",
NeighborASN: 65002, ExportFilterV6: "evobgp_export_v6",
ExportFilter: "evobgp_export_v4", })
if err != nil {
t.Fatal(err)
}
assertFileEquals(t, "testdata/scenarios/standard_layout/bird.d/evobgp_bgp_template.conf", tpl)
peer, err := RenderProtocolBGPFromTemplate(BGPPeerFromTemplateOptions{
ProtocolName: "evobgp_peer_ci",
TemplateName: BGPTemplateNameV4,
NeighborIP: "192.0.2.2",
NeighborASN: 65002,
SourceAddress: "192.0.2.1",
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -2,6 +2,7 @@ router id 192.0.2.1;
include "bird.d/evobgp_filters_v4.conf"; include "bird.d/evobgp_filters_v4.conf";
include "bird.d/evobgp_filters_v6.conf"; include "bird.d/evobgp_filters_v6.conf";
include "bird.d/evobgp_bgp_template.conf";
include "bird.d/evobgp_prefixes_v4.conf"; include "bird.d/evobgp_prefixes_v4.conf";
include "bird.d/evobgp_prefixes_v6.conf"; include "bird.d/evobgp_prefixes_v6.conf";
include "bird.d/evobgp_peers.conf"; include "bird.d/evobgp_peers.conf";
@@ -5,6 +5,7 @@ router id 192.0.2.1;
include "bird.d/evobgp_filters_v4.conf"; include "bird.d/evobgp_filters_v4.conf";
include "bird.d/evobgp_filters_v6.conf"; include "bird.d/evobgp_filters_v6.conf";
include "bird.d/evobgp_bgp_template.conf";
include "bird.d/evobgp_prefixes_v4.conf"; include "bird.d/evobgp_prefixes_v4.conf";
include "bird.d/evobgp_prefixes_v6.conf"; include "bird.d/evobgp_prefixes_v6.conf";
include "bird.d/evobgp_peers.conf"; include "bird.d/evobgp_peers.conf";
@@ -0,0 +1,15 @@
template bgp bgp_template {
local 192.0.2.1 as 65001;
ipv4 {
import all;
export filter evobgp_export_v4;
};
}
template bgp bgp_template_v6 {
local 2001:db8::1 as 65001;
ipv6 {
import all;
export filter evobgp_export_v6;
};
}
@@ -1,8 +1,6 @@
protocol bgp evobgp_peer_ci { protocol bgp evobgp_peer_ci from bgp_template {
local 192.0.2.1 as 65001;
neighbor 192.0.2.2 as 65002; neighbor 192.0.2.2 as 65002;
ipv4 { multihop;
import all; source address 192.0.2.1;
export filter evobgp_export_v4; passive;
};
} }
+61 -28
View File
@@ -236,6 +236,16 @@ func buildPreviewFragments(st store.Backend, tenantID, moduleID, revisionID stri
staticV6 := birdfmt.RenderStaticIPv6Routes("evobgp_prefixes_v6", sr6) staticV6 := birdfmt.RenderStaticIPv6Routes("evobgp_prefixes_v6", sr6)
locals := birdLocalsFromStore(st, tenantID) locals := birdLocalsFromStore(st, tenantID)
tplBody, err := birdfmt.RenderBGPTemplates(birdfmt.BGPTemplatesOptions{
LocalIPv4: locals.localV4,
LocalIPv6: locals.localV6,
LocalASN: locals.localASN,
ExportFilterV4: birdFilterNameV4,
ExportFilterV6: birdFilterNameV6,
})
if err != nil {
return nil, err
}
peersBody, err := renderPeersBirdFragment(st, tenantID, locals) peersBody, err := renderPeersBirdFragment(st, tenantID, locals)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -252,6 +262,7 @@ func buildPreviewFragments(st store.Backend, tenantID, moduleID, revisionID stri
p4 := birdfmt.FragmentIncludePath(birdfmt.FragmentFiltersV4) p4 := birdfmt.FragmentIncludePath(birdfmt.FragmentFiltersV4)
p6 := birdfmt.FragmentIncludePath(birdfmt.FragmentFiltersV6) p6 := birdfmt.FragmentIncludePath(birdfmt.FragmentFiltersV6)
pTpl := birdfmt.FragmentIncludePath(birdfmt.FragmentBGPTemplate)
px4 := birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV4) px4 := birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV4)
px6 := birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV6) px6 := birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV6)
pPeers := birdfmt.FragmentIncludePath(birdfmt.FragmentPeers) pPeers := birdfmt.FragmentIncludePath(birdfmt.FragmentPeers)
@@ -260,6 +271,7 @@ func buildPreviewFragments(st store.Backend, tenantID, moduleID, revisionID stri
"bird.conf": main, "bird.conf": main,
p4: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), f4), p4: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), f4),
p6: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), f6), p6: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), f6),
pTpl: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), tplBody),
px4: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), staticV4), px4: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), staticV4),
px6: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), staticV6), px6: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), staticV6),
pPeers: peersBody, pPeers: peersBody,
@@ -346,6 +358,30 @@ type peerPolicyJSON struct {
LocalASN float64 `json:"local_asn"` LocalASN float64 `json:"local_asn"`
} }
func effectivePeerLocals(loc birdLocals, pol peerPolicyJSON) (v4, v6 string, asn uint32) {
v4 = strings.TrimSpace(loc.localV4)
v6 = strings.TrimSpace(loc.localV6)
if s := strings.TrimSpace(pol.LocalIPv4); s != "" {
v4 = s
}
if s := strings.TrimSpace(pol.LocalIPv6); s != "" {
v6 = s
}
asn = loc.localASN
if pol.LocalASN >= 1 && pol.LocalASN <= 4294967295 {
asn = uint32(pol.LocalASN)
}
return v4, v6, asn
}
// peerNeedsLocalOverride is true when the peer's effective local IP or ASN differs from tenant defaults in the BGP template.
func peerNeedsLocalOverride(loc birdLocals, effLocal string, effASN uint32, ipv4 bool) bool {
if ipv4 {
return strings.TrimSpace(effLocal) != strings.TrimSpace(loc.localV4) || effASN != loc.localASN
}
return strings.TrimSpace(effLocal) != strings.TrimSpace(loc.localV6) || effASN != loc.localASN
}
func renderPeersBirdFragment(st store.Backend, tenantID string, loc birdLocals) (string, error) { func renderPeersBirdFragment(st store.Backend, tenantID string, loc birdLocals) (string, error) {
peers := st.ListPeers(tenantID) peers := st.ListPeers(tenantID)
var parts []string var parts []string
@@ -362,29 +398,22 @@ func renderPeersBirdFragment(st store.Backend, tenantID string, loc birdLocals)
continue continue
} }
pol := parsePeerPolicies(p.PoliciesJSON) pol := parsePeerPolicies(p.PoliciesJSON)
lv4 := loc.localV4 lv4, lv6, asn := effectivePeerLocals(loc, pol)
if strings.TrimSpace(pol.LocalIPv4) != "" {
lv4 = strings.TrimSpace(pol.LocalIPv4)
}
lv6 := loc.localV6
if strings.TrimSpace(pol.LocalIPv6) != "" {
lv6 = strings.TrimSpace(pol.LocalIPv6)
}
asn := loc.localASN
if pol.LocalASN >= 1 && pol.LocalASN <= 4294967295 {
asn = uint32(pol.LocalASN)
}
proto := peerProtocolName(p.ID) proto := peerProtocolName(p.ID)
ra := uint32(p.RemoteASN) ra := uint32(p.RemoteASN)
if addr.Is4() { if addr.Is4() {
s, err := birdfmt.RenderProtocolBGPIPv4(birdfmt.BGPPeerIPv4Options{ opts := birdfmt.BGPPeerFromTemplateOptions{
ProtocolName: proto, ProtocolName: proto,
LocalIP: lv4, TemplateName: birdfmt.BGPTemplateNameV4,
LocalASN: asn, NeighborIP: addr.String(),
NeighborIP: addr.String(), NeighborASN: ra,
NeighborASN: ra, SourceAddress: lv4,
ExportFilter: birdFilterNameV4, }
}) if peerNeedsLocalOverride(loc, lv4, asn, true) {
opts.OverrideLocalIP = lv4
opts.OverrideLocalASN = asn
}
s, err := birdfmt.RenderProtocolBGPFromTemplate(opts)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -392,14 +421,18 @@ func renderPeersBirdFragment(st store.Backend, tenantID string, loc birdLocals)
continue continue
} }
if addr.Is6() { if addr.Is6() {
s, err := birdfmt.RenderProtocolBGPIPv6(birdfmt.BGPPeerIPv6Options{ opts := birdfmt.BGPPeerFromTemplateOptions{
ProtocolName: proto, ProtocolName: proto,
LocalIP: lv6, TemplateName: birdfmt.BGPTemplateNameV6,
LocalASN: asn, NeighborIP: addr.String(),
NeighborIP: addr.String(), NeighborASN: ra,
NeighborASN: ra, SourceAddress: lv6,
ExportFilter: birdFilterNameV6, }
}) if peerNeedsLocalOverride(loc, lv6, asn, false) {
opts.OverrideLocalIP = lv6
opts.OverrideLocalASN = asn
}
s, err := birdfmt.RenderProtocolBGPFromTemplate(opts)
if err != nil { if err != nil {
return "", err return "", err
} }