Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/on-pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:
- A03b
- A05
- A06
- A14
- A62
- B00
- B02
Expand Down Expand Up @@ -104,6 +105,8 @@ jobs:
examples/basic/A05_components/test_runtime.py \
examples/basic/A06_merge_emulation/merge_emulation.py \
examples/basic/A06_merge_emulation/test_runtime.py \
examples/basic/A14_control_plane_regression/control_plane_regression.py \
examples/basic/A14_control_plane_regression/test_runtime.py \
examples/basic/A62_route_reflector/route_reflector.py \
examples/basic/A62_route_reflector/test_runtime.py \
examples/internet/B00_mini_internet/mini_internet.py \
Expand All @@ -124,6 +127,7 @@ jobs:
python seedemu/testing/cli.py clean examples/basic/A03b_from_real_world/example.yaml
python seedemu/testing/cli.py clean examples/basic/A05_components/example.yaml
python seedemu/testing/cli.py clean examples/basic/A06_merge_emulation/example.yaml
python seedemu/testing/cli.py clean examples/basic/A14_control_plane_regression/example.yaml
python seedemu/testing/cli.py clean examples/basic/A62_route_reflector/example.yaml
python seedemu/testing/cli.py clean examples/internet/B00_mini_internet/example.yaml
python seedemu/testing/cli.py clean examples/internet/B02_mini_internet_with_dns/example.yaml
Expand Down Expand Up @@ -168,6 +172,10 @@ jobs:
category: basic
requires_mpls: false
manifest: examples/basic/A06_merge_emulation/example.yaml
- code: A14
category: basic
requires_mpls: false
manifest: examples/basic/A14_control_plane_regression/example.yaml
- code: A62
category: basic
requires_mpls: false
Expand Down Expand Up @@ -273,6 +281,10 @@ jobs:
category: basic
requires_mpls: false
manifest: examples/basic/A06_merge_emulation/example.yaml
- code: A14
category: basic
requires_mpls: false
manifest: examples/basic/A14_control_plane_regression/example.yaml
- code: A62
category: basic
requires_mpls: false
Expand Down
26 changes: 26 additions & 0 deletions examples/basic/A14_control_plane_regression/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A14 Control-Plane Regression

This example is the compact runtime regression entry for the control-plane
foundation work. It intentionally keeps several independent slices in one
emulation so a reviewer can run one example after changes to Routing, BGP
intent metadata, FRR rendering, ExaBGP service binding, or MPLS readiness.

Covered slices:

- IX100 uses the legacy BIRD route server path with AS150/AS151 clients.
- AS2 mixes a BIRD router and an FRR router from the same OSPF/iBGP/eBGP intent.
- AS3 uses an FRR route reflector and an FRR RR client.
- AS180 installs ExaBGP through `ExaBgpService + Binding` and peers with an FRR
router in AS4. ExaBGP is not a router backend.
- AS20 enables MPLS/LDP readiness on a small transit chain. The default runtime
test checks generated MPLS interface and FRR LDP config. Full MPLS dataplane
validation remains host-gated because GitHub hosted runners may not provide
MPLS kernel modules.

Run it locally:

