Architecture
This page summarises the data flow and the role of each directory.
Data flow
Schema definition → Bootstrap data → Generators → Transforms → Artifacts
↓
Checks
Directory map
| Path | Purpose |
|---|---|
schemas/base/ | Core node definitions copied from schema-library |
schemas/extensions/ | VRF, routing, BGP, topology extensions from schema-library |
schemas/sp/ | SP-demo-specific schemas (MPLS, L3VPN service, PE role) |
objects/ | Pre-loaded bootstrap data (PEs, backbone, pools, tenants) |
generators/ | L3VpnGenerator materialises VRF + interfaces + IPs |
transforms/ | One Python+Jinja transform per vendor + the clab transform |
templates/ | Jinja2 templates (one per vendor + clab + macros) |
checks/ | Five checks gating the proposed-change pipeline (incl. Batfish) |
service_catalog/ | Streamlit sidecar (Dashboard + Create L3VPN) |
queries/ | GraphQL queries used by generators, transforms, checks |
menus/ | Sidebar menu configuration |
lab/ | Runtime-only; not committed |
Backbone
The MPLS backbone is static demo data and is dataset-specific — each
dataset ships its own backbone. The default financial dataset has 8 PEs,
all Arista cEOS (pe-01…pe-08), in a partial mesh of 15 p2p links over a
full iBGP mesh. pe-01 (London) and pe-08 (Zurich) are the two-degree hub
PEs that carry every customer attachment; pe-02…pe-07 form the meshed core
between them at degree 4–5. The isp dataset has 4 PEs, one per vendor, in a
full mesh (6 p2p links). Both run ISIS L2 for the underlay and LDP for label
distribution. To add or move PEs, edit
objects/datasets/<dataset>/60_backbone.yml (and 70_topology.yml for the
p2p links) by hand and rerun invoke bootstrap.
Customer edge
The financial dataset also ships four pre-provisioned CE routers
(objects/datasets/financial/65_ce.yml), two per customer, attached to the hub
PEs:
| CE | Customer (tenant) | PE | L3VPN |
|---|---|---|---|
ce-trading-lon | markets-trading | pe-01 Ethernet3 | trading-floor-vpn |
ce-trading-zrh | markets-trading | pe-08 Ethernet3 | trading-floor-vpn |
ce-ib-lon | investment-banking | pe-01 Ethernet4 | ib-advisory-vpn |
ce-ib-zrh | investment-banking | pe-08 Ethernet4 | ib-advisory-vpn |
They are Arista cEOS boxes with their own rendered configuration artifact
(ce-arista-eos), so the PE-CE eBGP session actually establishes in the
containerlab topology rather than being modelled only.
The PE ports they land on are seeded in 60_backbone.yml with
status: active and a description matching the L3VPN generator's idempotency
key (L3VPN <vpn name>). That is what pins the wiring: the generator binds the
site to that exact port instead of allocating the next free one, and the port is
already out of the free pool so no later service can claim it.
Customer autonomous system numbers
PE-CE eBGP peer ASNs come from the customer_asn_pool number pool
(65100–65199, objects/50_pools.yml). The L3VPN generator allocates one AS
per VPN — a customer is one routing domain across its sites — and links it
as customer_asn on the VPN. A site may still set bgp_peer_asn to override
it, which is how the isp dataset peers with pre-agreed customer AS numbers;
when every eBGP site overrides, no pool ASN is consumed.
L3VPN service flow
When an operator creates an L3VPN through the Streamlit catalog:
- The catalog allocates a
vpn_idfrom thevpn_id_poolnumber pool. - It opens a feature branch and creates
ServiceL3Vpn+ one or moreServiceL3VpnSiteobjects. - It adds the
ServiceL3Vpnto thel3vpnsgroup. A group-membership trigger (objects/events/00_triggers.yml) firesL3VpnGeneratoron the branch, which materialises the VRF, route targets, PE-CE interfaces, IP addresses, and an eBGP session if the routing protocol is set toebgp, all inside the customer's own IP namespace. The generator runs event-driven rather than in the proposed-change pipeline (execute_in_proposed_change: false) so its data lands before artifacts render — otherwise the pipeline races and the configuration diff comes out empty. A second rule in the same file re-runs the generator when a site is added to or removed from a service that is already in the group; see services/l3vpn for what that rule cannot cover. - Once the generator finishes, per-PE configuration artifacts are rendered by the transform layer against the now-complete branch data.
- The catalog opens a
CoreProposedChangetargetingmain. - The proposed-change checks run in the pipeline — any failure blocks the merge.
- The operator reviews the diff in the Infrahub UI and merges.
See services/l3vpn for the full service reference.
Transform layer
Each vendor has a dedicated Python transform module (in transforms/) backed
by a Jinja2 template (in templates/). The transform fetches the full PE
state via GraphQL and renders a single, complete device configuration
fragment. The mapping is:
| Vendor | Transform | Template |
|---|---|---|
| Arista EOS | transforms/pe_arista_eos.py | templates/pe_arista_eos.j2 |
| Cisco IOS-XR | transforms/pe_cisco_iosxr.py | templates/pe_cisco_iosxr.j2 |
| Juniper Junos | transforms/pe_juniper_junos.py | templates/pe_juniper_junos.j2 |
| Nokia SR OS | transforms/pe_nokia_sros.py | templates/pe_nokia_sros.j2 |
| Containerlab | transforms/clab_topology.py | templates/clab_topology.j2 |
All four per-vendor PE transforms are still in play: the isp dataset has one
PE per vendor and exercises all of them. The default financial backbone is
all-Arista, so it only renders through pe_arista_eos.
Checks
Five checks gate the proposed-change pipeline:
| Check | What it enforces |
|---|---|
l3vpn_overlap | No duplicate VPN IDs across active L3VPNs |
l3vpn_site_subnet | Customer subnet is reachable / not already allocated in the same VRF |
pe_interface_alloc | The nominated PE interface is free (status = free) |
backbone_session_count | Every PE has its full-mesh complement of N−1 iBGP sessions (N = PE count: 7 each for the 8-PE financial backbone, 3 each for the 4-PE isp backbone) |
batfish_backbone | Batfish-driven static validation of rendered backbone configs (parse status, undefined references, BGP session compatibility, IS-IS adjacency mesh). See Batfish validation. |
Per-vendor interface name macros
The schema uses an abstract interface name (Ethernet1, Ethernet2, …);
each vendor template translates that to the platform-native form via a
macro in templates/_macros.j2:
| Vendor | Macro | Ethernet1 becomes |
|---|---|---|
| Arista EOS | (no translation) | Ethernet1 |
| Cisco IOS-XR | iosxr_iface | GigabitEthernet0/0/0/0 |
| Juniper Junos | junos_iface | ge-0/0/0 |
| SR Linux (lab substitute) | srl_iface | ethernet-1/1 |
Without these macros the rendered IOS-XR / Junos configs would carry
Arista-style Ethernet<N> interface names — invalid on the target
platform and reported as parse errors by BatfishBackboneCheck.
Schema layering
Schemas are loaded in three passes so each layer can reference the previous:
invoke bootstrap
└── infrahubctl schema load schemas/base/
└── infrahubctl schema load schemas/extensions/
└── infrahubctl schema load schemas/sp/
The SP layer adds ServiceL3Vpn, ServiceL3VpnSite, TopologyMplsBackbone,
MplsIsisProcess, MplsLdpProcess, and MplsBgpProcess. See
schema-reference for field-level details.