Network Configuration Framework

Active Rust

Contents

Concept

Network Configuration Framework is a Rust CLI that compiles declarative YAML blueprints into vendor-neutral configuration artifacts. A single binary orchestrates the full pipeline: blueprint parsing, topology transformation, DeviceIR generation, template rendering, and traceable config file emission.

The core problem is determinism. Hand-written network configs drift between devices, between sites, between engineers. The Network Configuration Framework treats configuration as a compilation target: a blueprint declares intent (protocol layers, IP pools, policy constraints), and the compiler produces auditable, diffable output files suitable for CI/CD pipelines.

Blueprints are composed from importable fragments. A site blueprint imports protocol definitions (OSPFv3, BFD, VRRP) as separate YAML files, each declaring layer dependencies. The compiler resolves the import graph, orders layers by their requires: declarations, and executes primitives in sequence.


Technical Reports


Code Samples

01_isp_dual_stack_peering.yaml

version: 1
imports: []
layers:
- name: pools
  requires: []
  primitives:
  - type: global_pool
    name: fra-transit-v4
    pool: '198.51.100.0/24'
    vrf: null
  - type: global_pool
    name: fra-transit-v6
    pool: '2001:db8:fra::/48'
    vrf: null
  - type: global_pool
    name: fra-ce-v4
    pool: '203.0.113.0/24'
    vrf: null
  - type: global_pool
    name: fra-ce-v6
    pool: '2001:db8:ce::/48'
    vrf: null
  - type: global_resource_pool
    name: fra-private-asn
    resource_type: asn
    pool: '64512-64530'
- name: peering
  requires:
  - pools
  primitives:
  - type: mesh_nodes
    selector: $border_and_transit
    mesh_type: hub_and_spoke
    hub_selector: $borders
    spoke_selector: $transits
    naming_strategy: cisco_ge
  - type: mesh_nodes
    selector: $border_and_ce
    mesh_type: hub_and_spoke
    hub_selector: $borders
    spoke_selector: $customer_edges
    naming_strategy: cisco_ge
- name: addressing
  requires:
  - peering
  primitives:
  - type: provision_ips
    selector: $transit_links
    pool: fra-transit-v4
    subnet_size: 31
    ipv6_pool: fra-transit-v6
    ipv6_subnet_size: 127
    strategy: dense
    pool_size: null
    vrf: null
  - type: provision_ips
    selector: $ce_links
    pool: fra-ce-v4
    subnet_size: 30
    ipv6_pool: fra-ce-v6
    ipv6_subnet_size: 126
    strategy: dense
    pool_size: null
    vrf: null
  - type: allocate_resources
    selector: $customer_edges
    resource_type: asn
    pool: fra-private-asn
    strategy: dense
    pool_size: null
- name: secrets
  requires:
  - addressing
  primitives:
  - type: inject_secrets
    selector: $borders
    secrets:
      bgp_md5_transit: ${BGP_MD5_TRANSIT_KEY}
      bgp_md5_customer: ${BGP_MD5_CUSTOMER_KEY}
      tacacs_key: ${TACACS_SECRET}
- name: filters
  requires:
  - secrets
  primitives:
  - type: generate_safe_bgp_filters
    selector: $borders
    prefix_list_name: BOGON-FILTER-V4
    policy_name: TRANSIT-INBOUND
    block_rfc1918: true
    permit_default: false
- name: bgp
  requires:
  - filters
  primitives:
  - type: build_protocol_layer
    selector: $all_routers
    layer: bgp
    config:
      asn_base: '65100'
      protocol_type: bgp
    clone_underlying: true
assertions:
- name: border-routers-connected
  severity: error
  select: $borders
  check:
    type: is_connected
  help: A border router is isolated — check peering layer wiring.
- name: ce-reachable-from-border
  severity: error
  select: $customer_edges
  check:
    type: reachability
    target_selector: $borders
  help: A customer-edge router cannot reach any border router.
- name: transit-peering-bipartite
  severity: error
  select: $border_and_transit
  check:
    type: is_bipartite
  help: Transit peering graph is not bipartite — unexpected link between same-role nodes.