```bash
python -m seedemu.testing.cli clean examples/basic/A14_control_plane_regression/example.yaml
python -m seedemu.testing.cli compile examples/basic/A14_control_plane_regression/example.yaml
COMPOSE_PROJECT_NAME=seedemu-a14-control-plane python -m seedemu.testing.cli all examples/basic/A14_control_plane_regression/example.yaml
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
# encoding: utf-8

from __future__ import annotations

import argparse
from pathlib import Path
import sys


SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[2]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))

from seedemu.compiler import Docker, Platform
from seedemu.core import Binding, Emulator, Filter
from seedemu.layers import Base, Ebgp, Ibgp, Mpls, Ospf, PeerRelationship, Routing
from seedemu.services import ExaBgpService, WebService


AS3_CLUSTER_ID = "10.3.0.1"
EXABGP_PREFIX = "198.51.100.0/24"


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build the A14 control-plane regression example.")
parser.add_argument("legacy_platform", nargs="?", choices=["amd", "arm"])
parser.add_argument("--platform", choices=["amd", "arm"])
parser.add_argument("--output", default=str(SCRIPT_DIR / "output"))
parser.add_argument("--dumpfile")
parser.add_argument("--override", dest="override", action="store_true", default=True)
parser.add_argument("--no-override", dest="override", action="store_false")
parser.add_argument("--skip-render", dest="render", action="store_false", default=True)
args = parser.parse_args()
args.platform = args.platform or args.legacy_platform or "amd"
return args


def resolve_platform(name: str) -> Platform:
return Platform.AMD64 if name == "amd" else Platform.ARM64


def build_route_server_slice(emu: Emulator, base: Base, ebgp: Ebgp, web: WebService) -> None:
"""Keep the legacy BIRD route-server path visible in the regression topology."""

base.createInternetExchange(100)

for asn in [150, 151]:
current_as = base.createAutonomousSystem(asn)
current_as.createNetwork("net0")
current_as.createRouter("router0").joinNetwork("net0").joinNetwork("ix100")
current_as.createHost("web").joinNetwork("net0")

vnode = "web{}".format(asn)
web.install(vnode)
emu.addBinding(Binding(vnode, filter=Filter(nodeName="web", asn=asn)))
ebgp.addRsPeer(100, asn)


def build_mixed_backend_slice(emu: Emulator, base: Base, ebgp: Ebgp, web: WebService) -> None:
"""Exercise one transit AS with BIRD and FRR routers from shared BGP/OSPF intent."""

base.createInternetExchange(101)
base.createInternetExchange(102)

as2 = base.createAutonomousSystem(2)
as2.createNetwork("net0")
as2.createRouter("r1").joinNetwork("net0").joinNetwork("ix101")
as2.createRouter("r2", routingBackend="frr").joinNetwork("net0").joinNetwork("ix102")

as152 = base.createAutonomousSystem(152)
as152.createNetwork("net0")
as152.createRouter("router0", routingBackend="frr").joinNetwork("net0").joinNetwork("ix101")
as152.createHost("web").joinNetwork("net0")
web.install("web152")
emu.addBinding(Binding("web152", filter=Filter(nodeName="web", asn=152)))

as153 = base.createAutonomousSystem(153)
as153.createNetwork("net0")
as153.createRouter("router0").joinNetwork("net0").joinNetwork("ix102")
as153.createHost("web").joinNetwork("net0")
web.install("web153")
emu.addBinding(Binding("web153", filter=Filter(nodeName="web", asn=153)))

ebgp.addPrivatePeering(101, 2, 152, abRelationship=PeerRelationship.Provider)
ebgp.addPrivatePeering(102, 2, 153, abRelationship=PeerRelationship.Provider)


def build_frr_route_reflector_slice(base: Base, ebgp: Ebgp) -> None:
"""Validate FRR route-reflector rendering and runtime route propagation."""

base.createInternetExchange(103)

as3 = base.createAutonomousSystem(3)
as3.createNetwork("net0")
as3.createBgpCluster(AS3_CLUSTER_ID)
as3.createRouter("rr", routingBackend="frr").joinNetwork("net0").joinNetwork("ix103").joinBgpCluster(AS3_CLUSTER_ID).makeRouteReflector()
as3.createRouter("client", routingBackend="frr").joinNetwork("net0").joinBgpCluster(AS3_CLUSTER_ID)

as154 = base.createAutonomousSystem(154)
as154.createNetwork("net0")
as154.createRouter("router0").joinNetwork("net0").joinNetwork("ix103")
as154.createHost("host").joinNetwork("net0")

ebgp.addPrivatePeering(103, 3, 154, abRelationship=PeerRelationship.Provider)


def build_exabgp_slice(base: Base, exabgp: ExaBgpService, emu: Emulator) -> None:
"""Install ExaBGP as a Service + Binding speaker, never as a router backend."""

base.createInternetExchange(104)

as4 = base.createAutonomousSystem(4)
as4.createNetwork("net0")
as4.createRouter("router0", routingBackend="frr").joinNetwork("net0").joinNetwork("ix104")

as180 = base.createAutonomousSystem(180)
as180.createHost("exabgp").joinNetwork("ix104", address="10.104.0.180")

exabgp.install("as180_exabgp") \
.setLocalAsn(180) \
.addPeer("router0", router_asn=4, router_relationship="customer") \
.addAnnouncement(EXABGP_PREFIX)
emu.addBinding(Binding("as180_exabgp", filter=Filter(asn=180, nodeName="exabgp")))


def build_mpls_readiness_slice(base: Base, ebgp: Ebgp, mpls: Mpls) -> None:
"""Keep MPLS config/readiness in scope while leaving dataplane checks host-gated."""

base.createInternetExchange(105)
base.createInternetExchange(106)

as20 = base.createAutonomousSystem(20)
as20.createNetwork("net0")
as20.createNetwork("net1")
as20.createNetwork("net2")
as20.createRouter("r1").joinNetwork("net0").joinNetwork("ix105")
as20.createRouter("r2").joinNetwork("net0").joinNetwork("net1")
as20.createRouter("r3").joinNetwork("net1").joinNetwork("net2")
as20.createRouter("r4").joinNetwork("net2").joinNetwork("ix106")
mpls.enableOn(20)

as155 = base.createAutonomousSystem(155)
as155.createNetwork("net0")
as155.createRouter("router0").joinNetwork("net0").joinNetwork("ix105")

as156 = base.createAutonomousSystem(156)
as156.createNetwork("net0")
as156.createRouter("router0").joinNetwork("net0").joinNetwork("ix106")

ebgp.addPrivatePeering(105, 20, 155, abRelationship=PeerRelationship.Provider)
ebgp.addPrivatePeering(106, 20, 156, abRelationship=PeerRelationship.Provider)


def build_emulator() -> Emulator:
emu = Emulator()

base = Base()
routing = Routing()
ospf = Ospf()
ibgp = Ibgp()
ebgp = Ebgp()
mpls = Mpls()
exabgp = ExaBgpService()
web = WebService()

build_route_server_slice(emu, base, ebgp, web)
build_mixed_backend_slice(emu, base, ebgp, web)
build_frr_route_reflector_slice(base, ebgp)
build_exabgp_slice(base, exabgp, emu)
build_mpls_readiness_slice(base, ebgp, mpls)

emu.addLayer(base)
emu.addLayer(routing)
emu.addLayer(ospf)
emu.addLayer(ibgp)
emu.addLayer(ebgp)
emu.addLayer(mpls)
emu.addLayer(exabgp)
emu.addLayer(web)
return emu


def main() -> int:
args = parse_args()
emu = build_emulator()

if args.dumpfile:
emu.dump(args.dumpfile)
print("Saved A14 emulator to {}".format(args.dumpfile))
return 0

if args.render:
emu.render()

output_dir = Path(args.output).resolve()
output_dir.parent.mkdir(parents=True, exist_ok=True)
emu.compile(Docker(platform=resolve_platform(args.platform)), str(output_dir), override=args.override)
print("Generated A14 Docker output in {}".format(output_dir))
return 0


if __name__ == "__main__":
raise SystemExit(main())
94 changes: 94 additions & 0 deletions examples/basic/A14_control_plane_regression/example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
id: basic-a14-control-plane-regression
name: Control-Plane Regression
description: A compact regression topology for route-server, mixed BIRD/FRR, FRR route-reflector, ExaBGP service, and MPLS readiness coverage.
runner: internet
script: control_plane_regression.py
platform: amd
features:
- ipv4-default
- ebgp-route-server
- ebgp-private-peering
- routing-backend
- bird
- frr
- ibgp
- ibgp-route-reflector
- ospf
- exabgp-service
- mpls-readiness

compile:
enabled: true
output: output
clean:
- output
expected:
- output/docker-compose.yml
timeout: 600

build:
enabled: true
timeout: 1800

runtime:
compose: output/docker-compose.yml
readiness:
- name: A14 control-plane services are running
type: compose-ps
services:
- rs_ix_ix100
- brdnode_150_router0
- brdnode_151_router0
- brdnode_2_r1
- brdnode_2_r2
- brdnode_152_router0
- brdnode_153_router0
- brdnode_3_rr
- rnode_3_client
- brdnode_154_router0
- brdnode_4_router0
- hnode_180_exabgp
- brdnode_20_r1
- rnode_20_r2
- rnode_20_r3
- brdnode_20_r4
retries: 45
interval: 3

probes:
- name: AS151 learns AS150 prefix through the BIRD route server
type: exec
service: brdnode_151_router0
command: birdc show route | grep -q '10.150.0.0/24'
expect_exit: 0
retries: 40
interval: 5

- name: AS152 FRR router learns AS153 route through mixed-backend iBGP
type: exec
service: brdnode_152_router0
command: vtysh -c "show ip bgp" | grep -q '10.153.0.0/24'
expect_exit: 0
retries: 40
interval: 5

- name: AS3 FRR route-reflector client learns AS154 route
type: exec
service: rnode_3_client
command: vtysh -c "show ip bgp" | grep -q '10.154.0.0/24'
expect_exit: 0
retries: 40
interval: 5

- name: AS4 FRR router learns ExaBGP static route
type: exec
service: brdnode_4_router0
command: vtysh -c "show ip bgp 198.51.100.0/24" | grep -q '198.51.100.0/24'
expect_exit: 0
retries: 40
interval: 5

test_programs:
- name: A14 control-plane regression runtime validation
script: test_runtime.py
timeout: 1200
Loading
Loading