refactor: improve handling of deployable fragments in ApplyRevision and buildPreviewFragments functions. Introduce isDeployableBirdFragment to filter out UI-only preview keys, and update related logic in the API to ensure proper job queuing and response handling for module refresh operations.
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m0s
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 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m31s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
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 1m17s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m0s
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 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m31s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
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 1m17s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
This commit is contained in:
@@ -43,6 +43,9 @@ func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revisio
|
|||||||
// Phase 1: write staging tree
|
// Phase 1: write staging tree
|
||||||
for rel, content := range rev.PreviewFragments {
|
for rel, content := range rev.PreviewFragments {
|
||||||
rel = strings.TrimPrefix(rel, "/")
|
rel = strings.TrimPrefix(rel, "/")
|
||||||
|
if !isDeployableBirdFragment(rel) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
dst := filepath.Join(staging, rel)
|
dst := filepath.Join(staging, rel)
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -70,6 +73,9 @@ func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revisio
|
|||||||
// Phase 2: atomic swap into active
|
// Phase 2: atomic swap into active
|
||||||
for rel, content := range rev.PreviewFragments {
|
for rel, content := range rev.PreviewFragments {
|
||||||
rel = strings.TrimPrefix(rel, "/")
|
rel = strings.TrimPrefix(rel, "/")
|
||||||
|
if !isDeployableBirdFragment(rel) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
dst := filepath.Join(active, rel)
|
dst := filepath.Join(active, rel)
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
_ = restoreLKG(lkg, active)
|
_ = restoreLKG(lkg, active)
|
||||||
@@ -93,6 +99,11 @@ func ApplyRevision(ctx context.Context, ctl *birdfmt.BirdCtl, rev *store.Revisio
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isDeployableBirdFragment skips UI-only preview keys (e.g. flattened full config) not referenced from bird.conf.
|
||||||
|
func isDeployableBirdFragment(rel string) bool {
|
||||||
|
return rel != "" && !strings.HasPrefix(rel, "_")
|
||||||
|
}
|
||||||
|
|
||||||
func snapshotBirdTree(activeRoot, dstRoot string) error {
|
func snapshotBirdTree(activeRoot, dstRoot string) error {
|
||||||
if err := os.MkdirAll(dstRoot, 0o755); err != nil {
|
if err := os.MkdirAll(dstRoot, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ func BuildGzippedTar(revisionID, speakerID string, fragments map[string]string,
|
|||||||
if p == "." || strings.HasPrefix(p, "..") {
|
if p == "." || strings.HasPrefix(p, "..") {
|
||||||
return nil, fmt.Errorf("bundle: invalid path %q", p)
|
return nil, fmt.Errorf("bundle: invalid path %q", p)
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(p, "_") {
|
||||||
|
continue // UI-only preview aggregate, not shipped to nodes
|
||||||
|
}
|
||||||
norm[p] = content
|
norm[p] = content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -247,10 +247,6 @@ func (s *Server) handleModuleRefresh(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
|
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if mod.Type == "IP_RANGES" {
|
|
||||||
writeNoContent(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
idem := r.Header.Get("Idempotency-Key")
|
idem := r.Header.Get("Idempotency-Key")
|
||||||
var idemPtr *string
|
var idemPtr *string
|
||||||
if strings.TrimSpace(idem) != "" {
|
if strings.TrimSpace(idem) != "" {
|
||||||
@@ -320,6 +316,22 @@ func strPtrOrNull(s string) any {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enqueueModuleRefreshIfEnabled queues module_refresh when the module exists and is enabled (best-effort, no HTTP error).
|
||||||
|
func (s *Server) enqueueModuleRefreshIfEnabled(tenantID, moduleID, trigger string) {
|
||||||
|
if s.jobs == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mod, err := s.store.GetModule(tenantID, moduleID)
|
||||||
|
if err != nil || !mod.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mid := moduleID
|
||||||
|
_, _, _ = s.jobs.Enqueue(tenantID, jobs.KindModuleRefresh, nil, &mid, map[string]any{
|
||||||
|
"module_id": moduleID,
|
||||||
|
"trigger": trigger,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleGetRevision(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetRevision(w http.ResponseWriter, r *http.Request) {
|
||||||
a, ok := authFromContext(r.Context())
|
a, ok := authFromContext(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -400,11 +400,13 @@ func (s *Server) handlePostIPRange(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
x, err := s.store.CreateIPRangeEntry(a.TenantID, r.PathValue("module_id"), &body)
|
mid := r.PathValue("module_id")
|
||||||
|
x, err := s.store.CreateIPRangeEntry(a.TenantID, mid, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreErr(w, err)
|
writeStoreErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_create")
|
||||||
writeJSON(w, http.StatusCreated, ipRangeJSON(x))
|
writeJSON(w, http.StatusCreated, ipRangeJSON(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,11 +420,13 @@ func (s *Server) handlePatchIPRange(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
x, err := s.store.UpdateIPRangeEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id"), &body)
|
mid := r.PathValue("module_id")
|
||||||
|
x, err := s.store.UpdateIPRangeEntry(a.TenantID, mid, r.PathValue("entry_id"), &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeStoreErr(w, err)
|
writeStoreErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_patch")
|
||||||
writeJSON(w, http.StatusOK, ipRangeJSON(x))
|
writeJSON(w, http.StatusOK, ipRangeJSON(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,10 +435,12 @@ func (s *Server) handleDeleteIPRange(w http.ResponseWriter, r *http.Request) {
|
|||||||
if !ok || !s.requireAtLeast(w, a, "editor") {
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.store.DeleteIPRangeEntry(a.TenantID, r.PathValue("module_id"), r.PathValue("entry_id")); err != nil {
|
mid := r.PathValue("module_id")
|
||||||
|
if err := s.store.DeleteIPRangeEntry(a.TenantID, mid, r.PathValue("entry_id")); err != nil {
|
||||||
writeStoreErr(w, err)
|
writeStoreErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_delete")
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("refresh IP_RANGES no op", func(t *testing.T) {
|
t.Run("refresh IP_RANGES queues render job", func(t *testing.T) {
|
||||||
req, _ := http.NewRequest(http.MethodPost, base+"/v1/modules/"+modIP+"/refresh", nil)
|
req, _ := http.NewRequest(http.MethodPost, base+"/v1/modules/"+modIP+"/refresh", nil)
|
||||||
req.Header.Set("Authorization", "Bearer opkey")
|
req.Header.Set("Authorization", "Bearer opkey")
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
@@ -76,10 +76,17 @@ func TestAPIRefreshApplyJobsBundle(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||||
}
|
}
|
||||||
|
var body struct {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
waitJob(t, client, base, "opkey", body.JobID)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("refresh CDN queues job", func(t *testing.T) {
|
t.Run("refresh CDN queues job", func(t *testing.T) {
|
||||||
|
|||||||
+232
-17
@@ -3,6 +3,7 @@ package pipeline
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -17,6 +18,12 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
birdFilterNameV4 = "evobgp_export_v4"
|
||||||
|
birdFilterNameV6 = "evobgp_export_v6"
|
||||||
|
auxBirdFullExpanded = "_bird_full_expanded.conf"
|
||||||
|
)
|
||||||
|
|
||||||
// MaterializedASPrefixKey returns the revision snapshot key for an AS-only entry (not a CIDR).
|
// MaterializedASPrefixKey returns the revision snapshot key for an AS-only entry (not a CIDR).
|
||||||
func MaterializedASPrefixKey(asn int64) string {
|
func MaterializedASPrefixKey(asn int64) string {
|
||||||
return fmt.Sprintf("as:%d", asn)
|
return fmt.Sprintf("as:%d", asn)
|
||||||
@@ -127,7 +134,7 @@ func RefreshModule(ctx context.Context, st store.Backend, hc *http.Client, tenan
|
|||||||
revisionID = uuid.NewString()
|
revisionID = uuid.NewString()
|
||||||
parent := parentRevision(st, tenantID, moduleID)
|
parent := parentRevision(st, tenantID, moduleID)
|
||||||
hash := hashMaterialization(moduleID, rows)
|
hash := hashMaterialization(moduleID, rows)
|
||||||
preview, err := buildPreviewFragments(revisionID, rows)
|
preview, err := buildPreviewFragments(st, tenantID, moduleID, revisionID, rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -179,7 +186,7 @@ func hashMaterialization(moduleID string, rows []store.PrefixRow) string {
|
|||||||
return fmt.Sprintf("sha256:%x", h.Sum(nil))
|
return fmt.Sprintf("sha256:%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPreviewFragments(revisionID string, rows []store.PrefixRow) (map[string]string, error) {
|
func buildPreviewFragments(st store.Backend, tenantID, moduleID, revisionID string, rows []store.PrefixRow) (map[string]string, error) {
|
||||||
var v4, v6 []netip.Prefix
|
var v4, v6 []netip.Prefix
|
||||||
var pathASNs []int64
|
var pathASNs []int64
|
||||||
for _, pr := range rows {
|
for _, pr := range rows {
|
||||||
@@ -202,29 +209,237 @@ func buildPreviewFragments(revisionID string, rows []store.PrefixRow) (map[strin
|
|||||||
v6 = append(v6, pfx.Masked())
|
v6 = append(v6, pfx.Masked())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f4, err := birdfmt.RenderExportFilterIPv4("evobgp_export_v4", v4, pathASNs)
|
f4, err := birdfmt.RenderExportFilterIPv4(birdFilterNameV4, v4, pathASNs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f6, err := birdfmt.RenderExportFilterIPv6("evobgp_export_v6", v6, pathASNs)
|
f6, err := birdfmt.RenderExportFilterIPv6(birdFilterNameV6, v6, pathASNs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
birdD := birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), f4, f6)
|
staticV4 := birdfmt.RenderStaticIPv4Protocol("evobgp_prefixes_v4", v4)
|
||||||
main := `# EvoBGP generated (pipeline refresh)
|
staticV6 := birdfmt.RenderStaticIPv6Protocol("evobgp_prefixes_v6", v6)
|
||||||
router id 192.0.2.1;
|
|
||||||
include "bird.d/evobgp_generated.conf";
|
|
||||||
|
|
||||||
protocol device {
|
locals := birdLocalsFromStore(st, tenantID)
|
||||||
|
peersBody, err := renderPeersBirdFragment(st, tenantID, locals)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
main, err := birdfmt.RenderMainBirdConf(birdfmt.MainBirdConfOptions{
|
||||||
|
RouterID: locals.routerID,
|
||||||
|
Includes: birdfmt.StandardIncludeFragments(),
|
||||||
|
Preamble: fmt.Sprintf("EvoBGP module %s revision %s", moduleID, revisionID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p4 := birdfmt.FragmentIncludePath(birdfmt.FragmentFiltersV4)
|
||||||
|
p6 := birdfmt.FragmentIncludePath(birdfmt.FragmentFiltersV6)
|
||||||
|
px4 := birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV4)
|
||||||
|
px6 := birdfmt.FragmentIncludePath(birdfmt.FragmentPrefixesV6)
|
||||||
|
pPeers := birdfmt.FragmentIncludePath(birdfmt.FragmentPeers)
|
||||||
|
|
||||||
|
out := map[string]string{
|
||||||
|
"bird.conf": main,
|
||||||
|
p4: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), f4),
|
||||||
|
p6: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), f6),
|
||||||
|
px4: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), staticV4),
|
||||||
|
px6: birdfmt.JoinFragments(birdfmt.ManagedBanner(revisionID), staticV6),
|
||||||
|
pPeers: peersBody,
|
||||||
|
}
|
||||||
|
out[auxBirdFullExpanded] = buildExpandedBirdText(main, out)
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol direct {
|
type birdLocals struct {
|
||||||
ipv4;
|
routerID string
|
||||||
ipv6;
|
localV4 string
|
||||||
|
localV6 string
|
||||||
|
localASN uint32
|
||||||
}
|
}
|
||||||
`
|
|
||||||
return map[string]string{
|
func birdLocalsFromStore(st store.Backend, tenantID string) birdLocals {
|
||||||
"bird.conf": main,
|
def := birdLocals{
|
||||||
"bird.d/evobgp_generated.conf": birdD,
|
routerID: "192.0.2.1",
|
||||||
}, nil
|
localV4: "192.0.2.1",
|
||||||
|
localV6: "2001:db8::1",
|
||||||
|
localASN: 65001,
|
||||||
|
}
|
||||||
|
settings, err := st.ListGlobalSettings(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
loc := def
|
||||||
|
if s := stringFromSettingsMap(settings, "bird_router_id"); s != "" {
|
||||||
|
loc.routerID = s
|
||||||
|
}
|
||||||
|
if s := stringFromSettingsMap(settings, "bird_local_ipv4"); s != "" {
|
||||||
|
loc.localV4 = s
|
||||||
|
}
|
||||||
|
if s := stringFromSettingsMap(settings, "bird_local_ipv6"); s != "" {
|
||||||
|
loc.localV6 = s
|
||||||
|
}
|
||||||
|
if n := uint32FromSettingsMap(settings, "bird_local_asn"); n != 0 {
|
||||||
|
loc.localASN = n
|
||||||
|
}
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringFromSettingsMap(m map[string]any, key string) string {
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint32FromSettingsMap(m map[string]any, key string) uint32 {
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case float64:
|
||||||
|
if x >= 1 && x <= 4294967295 {
|
||||||
|
return uint32(x)
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
if x >= 1 && x <= 4294967295 {
|
||||||
|
return uint32(x)
|
||||||
|
}
|
||||||
|
case int64:
|
||||||
|
if x >= 1 && x <= 4294967295 {
|
||||||
|
return uint32(x)
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
if n, err := strconv.ParseUint(strings.TrimSpace(x), 10, 32); err == nil && n >= 1 {
|
||||||
|
return uint32(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerPolicyJSON struct {
|
||||||
|
LocalIPv4 string `json:"local_ipv4"`
|
||||||
|
LocalIPv6 string `json:"local_ipv6"`
|
||||||
|
LocalASN float64 `json:"local_asn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderPeersBirdFragment(st store.Backend, tenantID string, loc birdLocals) (string, error) {
|
||||||
|
peers := st.ListPeers(tenantID)
|
||||||
|
var parts []string
|
||||||
|
parts = append(parts, birdfmt.ManagedBanner("peers"))
|
||||||
|
for _, p := range peers {
|
||||||
|
if p == nil || !p.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
neighbor := strings.TrimSpace(p.Neighbor)
|
||||||
|
if neighbor == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, err := netip.ParseAddr(neighbor)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !store.ValidASN(p.RemoteASN) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pol := parsePeerPolicies(p.PoliciesJSON)
|
||||||
|
lv4 := loc.localV4
|
||||||
|
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)
|
||||||
|
ra := uint32(p.RemoteASN)
|
||||||
|
if addr.Is4() {
|
||||||
|
s, err := birdfmt.RenderProtocolBGPIPv4(birdfmt.BGPPeerIPv4Options{
|
||||||
|
ProtocolName: proto,
|
||||||
|
LocalIP: lv4,
|
||||||
|
LocalASN: asn,
|
||||||
|
NeighborIP: addr.String(),
|
||||||
|
NeighborASN: ra,
|
||||||
|
ExportFilter: birdFilterNameV4,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
parts = append(parts, s)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if addr.Is6() {
|
||||||
|
s, err := birdfmt.RenderProtocolBGPIPv6(birdfmt.BGPPeerIPv6Options{
|
||||||
|
ProtocolName: proto,
|
||||||
|
LocalIP: lv6,
|
||||||
|
LocalASN: asn,
|
||||||
|
NeighborIP: addr.String(),
|
||||||
|
NeighborASN: ra,
|
||||||
|
ExportFilter: birdFilterNameV6,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
parts = append(parts, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
parts = append(parts, "# (no enabled BGP peers with valid neighbor addresses)\n")
|
||||||
|
}
|
||||||
|
return birdfmt.JoinFragments(parts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePeerPolicies(raw string) peerPolicyJSON {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" || raw == "{}" {
|
||||||
|
return peerPolicyJSON{}
|
||||||
|
}
|
||||||
|
var pol peerPolicyJSON
|
||||||
|
_ = json.Unmarshal([]byte(raw), &pol)
|
||||||
|
return pol
|
||||||
|
}
|
||||||
|
|
||||||
|
func peerProtocolName(peerID string) string {
|
||||||
|
s := strings.ReplaceAll(strings.TrimSpace(peerID), "-", "")
|
||||||
|
if len(s) > 16 {
|
||||||
|
s = s[:16]
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
s = "x"
|
||||||
|
}
|
||||||
|
return "evobgp_p_" + s
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildExpandedBirdText concatenates bird.conf and the contents of each standard include (for UI / preview).
|
||||||
|
func buildExpandedBirdText(main string, frags map[string]string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(strings.TrimSpace(main))
|
||||||
|
b.WriteString("\n")
|
||||||
|
for _, inc := range birdfmt.StandardIncludeFragments() {
|
||||||
|
b.WriteString("\n# ---------- include \"")
|
||||||
|
b.WriteString(inc)
|
||||||
|
b.WriteString("\" ----------\n")
|
||||||
|
body := strings.TrimSpace(frags[inc])
|
||||||
|
if body == "" {
|
||||||
|
b.WriteString("# (empty)\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(body)
|
||||||
|
if !strings.HasSuffix(body, "\n") {
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,8 +211,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem('evobgp_api_token') ?? ''}` }
|
headers: { Authorization: `Bearer ${localStorage.getItem('evobgp_api_token') ?? ''}` }
|
||||||
});
|
});
|
||||||
if (res.status === 204) toast.message('Refresh не требуется (IP_RANGES)');
|
if (res.status === 202) toast.success('Задача поставлена в очередь (render ревизии)');
|
||||||
else if (res.status === 202) toast.success('Задача поставлена в очередь');
|
|
||||||
else toast.error(`HTTP ${res.status}`);
|
else toast.error(`HTTP ${res.status}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : String(e));
|
toast.error(e instanceof Error ? e.message : String(e));
|
||||||
|
|||||||
@@ -157,7 +157,12 @@
|
|||||||
async function doApply() {
|
async function doApply() {
|
||||||
applying = true;
|
applying = true;
|
||||||
try {
|
try {
|
||||||
await apiMutate('/v1/apply', 'POST', {});
|
const revId = revisions[0]?.id;
|
||||||
|
if (!revId) {
|
||||||
|
toast.error('Нет ревизий — сначала refresh модуля или дождитесь задачи render');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await apiMutate('/v1/apply', 'POST', { revision_id: revId });
|
||||||
toast.success('Apply запущен на всех спикерах');
|
toast.success('Apply запущен на всех спикерах');
|
||||||
applyConfirm = false;
|
applyConfirm = false;
|
||||||
await loadJobs();
|
await loadJobs();
|
||||||
@@ -265,7 +270,7 @@
|
|||||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle class="text-base">История ревизий</CardTitle>
|
<CardTitle class="text-base">История ревизий</CardTitle>
|
||||||
<CardDescription>Автоматически создаются при apply</CardDescription>
|
<CardDescription>Создаются задачей module_refresh (CDN / IP / AS и т.д.); в превью есть полный текст BIRD с инклюдами</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onclick={loadRevisions} disabled={revLoading}>
|
<Button variant="outline" size="sm" onclick={loadRevisions} disabled={revLoading}>
|
||||||
<RefreshCw class={revLoading ? 'animate-spin' : ''} />
|
<RefreshCw class={revLoading ? 'animate-spin' : ''} />
|
||||||
|
|||||||
Reference in New Issue
Block a user