- name: links-have-ipv4
  severity: error
  select: edges[true]
  check:
    type: field_exists
    field: src_ip
  help: An edge is missing its IPv4 source address — check provision_ips.
- name: transit-ips-in-range
  severity: warning
  select: edges[link_type='transit']
  check:
    type: field_in_cidr
    field: src_ip
    cidr: '198.51.100.0/24'
  help: A transit link IP is outside the expected 198.51.100.0/24 range.
rules: {}
groups:
  transit_links: edges[link_type=='transit']
  all_routers: $borders | $transits | $customer_edges
  customer_edges: nodes[role=='ce']
  borders: nodes[role=='border']
  border_and_ce: $borders | $customer_edges
  border_and_transit: $borders | $transits
  ce_links: edges[link_type=='ce']
  transits: nodes[role=='transit']
blast_radius:
- name: limit-teardown
  select: null
  check:
    type: max_deletions
    count: 2
transforms:
- name: junos-interface-rewrite
  when: device_os == 'junos'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: '''xe-0/0/'' + string(stanza.fields.abstract_index)'
- name: junos-transit-apply-group
  when: device_os == 'junos'
  rules:
  - match_expr: stanza.kind == 'bgp_neighbor' and stanza.fields.description == 'transit'
    apply:
      apply_groups: '''TRANSIT-DEFAULTS'''
hardware_library:
  chassis: []
  linecards: []

02_campus_compliance_audit.yaml

version: 1
imports: []
layers:
- name: provisioning
  requires: []
  primitives:
  - type: conditional
    condition: topology.node_count >= 3
    then_primitives:
    - type: custom_primitive
      name: building-loopbacks
      parameters:
        pool_cidr: '10.250.0.0/16'
      primitives:
      - type: provision_ips
        selector: $all_switches
        pool: ''
        subnet_size: 32
        ipv6_pool: null
        ipv6_subnet_size: null
        strategy: dense
        pool_size: null
        vrf: null
    else_primitives: null
- name: management
  requires:
  - provisioning
  primitives:
  - type: build_access_policy
    selector: $dist_switches
    policy_name: MGMT-ACCESS
    rules:
    - name: allow-noc-ssh
      action: permit
      source_prefix: '10.0.0.0/8'
      destination_prefix: null
      protocol: tcp
      source_port: null
      destination_port: 22
    - name: allow-noc-https
      action: permit
      source_prefix: '10.0.0.0/8'
      destination_prefix: null
      protocol: tcp
      source_port: null
      destination_port: 443
    - name: deny-all-mgmt
      action: deny
      source_prefix: null
      destination_prefix: null
      protocol: null
      source_port: null
      destination_port: null
assertions:
- name: ntp-compliance
  severity: error
  select: $all_switches
  check:
    type: use_rule
    name: ntp-configured
  help: Node is missing NTP configuration — at least 2 NTP servers required.
- name: syslog-compliance
  severity: error
  select: $all_switches
  check:
    type: use_rule
    name: syslog-configured
  help: Node has no syslog server configured.
- name: management-vlan-audit
  severity: warning
  select: $access_switches
  check:
    type: use_rule
    name: management-vlan-correct
  help: Access switch management VLAN should be 99.
- name: distribution-inventory-schema
  severity: error
  select: $dist_switches
  check:
    type: match_schema
    schema:
      properties:
        firmware_version:
          type: string
        hostname:
          minLength: 1
          type: string
        serial_number:
          pattern: ^[A-Z0-9]{11}$
          type: string
        site:
          enum:
          - eng
          - sci
          - lib
          - admin
          - med
          - arts
          type: string
      required:
      - hostname
      - serial_number
      - firmware_version
      - site
      type: object
  help: Distribution switch is missing required inventory fields or has invalid data.
- name: hostnames-unique-per-building
  severity: error
  select: $all_switches
  check:
    type: unique_per_group
    field: hostname
    group_by: site
  help: Duplicate hostname detected within the same building.
- name: ibgp-acyclic
  severity: error
  select: nodes[protocol='ibgp']
  check:
    type: is_acyclic
  help: iBGP session graph contains a [cycle](../cycle) — check route-reflector hierarchy.
- name: access-dual-homed
  severity: error
  select: $access_switches
  check:
    type: min_edges
    count: 2
  help: Access switch has fewer than 2 uplinks — single point of failure.
- name: all-nodes-have-hostname
  severity: error
  select: nodes[true]
  check:
    type: field_exists
    field: hostname
  help: A node is missing its hostname field.
rules:
  management-vlan-correct: has(node.mgmt_vlan) and node.mgmt_vlan == 99
  syslog-configured: has(node.syslog_server) and node.syslog_server != ''
  ntp-configured: has(node.ntp_servers) and size(node.ntp_servers) >= 2
groups:
  all_switches: $access_switches | $dist_switches
  access_switches: nodes[role=='access']
  dist_switches: nodes[role=='distribution']
  campus_links: edges[link_type=='campus']
blast_radius:
- name: audit-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 10.0
transforms: []
hardware_library:
  chassis: []
  linecards: []

03_wan_migration.yaml

version: 1
imports: []
layers:
- name: pools
  requires: []
  primitives:
  - type: global_resource_pool
    name: isis-net-pool
    resource_type: isis_net
    pool: '49.0001.0000.0000.0001-49.0001.0000.0000.0200'
  - type: global_resource_pool
    name: sr-sid-pool
    resource_type: segment_id
    pool: '16001-16200'
- name: migration
  requires:
  - pools
  primitives:
  - type: conditional
    condition: topology.node_count < 10
    then_primitives:
    - type: allocate_resources
      selector: $london_pe
      resource_type: isis_net
      pool: isis-net-pool
      strategy: dense
      pool_size: null
    - type: allocate_resources
      selector: $london_pe
      resource_type: segment_id
      pool: sr-sid-pool
      strategy: dense
      pool_size: null
    else_primitives:
    - type: allocate_resources
      selector: $all_pe
      resource_type: isis_net
      pool: isis-net-pool
      strategy: dense
      pool_size: null
    - type: allocate_resources
      selector: $all_pe
      resource_type: segment_id
      pool: sr-sid-pool
      strategy: dense
      pool_size: null
- name: secrets
  requires:
  - migration
  primitives:
  - type: inject_secrets
    selector: nodes[city='london']
    secrets:
      isis_auth_key: ${ISIS_KEY_LONDON}
      tacacs_key: ${TACACS_KEY_EU}
  - type: inject_secrets
    selector: nodes[city='paris']
    secrets:
      isis_auth_key: ${ISIS_KEY_PARIS}
      tacacs_key: ${TACACS_KEY_EU}
  - type: inject_secrets
    selector: nodes[city='frankfurt']
    secrets:
      isis_auth_key: ${ISIS_KEY_FRANKFURT}
      tacacs_key: ${TACACS_KEY_EU}
- name: filters
  requires:
  - secrets
  primitives:
  - type: generate_safe_bgp_filters
    selector: $border_routers
    prefix_list_name: BOGON-INBOUND
    policy_name: EBGP-INBOUND-FILTER
    block_rfc1918: true
    permit_default: false
- name: routing-policy
  requires:
  - filters
  primitives:
  - type: build_routing_policy
    selector: $all_pe
    policy_name: MIGRATION-PREFER-ISIS
    statements:
    - name: prefer-isis-routes
      action: permit
      match_prefix_list: null
      match_community_list: ISIS-MIGRATED
      match_as_path: null
      set_local_preference: 200
      set_metric: null
      set_as_path_prepend: null
      set_community: null
      next_hop: null
    - name: depref-ospf-routes
      action: permit
      match_prefix_list: null
      match_community_list: OSPF-LEGACY
      match_as_path: null
      set_local_preference: 80
      set_metric: null
      set_as_path_prepend: null
      set_community: null
      next_hop: null
    - name: default-permit
      action: permit
      match_prefix_list: null
      match_community_list: null
      match_as_path: null
      set_local_preference: null
      set_metric: null
      set_as_path_prepend: null
      set_community: null
      next_hop: null
- name: isis-overlay
  requires:
  - routing-policy
  primitives:
  - type: build_protocol_layer
    selector: $pe_and_p
    layer: isis
    config:
      level: level-2
      protocol_type: isis
    clone_underlying: true
assertions:
- name: pe-reaches-p-via-isis
  severity: error
  select: $all_pe
  check:
    type: reachability
    target_selector: $p_routers
  help: A PE router cannot reach any P router — IS-IS adjacency may be down.
- name: pe-has-isis-net
  severity: error
  select: $all_pe
  check:
    type: field_exists
    field: isis_net
  help: PE is missing its IS-IS NET address — check allocate_resources.
- name: pe-has-sr-sid
  severity: error
  select: $all_pe
  check:
    type: field_exists
    field: segment_id
  help: PE is missing its SR node SID — check allocate_resources.
- name: border-reachable
  severity: error
  select: $border_routers
  check:
    type: reachability
    target_selector: $all_pe
  help: Border router is unreachable from PE — migration may have broken transit.
rules: {}
groups:
  p_routers: nodes[role=='p']
  london_pe: nodes[city=='london' and role=='pe']
  border_routers: nodes[role=='border']
  pe_and_p: $all_pe | $p_routers
  all_pe: nodes[role=='pe']
blast_radius:
- name: migration-change-limit
  select: null
  check:
    type: max_modifications
    count: 5
transforms:
- name: iosxr-interface-rewrite
  when: device_os == 'iosxr'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: '''GigabitEthernet0/0/0/'' + string(stanza.fields.abstract_index)'
- name: iosxr-sr-annotation
  when: device_os == 'iosxr'
  rules:
  - match_expr: stanza.kind == 'isis_neighbor'
    apply:
      metric: '10'
      sr_enabled: 'true'
hardware_library:
  chassis: []
  linecards: []

04_multi_tenant_dc.yaml

version: 1
imports: []
layers:
- name: fabric
  requires: []
  primitives:
  - type: mesh_nodes
    selector: $spines_and_leaves
    mesh_type: hub_and_spoke
    hub_selector: $spines
    spoke_selector: $leaves
    naming_strategy: cisco_ge
- name: addressing
  requires:
  - fabric
  primitives:
  - type: provision_ips
    selector: $equities_links
    pool: '10.100.0.0/16'
    subnet_size: 30
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: equities
  - type: provision_ips
    selector: $fixed_income_links
    pool: '10.100.0.0/16'
    subnet_size: 30
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: fixed-income
  - type: provision_ips
    selector: $risk_links
    pool: '10.100.0.0/16'
    subnet_size: 30
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: risk
  - type: provision_ips
    selector: $fabric_links
    pool: '172.16.0.0/16'
    subnet_size: 31
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: null
- name: security
  requires:
  - addressing
  primitives:
  - type: build_access_policy
    selector: $spines
    policy_name: INTER-VRF-DENY
    rules:
    - name: deny-eq-to-fi
      action: deny
      source_prefix: '10.100.0.0/16'
      destination_prefix: '10.100.0.0/16'
      protocol: ip
      source_port: null
      destination_port: null
    - name: deny-eq-to-risk
      action: deny
      source_prefix: '10.100.0.0/16'
      destination_prefix: '10.100.0.0/16'
      protocol: ip
      source_port: null
      destination_port: null
    - name: permit-intra-vrf
      action: permit
      source_prefix: null
      destination_prefix: null
      protocol: null
      source_port: null
      destination_port: null
- name: overlay
  requires:
  - security
  primitives:
  - type: build_protocol_layer
    selector: $spines_and_leaves
    layer: bgp
    config:
      asn_base: '65200'
      protocol_type: bgp
    clone_underlying: true
assertions:
- name: every-leaf-has-tenant
  severity: error
  select: $leaves
  check:
    type: use_rule
    name: tenant-assigned
  help: Leaf switch is missing tenant assignment.
- name: every-leaf-has-vrf
  severity: error
  select: $leaves
  check:
    type: use_rule
    name: vrf-configured
  help: Leaf switch has no VRF configured.
- name: every-leaf-has-vlan
  severity: warning
  select: $leaves
  check:
    type: use_rule
    name: leaf-has-vlan
  help: Leaf switch is missing VLAN ID — check tenant provisioning.
- name: tenant-metadata-schema
  severity: error
  select: $leaves
  check:
    type: match_schema
    schema:
      properties:
        hostname:
          minLength: 1
          type: string
        tenant:
          enum:
          - equities
          - fixed-income
          - risk
          type: string
        vlan_id:
          maximum: 4094
          minimum: 1
          type: integer
        vrf_name:
          type: string
      required:
      - hostname
      - tenant
      - vrf_name
      - vlan_id
      type: object
  help: Leaf node data does not match expected tenant metadata schema.
- name: vlans-unique-per-tenant
  severity: error
  select: $leaves
  check:
    type: unique_per_group
    field: vlan_id
    group_by: tenant
  help: Duplicate VLAN ID detected within the same tenant.
- name: equities-connected
  severity: error
  select: $equities_or_spines
  check:
    type: is_connected
  help: Equities tenant subgraph is disconnected — a leaf is unreachable.
- name: fixed-income-connected
  severity: error
  select: $fixed_income_or_spines
  check:
    type: is_connected
  help: Fixed-income tenant subgraph is disconnected — a leaf is unreachable.
- name: risk-connected
  severity: error
  select: $risk_or_spines
  check:
    type: is_connected
  help: Risk tenant subgraph is disconnected — a leaf is unreachable.
rules:
  vrf-configured: has(node.vrf_name) and node.vrf_name != ''
  leaf-has-vlan: has(node.vlan_id) and node.vlan_id > 0 and node.vlan_id < 4095
  tenant-assigned: has(node.tenant) and node.tenant != ''
groups:
  risk_or_spines: nodes[tenant=='risk'] | $spines
  spines_and_leaves: $spines | $leaves
  equities_links: edges[tenant=='equities']
  risk_links: edges[tenant=='risk']
  fixed_income_links: edges[tenant=='fixed-income']
  equities_or_spines: nodes[tenant=='equities'] | $spines
  spines: nodes[role=='spine']
  fabric_links: edges[link_type=='fabric']
  fixed_income_or_spines: nodes[tenant=='fixed-income'] | $spines
  leaves: nodes[role=='leaf']
blast_radius:
- name: tenant-vrf-protection
  select: null
  check:
    type: max_deletions
    count: 3
- name: fabric-change-cap
  select: null
  check:
    type: max_percentage_changed
    value: 25.0
