Skip to content

Repository files navigation

junos-forge

Template-driven configuration management for Juniper switches and routers: generate standardised Junos configs from a YAML inventory, push them over NETCONF with a diff and a commit check, verify device health afterwards, and roll back automatically if anything regressed. Then audit the fleet against a compliance rule set.

Built around a three-device campus topology (one core router, two access switches) modelled on a university network. The inventory is synthetic — hostnames, addressing and credentials are invented for the demonstration, and every password hash in the repository is a placeholder. No real device data, secrets or topology are published here.

inventory/ ──► Jinja2 templates ──► candidate config
                                         │
                                    diff + commit check
                                         │
                                  commit confirmed 5
                                         │
                              post-commit health checks
                                    ┌────┴────┐
                                 pass       fail
                                    │          │
                                 confirm   rollback 1

Why the interesting part is the rollback

Anyone can render a template and paste it into a terminal. The part that matters in production is what happens when the change is wrong. Every commit here is a commit confirmed: the device is told to revert itself unless the pipeline checks in again. Between the commit and the confirmation, the device is re-interrogated over NETCONF and compared against its pre-change state:

Check Fails when
management-reachable the device stopped answering RPCs
hostname-applied the config did not actually take
no-interface-regression a port that was up before is now down
ospf-adjacencies adjacency count dropped, or a neighbour is stuck

If any check fails, the pipeline commits rollback 1 immediately rather than waiting for the timer. A locked-out switch in a wiring closet is a truck roll; this is the code that prevents one.

make demo produces this — a real diff generated from the template drift above, followed by a commit that fails its post-commit health check and gets rolled back automatically:

Terminal output of make demo: a real Junos config diff, then a commit that fails its post-commit health check and rolls back

Runs without hardware

vJunos images require a Juniper account and ~4 GB of RAM per node, so the default backend is a simulator — not canned output, but a real model of a Junos box. It parses the config you load, merges it honouring replace: semantics, computes a genuine show | compare diff, models commit-time errors (undefined VLAN references, inet and ethernet-switching on one unit), and keeps a rollback file. That is why the same test suite covers both backends, and why re-running a deploy correctly reports no-change.

Point it at real devices with --real — see lab/README.md.

Quick start

make venv
make test        # 107 tests
make demo        # full walkthrough: build, drift, remediation, rollback, audit

The demo runs six scenarios end to end:

  1. Day-0 build — a factory-fresh switch converges to the standard (462 lines of diff)
  2. Idempotency — the same run produces zero diff and commits nothing
  3. Drift — a hand edit on the box is caught by the audit engine
  4. Remediation — the next deploy removes it, showing the exact diff
  5. Safe rollback — a change that breaks health is reverted automatically
  6. Fleet audit — compliance state across all three devices

Commands

python -m junos_forge.cli validate        # inventory + template check, no device
python -m junos_forge.cli render          # write configs to artifacts/configs/
python -m junos_forge.cli diff            # candidate diff, commits nothing
python -m junos_forge.cli deploy --commit # safe commit with automatic rollback
python -m junos_forge.cli facts           # operational facts -> JSON
python -m junos_forge.cli audit           # compliance report

python -m junos_forge.cli --hosts uw-acc-01 diff     # one device
python -m junos_forge.cli --real deploy --commit     # real NETCONF

deploy without --commit is a dry run. audit exits non-zero on any critical or high finding, so it works as a CI gate.

What gets generated

Per device, from role and host variables:

  • System — hostname, DNS, NTP, syslog, archival-on-commit, hardened SSH (no telnet, no root login), and a PROTECT-RE firewall filter on lo0 that restricts the management plane to NOC prefixes and polices ICMP
  • Interfaces — access, trunk, routed and administratively-disabled ports, each with an enforced description; IRB/SVIs, loopback, PoE
  • VLANs — with l3-interface bound only on the device that owns the gateway
  • OSPF — point-to-point uplinks with BFD; user-facing SVIs forced passive on access switches so nothing on a wall jack can form an adjacency
  • Port security (access role only) — per-port MAC limits with a per-port override, DHCP snooping, dynamic ARP inspection, IP source guard, storm control, RSTP edge/BPDU block, and 802.1X with MAC-RADIUS fallback

Uplinks and trunks are explicitly exempt from port security — a MAC limit on an uplink black-holes an entire building.

