diff --git a/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md b/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md index c6fce3f..bb30ea7 100644 --- a/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md +++ b/.cursor/plans/evobgp_архитектура_0e73ef02.plan.md @@ -26,6 +26,15 @@ todos: - id: replica-bundle content: Подписанный бандл ревизии, API, evobgp-node status: pending + - id: risk-hardening + content: Двухфазный deploy, LKG, pin BIRD, политика миграций, обязательная подпись бандла на ноде + status: completed + - id: test-bird-matrix + content: Матрица сценариев BIRD + golden-тесты birdfmt + bird -p в CI + status: completed + - id: ci-gitea + content: .gitea/workflows/ci.yaml, документация для act runner + status: completed isProject: false --- @@ -50,7 +59,10 @@ Control-plane на **Go**, анонс префиксов через **BIRD**, п 10. [Реплика evobgp-node](#10-реплика-evobgp-node) 11. [Выбор СУБД](#11-выбор-субд) 12. [Профиль microVPS — детализация](#12-профиль-microvps-детализация) -13. [Риски и этапы](#13-риски-и-этапы-внедрения) +13. [Снижение рисков (меры и процессы)](#13-снижение-рисков-меры-и-процессы) +14. [Тестирование BIRD2 и матрица сценариев](#14-тестирование-bird2-и-матрица-сценариев) +15. [CI/CD (Gitea Actions)](#15-cicd-gitea-actions) +16. [Риски и этапы внедрения](#16-риски-и-этапы-внедрения) --- @@ -576,7 +588,75 @@ Raft, HTTP/`gorqlite`; для HA control-plane; не замена дефолтн --- -## 13. Риски и этапы внедрения +## 13. Снижение рисков (меры и процессы) + +Дополняет [§9](#9-эксплуатация-и-масштаб) и [§10](#10-реплика-evobgp-node) конкретными обязательными практиками. + +### Конфигурация BIRD: безопасное применение + +- **Двухфазный deploy:** (1) запись новой ревизии во **временный** каталог на общем volume и проверка `**bird -c -p`** (парсинг без запуска демона; см. `bird(8)`); (2) только при нулевом коде выхода — **атомарная** подмена активных файлов (rename) и `**birdc configure`**. Опционально перед фазой (2) — `preview`/diff в control-plane (REST). +- **Last-known-good (LKG):** хранить на volume **предыдущую** применённую ревизию; при неуспехе `configure` или ненулевом exit **автоматически** восстановить файлы LKG, зафиксировать событие в логах/метриках и **не** оставлять BIRD в полусобранном состоянии. +- **Canary в production:** перед полным apply — отдельный `bgp_speaker` или подмножество `bgp_peer` (см. [§9](#9-эксплуатация-и-масштаб)); полный выкат только после проверки сессий/префиксов на канареечном спикере. + +### Данные, очередь, microVPS + +- **Миграции:** в основной ветке — только **вперёд**; откат схемы — явные down-миграции (если приняты в процессе) или восстановление БД из бэкапа; политика фиксируется в операторской документации. +- **Jobs:** обязательные `**idempotency_key`** и уникальность в `job_audit` там, где это предотвращает двойной apply/reload. +- **microVPS:** пороги мониторинга на рост таблиц ревизий/артефактов, **retention** старых ревизий, лимиты ротации логов Docker (см. [§12](#12-профиль-microvps-детализация)) — с **алертами** при приближении к лимиту диска и OOM. + +### Безопасность + +- **evobgp-node:** в production **обязательна** проверка **подписи** бандла (отдельно от TLS транспорта); ключ подписи не смешивать с другими ролями. +- Секреты BGP (пароли, ключи) — только secret store / Docker secrets; **не** логировать полные конфиги с секретами. + +### Поставка и совместимость + +- В **Dockerfile** / Compose зафиксировать **версию образа BIRD 2** (тег minor или digest), совпадающую с образом, в котором выполняется `**bird -p`** в CI ([§15](#15-cicd-gitea-actions)). +- **Статический каркас** (router id, локальные интерфейсы, операторские правки) — в файлах **вне** автогенерируемых фрагментов; сгенерированное — только в согласованных путях `bird.d/` (см. [§8](#8-генерация-bird-и-ревизии), [§10](#10-реплика-evobgp-node)). + +### Порядок внедрения (уточнение) + +Перед полным набором микросервисов целесообразен параллельный этап: **контракт OpenAPI + каркас `internal/birdfmt` + golden-тесты + проверка `bird -p` в CI** ([§14](#14-тестирование-bird2-и-матрица-сценариев), [§15](#15-cicd-gitea-actions)), затем миграции PG и один вертикальный сценарий (например `IP_RANGES`). + +--- + +## 14. Тестирование BIRD2 и матрица сценариев + +Цель — покрыть **поверхность генератора** EvoBGP, а не весь язык BIRD. Комбинации фиксируются **матрицей** и каталогом сценариев в репозитории (`internal/birdfmt/testdata/scenarios/`). + +### Матрица покрытия (ориентир) + + +| Измерение | Варианты для покрытия | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| Роль спикера | master (полный набор фрагментов); реплика / **бандл** для `evobgp-node` (manifest + подмножество includes + локальный `local.conf`) | +| Типы модулей (выход render) | `AS_PREFIXES`, `CDN_CIDRS`, `DOMAINS` (материализованные префиксы), `IP_RANGES` — минимум по одному сценарию; комбинация **2+ типов** в одной ревизии | +| Пиры | при поддержке каркасом — только static; **1× IPv4**, **1× IPv6**, несколько пиров, смешанный v4/v6 | +| Community | каждый поддерживаемый `kind` в справочнике + вариант без community (default) | +| Фильтры | экспорт «разрешить анонс»; при генерации — отрицательные кейсы (reject) | +| Граничные данные | пустой префикс-лист; один префикс; большой список (нагрузка на размер файла) | +| Шаблоны include | каждый именуемый фрагмент из `internal/birdfmt` участвует хотя бы в одном интеграционном сценарии | + + +По мере расширения генератора матрица **дополняется**; регрессия — новыми строками в таблице тестов и при необходимости новыми подкаталогами сценариев. + +### Виды тестов + +1. **Unit / snapshot (Go):** пакет `internal/birdfmt` — вход из фикстур, выход сравнивается с `*.golden` или `txtar`. +2. **Синтаксис BIRD в CI:** для каждого сценария с `bird.conf` выполняется `**bird -c … -p`** в контейнере с **той же major/minor версией BIRD 2**, что в production ([§13](#13-снижение-рисков-меры-и-процессы)). +3. **Позже:** контрактные тесты HTTP по `docs/openapi.yaml` после реализации `internal/httpapi`. + +--- + +## 15. CI/CD (Gitea Actions) + +- Workflows: каталог `**.gitea/workflows/`** в корне репозитория; синтаксис совместим с GitHub Actions ([документация Gitea Actions](https://docs.gitea.com/usage/actions/quickstart/)). +- Нужен зарегистрированный **act runner** с меткой `ubuntu-latest` (или согласованной с инсталляцией) и при job с Docker — доступ **Docker** на runner. +- Рекомендуемый pipeline: **lint OpenAPI** (`npx @redocly/cli lint docs/openapi.yaml`), `**go vet` / `go test` / `go build ./...`**, **проверка всех `testdata/scenarios/*/bird.conf` через `bird -p`** (см. workflow в репозитории). + +--- + +## 16. Риски и этапы внедрения ### Риски diff --git a/.gitea/README.md b/.gitea/README.md new file mode 100644 index 0000000..d7f725b --- /dev/null +++ b/.gitea/README.md @@ -0,0 +1,10 @@ +# Gitea Actions + +В репозитории включён workflow [workflows/ci.yaml](workflows/ci.yaml). + +Требования к runner: + +- Метка `ubuntu-latest` (или измените `runs-on` под свои labels). +- Для job `bird2` на runner должен быть доступен **Docker** (проверка `bird -c … -p` в контейнере `debian:bookworm-slim` с пакетом `bird2`). + +Секреты не обязательны для базового CI; для публикации образов добавьте свои шаги и credentials в отдельный workflow. diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..006cfae --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + openapi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Lint OpenAPI (Redocly) + run: npx --yes @redocly/cli@1 lint docs/openapi.yaml + + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + - name: Vet + run: go vet ./... + - name: Test + run: go test ./... -race -count=1 + - name: Build all commands + run: | + set -euxo pipefail + out="${RUNNER_TEMP}/evobgp-bin" + mkdir -p "$out" + for d in cmd/*/; do + name="$(basename "$d")" + go build -o "$out/$name" "./$d" + done + + bird2: + runs-on: ubuntu-latest + needs: [go] + steps: + - uses: actions/checkout@v4 + - name: bird -p on all scenario bird.conf files + run: | + set -euxo pipefail + for conf in internal/birdfmt/testdata/scenarios/*/bird.conf; do + echo "==> $conf" + docker run --rm \ + -e "BIRD_CONF=/work/${conf}" \ + -v "$PWD:/work:ro" \ + debian:bookworm-slim \ + bash -ceu 'apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bird2 >/dev/null && bird -c "$BIRD_CONF" -p' + done diff --git a/cmd/evobgp-agent/main.go b/cmd/evobgp-agent/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-agent/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/cmd/evobgp-all/main.go b/cmd/evobgp-all/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-all/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/cmd/evobgp-api/main.go b/cmd/evobgp-api/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-api/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/cmd/evobgp-deploy/main.go b/cmd/evobgp-deploy/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-deploy/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/cmd/evobgp-ingest/main.go b/cmd/evobgp-ingest/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-ingest/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/cmd/evobgp-node/main.go b/cmd/evobgp-node/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-node/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/cmd/evobgp-render/main.go b/cmd/evobgp-render/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-render/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/cmd/evobgp-scheduler/main.go b/cmd/evobgp-scheduler/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/evobgp-scheduler/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c661833 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module evobgp + +go 1.22 diff --git a/internal/birdfmt/scenarios.go b/internal/birdfmt/scenarios.go new file mode 100644 index 0000000..87320cb --- /dev/null +++ b/internal/birdfmt/scenarios.go @@ -0,0 +1,64 @@ +package birdfmt + +import ( + "embed" + "io/fs" + "path" + "sort" +) + +//go:embed testdata/scenarios +var scenariosFS embed.FS + +// BirdScenarioPaths returns POSIX-style directory paths (under testdata/scenarios) +// that contain a bird.conf file, sorted for stable iteration. +func BirdScenarioPaths() ([]string, error) { + var dirs []string + err := fs.WalkDir(scenariosFS, "testdata/scenarios", func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || d.Name() != "bird.conf" { + return nil + } + dir := path.Dir(p) + if dir == "testdata/scenarios" { + return nil + } + dirs = append(dirs, dir) + return nil + }) + if err != nil { + return nil, err + } + sort.Strings(dirs) + return dirs, nil +} + +// ScenarioBirdConfRel returns the path relative to module root internal/birdfmt/ for bird.conf. +func ScenarioBirdConfRel(scenarioDir string) string { + return path.Join(scenarioDir, "bird.conf") +} + +// ReadScenarioBirdConf returns the contents of bird.conf for a scenario directory from embed. +func ReadScenarioBirdConf(scenarioDir string) ([]byte, error) { + return scenariosFS.ReadFile(path.Join(scenarioDir, "bird.conf")) +} + +// RepoRelativeBirdConf is the path from the repository root (for CI / docker bind mounts). +func RepoRelativeBirdConf(scenarioDir string) string { + return path.Join("internal/birdfmt", scenarioDir, "bird.conf") +} + +// AllScenarioNames returns leaf scenario names (last path segment) for tables / docs. +func AllScenarioNames() ([]string, error) { + paths, err := BirdScenarioPaths() + if err != nil { + return nil, err + } + names := make([]string, len(paths)) + for i, p := range paths { + names[i] = path.Base(p) + } + return names, nil +} diff --git a/internal/birdfmt/scenarios_test.go b/internal/birdfmt/scenarios_test.go new file mode 100644 index 0000000..162fbf8 --- /dev/null +++ b/internal/birdfmt/scenarios_test.go @@ -0,0 +1,68 @@ +package birdfmt + +import ( + "path" + "slices" + "strings" + "testing" +) + +func TestBirdScenarioPaths_Table(t *testing.T) { + paths, err := BirdScenarioPaths() + if err != nil { + t.Fatal(err) + } + // Expected scenario leaf names — extend when adding directories under testdata/scenarios. + wantNames := []string{ + "bgp_dual_peer", + "bgp_ipv4_peer", + "bgp_ipv6_peer", + "domains_resolved", + "empty_static", + "filter_export", + "large_prefix_list", + "mixed_static_bgp", + "minimal", + "static_ipv4", + "with_include", + } + var gotNames []string + for _, p := range paths { + gotNames = append(gotNames, path.Base(p)) + } + slices.Sort(gotNames) + if !slices.Equal(gotNames, wantNames) { + t.Fatalf("scenario set drift: got %v want %v", gotNames, wantNames) + } + + for _, p := range paths { + t.Run(path.Base(p), func(t *testing.T) { + b, err := ReadScenarioBirdConf(p) + if err != nil { + t.Fatal(err) + } + if len(b) == 0 { + t.Fatal("empty bird.conf") + } + if !strings.Contains(string(b), "router id") { + t.Fatal("expected router id in scenario") + } + }) + } +} + +func TestRepoRelativeBirdConf_Table(t *testing.T) { + paths, err := BirdScenarioPaths() + if err != nil { + t.Fatal(err) + } + for _, p := range paths { + rel := RepoRelativeBirdConf(p) + if !strings.HasPrefix(rel, "internal/birdfmt/") { + t.Fatalf("%s: bad prefix %q", p, rel) + } + if !strings.HasSuffix(rel, "/bird.conf") { + t.Fatalf("%s: expected .../bird.conf, got %q", p, rel) + } + } +} diff --git a/internal/birdfmt/static.go b/internal/birdfmt/static.go new file mode 100644 index 0000000..be16ba0 --- /dev/null +++ b/internal/birdfmt/static.go @@ -0,0 +1,39 @@ +package birdfmt + +import ( + "net/netip" + "sort" + "strings" +) + +// RenderStaticIPv4Protocol renders a BIRD 2 `protocol static` block for IPv4 prefixes. +// Prefixes are sorted for stable output (golden tests, reproducible rev). +func RenderStaticIPv4Protocol(protocolName string, prefixes []netip.Prefix) string { + if protocolName == "" { + protocolName = "evobgp_static_v4" + } + uniq := make(map[string]netip.Prefix) + for _, p := range prefixes { + if !p.Addr().Is4() { + continue + } + uniq[p.String()] = p.Masked() + } + keys := make([]string, 0, len(uniq)) + for k := range uniq { + keys = append(keys, k) + } + sort.Strings(keys) + + var b strings.Builder + b.WriteString("protocol static ") + b.WriteString(protocolName) + b.WriteString(" {\n ipv4;\n") + for _, k := range keys { + b.WriteString(" route ") + b.WriteString(k) + b.WriteString(" unreachable;\n") + } + b.WriteString("}\n") + return b.String() +} diff --git a/internal/birdfmt/static_test.go b/internal/birdfmt/static_test.go new file mode 100644 index 0000000..7f7590a --- /dev/null +++ b/internal/birdfmt/static_test.go @@ -0,0 +1,34 @@ +package birdfmt + +import ( + _ "embed" + "net/netip" + "strings" + "testing" +) + +//go:embed testdata/golden/static_ipv4_small.golden +var goldenStaticIPv4Small string + +func TestRenderStaticIPv4Protocol_Golden(t *testing.T) { + p1 := netip.MustParsePrefix("203.0.113.0/24") + p2 := netip.MustParsePrefix("198.51.100.0/24") + // duplicate to assert dedup + got := RenderStaticIPv4Protocol("evobgp_test", []netip.Prefix{p1, p2, p1}) + + want := strings.TrimSuffix(goldenStaticIPv4Small, "\n") + got = strings.TrimSuffix(got, "\n") + if got != want { + t.Fatalf("golden mismatch\n--- got ---\n%s\n--- want ---\n%s", got, want) + } +} + +func TestRenderStaticIPv4Protocol_Empty(t *testing.T) { + got := RenderStaticIPv4Protocol("evobgp_empty", nil) + if !strings.Contains(got, "protocol static evobgp_empty") { + t.Fatal(got) + } + if !strings.Contains(got, "ipv4") { + t.Fatal(got) + } +} diff --git a/internal/birdfmt/testdata/golden/static_ipv4_small.golden b/internal/birdfmt/testdata/golden/static_ipv4_small.golden new file mode 100644 index 0000000..2f619fa --- /dev/null +++ b/internal/birdfmt/testdata/golden/static_ipv4_small.golden @@ -0,0 +1,5 @@ +protocol static evobgp_test { + ipv4; + route 198.51.100.0/24 unreachable; + route 203.0.113.0/24 unreachable; +} diff --git a/internal/birdfmt/testdata/scenarios/bgp_dual_peer/bird.conf b/internal/birdfmt/testdata/scenarios/bgp_dual_peer/bird.conf new file mode 100644 index 0000000..aec5bcc --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/bgp_dual_peer/bird.conf @@ -0,0 +1,28 @@ +# tags: peer, ipv4, ipv6, mixed +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +protocol bgp peer_a { + local 192.0.2.1 as 65001; + neighbor 192.0.2.2 as 65002; + ipv4 { + import all; + export all; + }; +} + +protocol bgp peer_b { + local fd00:ebgp::1 as 65001; + neighbor fd00:ebgp::3 as 65003; + ipv6 { + import all; + export all; + }; +} diff --git a/internal/birdfmt/testdata/scenarios/bgp_ipv4_peer/bird.conf b/internal/birdfmt/testdata/scenarios/bgp_ipv4_peer/bird.conf new file mode 100644 index 0000000..9d27dd4 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/bgp_ipv4_peer/bird.conf @@ -0,0 +1,19 @@ +# tags: peer, ipv4, AS_PREFIXES +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +protocol bgp peer_v4 { + local 192.0.2.1 as 65001; + neighbor 192.0.2.2 as 65002; + ipv4 { + import all; + export all; + }; +} diff --git a/internal/birdfmt/testdata/scenarios/bgp_ipv6_peer/bird.conf b/internal/birdfmt/testdata/scenarios/bgp_ipv6_peer/bird.conf new file mode 100644 index 0000000..d37bb50 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/bgp_ipv6_peer/bird.conf @@ -0,0 +1,19 @@ +# tags: peer, ipv6, AS_PREFIXES +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +protocol bgp peer_v6 { + local fd00:ebgp::1 as 65001; + neighbor fd00:ebgp::2 as 65002; + ipv6 { + import all; + export all; + }; +} diff --git a/internal/birdfmt/testdata/scenarios/domains_resolved/bird.conf b/internal/birdfmt/testdata/scenarios/domains_resolved/bird.conf new file mode 100644 index 0000000..6e9a839 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/domains_resolved/bird.conf @@ -0,0 +1,18 @@ +# tags: DOMAINS, module_output +# DOMAINS modules materialize to prefixes before render; same shape as static IPv4. + +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +protocol static evobgp_from_domains { + ipv4; + route 192.0.2.10/32 unreachable; + route 192.0.2.11/32 unreachable; +} diff --git a/internal/birdfmt/testdata/scenarios/empty_static/bird.conf b/internal/birdfmt/testdata/scenarios/empty_static/bird.conf new file mode 100644 index 0000000..4fb47c5 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/empty_static/bird.conf @@ -0,0 +1,16 @@ +# tags: edge, IP_RANGES +# Empty static IPv4 table (generator must emit valid empty protocol). + +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +protocol static evobgp_empty { + ipv4; +} diff --git a/internal/birdfmt/testdata/scenarios/filter_export/bird.conf b/internal/birdfmt/testdata/scenarios/filter_export/bird.conf new file mode 100644 index 0000000..f4d6a0f --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/filter_export/bird.conf @@ -0,0 +1,26 @@ +# tags: community, filter, peer +# Export filter (placeholder for community-aware export). + +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +filter evobgp_export_v4 { + if net ~ [ 203.0.113.0/24 ] then accept; + reject; +} + +protocol bgp peer_filtered { + local 192.0.2.1 as 65001; + neighbor 192.0.2.2 as 65002; + ipv4 { + import all; + export filter evobgp_export_v4; + }; +} diff --git a/internal/birdfmt/testdata/scenarios/large_prefix_list/bird.conf b/internal/birdfmt/testdata/scenarios/large_prefix_list/bird.conf new file mode 100644 index 0000000..1c95806 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/large_prefix_list/bird.conf @@ -0,0 +1,12 @@ +# tags: stress, CDN_CIDRS +router id 192.0.2.1; + +include "bird.d/bulk_static.conf"; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} diff --git a/internal/birdfmt/testdata/scenarios/large_prefix_list/bird.d/bulk_static.conf b/internal/birdfmt/testdata/scenarios/large_prefix_list/bird.d/bulk_static.conf new file mode 100644 index 0000000..aa0e419 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/large_prefix_list/bird.d/bulk_static.conf @@ -0,0 +1,23 @@ +protocol static evobgp_bulk { + ipv4; + route 10.200.0.0/24 unreachable; + route 10.200.1.0/24 unreachable; + route 10.200.2.0/24 unreachable; + route 10.200.3.0/24 unreachable; + route 10.200.4.0/24 unreachable; + route 10.200.5.0/24 unreachable; + route 10.200.6.0/24 unreachable; + route 10.200.7.0/24 unreachable; + route 10.200.8.0/24 unreachable; + route 10.200.9.0/24 unreachable; + route 10.200.10.0/24 unreachable; + route 10.200.11.0/24 unreachable; + route 10.200.12.0/24 unreachable; + route 10.200.13.0/24 unreachable; + route 10.200.14.0/24 unreachable; + route 10.200.15.0/24 unreachable; + route 10.200.16.0/24 unreachable; + route 10.200.17.0/24 unreachable; + route 10.200.18.0/24 unreachable; + route 10.200.19.0/24 unreachable; +} diff --git a/internal/birdfmt/testdata/scenarios/minimal/bird.conf b/internal/birdfmt/testdata/scenarios/minimal/bird.conf new file mode 100644 index 0000000..dae7f12 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/minimal/bird.conf @@ -0,0 +1,12 @@ +# tags: baseline, master +# Minimal BIRD 2 skeleton (device + direct), no EvoBGP-generated fragments yet. + +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} diff --git a/internal/birdfmt/testdata/scenarios/mixed_static_bgp/bird.conf b/internal/birdfmt/testdata/scenarios/mixed_static_bgp/bird.conf new file mode 100644 index 0000000..c108542 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/mixed_static_bgp/bird.conf @@ -0,0 +1,27 @@ +# tags: IP_RANGES, CDN_CIDRS, peer, combined +# Combined static materialization + BGP session (multi-module revision sketch). + +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +protocol static evobgp_prefixes { + ipv4; + route 203.0.113.0/24 unreachable; + route 203.0.114.0/24 unreachable; +} + +protocol bgp uplink { + local 192.0.2.1 as 65001; + neighbor 192.0.2.2 as 65002; + ipv4 { + import all; + export all; + }; +} diff --git a/internal/birdfmt/testdata/scenarios/static_ipv4/bird.conf b/internal/birdfmt/testdata/scenarios/static_ipv4/bird.conf new file mode 100644 index 0000000..f40b1dc --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/static_ipv4/bird.conf @@ -0,0 +1,18 @@ +# tags: IP_RANGES, module_output +# Static IPv4 routes only (materialized prefix list style). + +router id 192.0.2.1; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} + +protocol static evobgp_v4 { + ipv4; + route 198.51.100.0/24 unreachable; + route 203.0.113.0/24 unreachable; +} diff --git a/internal/birdfmt/testdata/scenarios/with_include/bird.conf b/internal/birdfmt/testdata/scenarios/with_include/bird.conf new file mode 100644 index 0000000..d7dc6bc --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/with_include/bird.conf @@ -0,0 +1,14 @@ +# tags: baseline, include +# Operator shell includes generated fragments from bird.d/ + +router id 192.0.2.1; + +include "bird.d/static.conf"; + +protocol device { +} + +protocol direct { + ipv4; + ipv6; +} diff --git a/internal/birdfmt/testdata/scenarios/with_include/bird.d/static.conf b/internal/birdfmt/testdata/scenarios/with_include/bird.d/static.conf new file mode 100644 index 0000000..f1876f6 --- /dev/null +++ b/internal/birdfmt/testdata/scenarios/with_include/bird.d/static.conf @@ -0,0 +1,4 @@ +protocol static evobgp_inc { + ipv4; + route 203.0.113.0/24 unreachable; +}