transforms:
- name: nxos-interface-rewrite
  when: device_os == 'nxos'
  rules:
  - match_expr: stanza.kind == 'interface'
    apply:
      name: '''Ethernet1/'' + string(stanza.fields.abstract_index + 1)'
- name: nxos-vrf-context
  when: device_os == 'nxos'
  rules:
  - match_expr: stanza.kind == 'bgp_neighbor' and has(stanza.fields.vrf)
    apply:
      nxos_vrf_context: stanza.fields.vrf
hardware_library:
  chassis: []
  linecards: []

evpn-vxlan-fabric.yaml

version: 1
imports:
- '../docs/library/datacenter-rules.yaml'
- '../docs/library/hardware-lowering.yaml'
layers:
- name: resources
  requires: []
  primitives:
  - type: map_hardware_inventory
    selector: $leaves
    chassis_model: dcs-7508
    slots:
      1: linecard-48port-10g
  - type: allocate_resources
    selector: $all_nodes
    resource_type: bgp_as
    pool: '65001-65001'
    strategy: dense
    pool_size: null
  - type: allocate_resources
    selector: $all_nodes
    resource_type: router_id
    pool: '10.255.0.1-10.255.0.255'
    strategy: dense
    pool_size: null
- name: underlay
  requires: []
  primitives:
  - type: provision_ips
    selector: edges[true]
    pool: '10.0.0.0/24'
    subnet_size: 31
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: null
  - type: build_protocol_layer
    selector: $all_nodes
    layer: ospf
    config:
      area: '0.0.0.0'
    clone_underlying: true
- name: control_plane
  requires: []
  primitives:
  - type: build_protocol_layer
    selector: $all_nodes
    layer: bgp
    config:
      peer_type: ibgp
      protocol_type: bgp
    clone_underlying: false
  - type: mesh_nodes
    selector: $bgp_nodes
    mesh_type: full
    hub_selector: null
    spoke_selector: null
    naming_strategy: null
- name: policies
  requires: []
  primitives:
  - type: build_prefix_list
    selector: $all_nodes
    prefix_list_name: PL-LOOPBACKS
    entries:
    - prefix: '10.255.0.0/24'
      action: permit
      le: 32
      ge: null
  - type: build_community_list
    selector: $all_nodes
    community_list_name: CL-EVPN
    entries:
    - community: '65001:1000'
      action: permit
  - type: build_routing_policy
    selector: $all_nodes
    policy_name: RP-UNDERLAY-EXPORT
    statements:
    - name: PERMIT-LOOPBACKS
      action: permit
      match_prefix_list: PL-LOOPBACKS
      match_community_list: null
      match_as_path: null
      set_local_preference: null
      set_metric: null
      set_as_path_prepend: null
      set_community: null
      next_hop: null
- name: overlays
  requires: []
  primitives:
  - type: build_vxlan
    selector: $leaves
    vni_base: 10000
    mcast_group_base: '239.1.1.1'
  - type: build_evpn
    selector: $leaves
    route_distinguisher_base: auto
    route_target_base: '65001:10000'
assertions:
- name: loopback-prefix-check
  severity: error
  select: $all_nodes
  check:
    type: field_in_cidr
    field: router_id
    cidr: '10.255.0.0/24'
  help: null
rules: {}
groups:
  spines: nodes[role=='spine']
  leaves: nodes[role=='leaf']
  all_nodes: nodes[true]
  bgp_nodes: nodes[layer=='bgp']
blast_radius: []
transforms: []
hardware_library:
  chassis: []
  linecards: []

evpn-vxlan-mapping.yaml

# examples/evpn-vxlan-mapping.yaml
#
# Multi-vendor DeviceIR mapping for the EVPN-VXLAN fabric.
# Demonstrates how the same topology data generates vendor-specific CLI stanzas.

rules:
  # 1. Leaf Configuration
  - selector: "nodes[role=='leaf']"
    rules:
      - stanza:
          kind: "interface"
          fields:
            name: "Loopback0" # Template handles vendor-specific mapping if needed
            address: "/32"
            description: "Router-ID / VTEP source"

      - stanza:
          kind: "bgp_neighbor"
          fields:
            local_as: ""
            peer_ip: ""
            remote_as: ""
            description: "iBGP EVPN Peer"
            export_policy: "RP-UNDERLAY-EXPORT"

  # 2. Spine Configuration
  - selector: "nodes[role=='spine']"
    rules:
      - stanza:
          kind: "interface"
          fields:
            name: "Loopback0"
            address: "/32"
            description: "Router-ID"

      - stanza:
          kind: "ospf_neighbor"
          fields:
            interface: ""
            area: "0.0.0.0"
            network_type: "point-to-point"

  # 3. Global Policy Application (Prefix-Lists)
  - selector: "nodes[true]"
    rules:
      - stanza:
          kind: "prefix_list"
          key: "PL-LOOPBACKS"
          fields:
            entries:
              - prefix: "10.255.0.0/24"
                action: "permit"
                le: 32

multi-protocol-site.yaml

version: 1
imports:
- '../protocols/ospfv3.yaml'
- '../protocols/bfd.yaml'
- '../protocols/vrrp.yaml'
layers:
- name: physical
  requires: []
  primitives:
  - type: mesh_nodes
    selector: $all_nodes
    mesh_type: full
    hub_selector: null
    spoke_selector: null
    naming_strategy: null
  - type: provision_ips
    selector: $all_links
    pool: '10.0.0.0/24'
    subnet_size: 30
    ipv6_pool: null
    ipv6_subnet_size: null
    strategy: dense
    pool_size: null
    vrf: null
assertions: []
rules: {}
groups:
  spines: nodes[role=='spine']
  all_links: edges[src>=0]
  all_nodes: nodes[true]
blast_radius: []
transforms: []
hardware_library:
  chassis: []
  linecards: []

three-site-mesh-mapping.yaml

# examples/three-site-mesh-mapping.yaml
#
# Companion mapping for three-site-mesh.yaml.
# Produces a BGP config file for site-a-r1 demonstrating end-to-end pipeline output.
#
# The mapping targets all_nodes and emits a bgp stanza grouped under "site-a-r1".
# Using kind: "bgp" ensures the rendered output contains BGP keywords.
#
# The default fallback template renders: kind= key=
# So each matched node produces a line: "kind=bgp key="
#
# Note: the `node` field is used by generate_configs to group output by device.
# It is not passed to the stanza template renderer.

rules:
  - selector: "all_nodes"
    stanza:
      kind: "bgp"
      fields:
        node: "site-a-r1"
        local_asn: "65001"
        description: "site-a full-mesh BGP session"

three-site-mesh.yaml

# examples/three-site-mesh.yaml
#
# Three-site BGP mesh — canonical reference blueprint for [ank_pydantic](../ank_pydantic) consumers.
#
# This blueprint demonstrates the complete netcfg primitive pipeline:
#
#   Stage 1 — MeshNodes: creates full-mesh point-to-point links within each site.
#              Each site's routers are fully meshed independently (intra-site only).
#              A full mesh of N nodes produces N*(N-1)/2 edges.
#
#   Stage 2 — ProvisionIps: assigns /30 subnets to each P2P link.
#              Each site has its own /24 address pool — IPs are scoped per-site.
#              ProvisionIps MUST run after MeshNodes (edges must exist first).
#              Writes src_ip, dst_ip, subnet to each endpoint node's data.
#
#   Stage 3 — BuildProtocolLayer: clones each physical node into a BGP overlay layer.
#              The BGP node inherits its parent's IP data (src_ip, dst_ip).
#              BuildProtocolLayer MUST run after ProvisionIps to inherit IP addresses.
#
# Address plan:
#   Site A: 10.1.0.0/24 — routers site-a-r1..r4
#   Site B: 10.2.0.0/24 — routers site-b-r1..r4
#   Site C: 10.3.0.0/24 — routers site-c-r1..r4
#
# Each /24 provides 64 /30 subnets — more than enough for 6 links per site.

version: 1
layers:
  # Stage 1: Create full mesh of P2P links within each site.
  # Selector "nodes[site=...]" matches nodes whose data_json contains {"site": "..."}.
  # 4 nodes per site → 4*(4-1)/2 = 6 edges per site → 18 edges total.
  - name: input
    primitives:
      - type: mesh_nodes
        selector: "nodes[site='a']"
        mesh_type: full
        

      - type: mesh_nodes
        selector: "nodes[site='b']"
        mesh_type: full
        

      - type: mesh_nodes
        selector: "nodes[site='c']"
        mesh_type: full
        

  # Stage 2: Assign /30 P2P addresses from per-site pools.
  # ProvisionIps runs AFTER MeshNodes — edges must exist before IPs can be assigned.
  # Each /30 uses .1 (src_ip) and .2 (dst_ip) host addresses.
  # "edges[src>=0]" selects all edges in the physical layer.
  - name: input
    primitives:
      - type: provision_ips
        selector: "edges[src>=0]"
        pool: "10.1.0.0/24"
        subnet_size: 30
        strategy: dense

  # Stage 3: Build BGP overlay — one BGP node per physical node.
  # BuildProtocolLayer MUST run after ProvisionIps to inherit src_ip/dst_ip.
  # The BGP node's data_json is deep-merged from its physical parent, so
  # the BGP node carries the same IP data that ProvisionIps wrote.
  - name: input
    primitives:
      - type: build_protocol_layer
        selector: "nodes[true]"
        layer: bgp
        config:
          protocol_type: bgp
          asn_base: "65000"

validate-test-blueprint.yaml

version: 1
layers:
  - name: physical
    primitives:
      - type: mesh_nodes
        selector: "nodes[hostname != '']"
        mesh_type: "full"
        edge_properties: {}
      - type: provision_ips
        selector: "edges[true]"
        pool: "10.0.0.0/16"
        strategy: "dense"


Usage

DSL Transformation Example

# DSL Transformation Rule
# Applied when device_os matches 'nxos'
transformations:
  - name: nxos_lowering
    when: "device_os == 'nxos'"
    rules:
      - match: "kind == 'interface' && name.startsWith('Ethernet')"
        apply:
          name: "name + '/1'"
          mtu: 9216  # Force jumbo frames

Architecture

flowchart LR P[Parse] --> T[Transform] T --> L[Lower] L --> R[Render] R --> E[Emit] style P fill:#e3f2fd style T fill:#e3f2fd style L fill:#e3f2fd style R fill:#e3f2fd style E fill:#e3f2fd

The pipeline runs in five phases:

  1. Parse — Read the blueprint YAML, resolve imports, validate structure.
  2. Transform — Execute primitives (mesh_nodes, provision_ips, build_protocol_layer) against the topology graph. Each primitive reads from and writes to node/edge data.
  3. Lower — Apply mapping rules that convert graph data into vendor-intermediate representation (DeviceIR). Selectors match nodes by role, site, or layer.
  4. Render — MiniJinja templates consume DeviceIR stanzas and produce vendor-specific CLI syntax. Template selection is driven by device_os metadata.
  5. Emit — Write .cfg files per device, using atomic rename for transactional output.

Vendor abstraction lives in the lowering and rendering phases. The same blueprint produces Arista EOS, Cisco NX-OS, or Juniper Junos output by swapping the mapping rules and template set. The topology graph and primitive execution are vendor-agnostic.

flowchart TD BGP["bgp layer
requires: addressing"] ADDR["addressing layer
requires: pools, topology"] POOLS["pools layer"] TOPO["topology layer"] BGP --> ADDR ADDR --> POOLS ADDR --> TOPO

Error reporting uses miette for source-snippet diagnostics: blueprint validation errors, IP pool exhaustion, and selector mismatches all produce span-highlighted terminal output pointing to the relevant YAML lines.


Quick Facts

   
Status Active
Stack Rust

What This Is

Deterministic, auditable, CI/CD-friendly Rust CLI for compiling declarative YAML network blueprints into vendor-neutral configuration artifacts. The netcfg binary orchestrates: blueprint parsing → topology transformation → DeviceIR generation → template rendering → traceable config file emission.


Current Milestone: v2.1 Protocol Library & Security Policy DSL

Goal: Deliver a standard library of importable protocol fragments covering the full simulator protocol set, LaTeX DSL formatting for technical reports, and a first-class security policy DSL with named groups, security zones, zone-based policy, NAT, and assertions.

Target features:


Current State (v1.2 Front & Back Ends — shipped)

The front and back ends of the compiler are fully functional end-to-end:

Known tech debt (v1.3):


Core Value

Single-binary network compiler: design, transform, and generate configs from YAML blueprints without Python.


Requirements


# Active (v2.1)


# Deferred (v2.0+)


# Validated (v1.0 - v1.2)


Key Decisions

Decision Rationale Outcome
Topology::from_core() wrapper bridges CoreTopology into Evaluator Evaluator tightly coupled to Python-wrapper type; wrapping at phase boundary is cleanest ✓ Good
NamedTempFile::new_in(parent) for transactional output Same-filesystem guarantee enables atomic POSIX rename ✓ Good
Clap 4 Args wrapper struct (ConfigCommand) containing Subcommand enum Matches Clap 4 nested subcommand pattern, consistent with BlueprintCommand ✓ Good
RenderEngine::render_node per-node API Clean separation between transformation and configuration generation ✓ Good
miette for diagnostic reporting Provides out-of-the-box snippet and span highlighting for YAML errors ✓ Good

Constraints

Last updated: 2026-03-06 — Milestone v2.1 started