Layout

inventory/          defaults.yml, groups.yml, hosts/*.yml
templates/          device.j2 and the stanza templates it includes
audit/rules.yml     compliance rules, declarative
junos_forge/
  inventory.py      variable resolution + pre-flight validation
  render.py         Jinja2 rendering + structural config checks
  junos_config.py   Junos config parser, differ and merger
  device.py         PyezSession (NETCONF) and SimulatedSession
  deploy.py         the safe-commit pipeline and health checks
  audit.py          rule engine
  facts.py          fact collection and JSON reports
  cli.py            command-line interface
lab/                containerlab topology for vJunos
tests/              107 tests

Inventory model

Variables resolve lowest to highest: defaults.yml → each group in groups: order → the host file. Dicts merge recursively; lists are replaced whole, because a half-merged interface list is never what anyone means.

# inventory/hosts/uw-acc-01.yml
hostname: uw-acc-01
groups: [campus, access]     # campus VLANs + access-layer port security
model: vjunos-switch
loopback: 10.255.1.1
ospf: {router_id: 10.255.1.1}

interfaces:
  - name: ge-0/0/0
    description: "UPLINK to uw-cor-01 ge-0/0/0"
    mode: routed
    address: 10.0.12.0/31
    ospf: {enabled: true, interface_type: p2p, metric: 100}
    exempt_port_security: true    # never MAC-limit an uplink

  - name: ge-0/0/4
    description: "IOT - ODE-2-HVAC-CTRL"
    mode: access
    vlan: iot
    dot1x: false                  # building controls do not speak 802.1X
    mac_limit: 1

Bad inventory fails before anything connects: undefined VLAN references, duplicate or undescribed interfaces, routed ports without addresses, unknown groups and invalid modes are all rejected at load time.

Auditing

Rules are declarative; rule types are Python handlers, so adding a check is usually four lines of YAML.

- id: SEC-RE-FILTER
  title: Loopback carries the PROTECT-RE input filter
  severity: critical
  type: config_present
  params:
    path: interfaces lo0 unit 0 family inet filter
    statement: input PROTECT-RE

- id: L2-PORT-SECURITY
  title: Access ports enforce a MAC limit
  severity: high
  applies_to: [access]        # skipped on the core
  type: access_ports_mac_limited

Fourteen rules ship, covering Junos version floor, telnet, root login, the RE filter, SNMP write communities, NTP, syslog, interface descriptions, spare ports left live, MAC limits, ARP inspection, and OSPF adjacency state.

Every rule is tested twice: once against the compliant generated config, and once against a config deliberately drifted to violate it. A rule set that only ever passes proves nothing.

Reports

artifacts/ collects machine-readable output for dashboards or CI:

artifacts/configs/uw-acc-01.conf        generated configuration
artifacts/facts/uw-acc-01.facts.json    per-device operational facts
artifacts/facts-report.json             fleet roll-up
artifacts/deploy-report.json            outcome, diff and checks per device
artifacts/audit-report.json             every finding
{
  "hostname": "uw-acc-01",
  "junos_version": "23.4R1.9",
  "up_time": "288 days, 15 hours",
  "summary": {
    "interfaces_total": 7, "interfaces_up": 6, "interfaces_dark": 0,
    "ospf_neighbors_full": 1, "undescribed_interfaces": []
  }
}

Testing

make test

107 tests covering config parsing and diffing, inventory resolution and validation, per-role rendering, the deploy pipeline (including rollback on a failed health check, commit-check abort, and unreachable devices), and every audit rule in both directions.

Two real bugs the suite was written to pin down, both still covered by regression tests:

  • {#- ... -#} with trim_blocks enabled silently produced }replace: on one line, gluing a load tag onto config. check_braces() now rejects it.
  • Junos container keys are multi-word (unit 0, family inet), so a config path cannot be split on spaces. Node.get() matches greedily with backtracking.

CI

.github/workflows/ci.yml runs the tests on 3.10 and 3.12, validates the inventory, renders every config, shows the resulting device-level diff in the job log so a template change can be reviewed before approval, and runs the audit nightly as a drift gate.

About

Template-driven Junos config management: render from a YAML inventory, deploy over NETCONF with commit-confirmed and automatic rollback on failed health checks, then audit the fleet for compliance drift. Includes a Junos simulator, so no hardware required.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages