A full walkthrough of how to build, test, and ship network config management and automation pipelines to production. Real code, real workflow, real team collaboration patterns.
Config management is the practice of treating your network device configurations the same way software engineers treat code. Every configuration is version-controlled, reviewed, tested, and deployed through a repeatable automated pipeline rather than typed by hand into a terminal. The goal is to eliminate configuration drift, reduce human error, and make every change auditable and reversible.
A single authoritative system (NetBox, Nautobot, or YAML files in Git) that defines what every device should look like. No config exists outside this system.
Jinja2 templates define config structure. Variables from the source of truth are injected to render device-specific configs programmatically.
Python tooling using Netmiko, NAPALM, or Ansible pushes rendered configs to devices, validates the result, and rolls back on failure.
This is the full pipeline from intent to production. Every stage has a clear input, output, and owner. Nothing touches a device without passing through every stage first.
Python pulls device inventory, IP addressing, VLAN assignments, BGP peer data, and interface roles from NetBox via its REST API. This becomes the data model for config rendering.
Templates define the config structure for each device role (spine, leaf, PE router, border router). Variables from NetBox are injected to produce device-specific configs.
Batfish performs offline network modelling against the rendered configs before they touch any device. It checks for routing loops, reachability violations, and policy mismatches.
Netmiko pushes configs in a controlled rollout. Post-deploy verification checks BGP sessions, OSPF adjacencies, and interface states before marking the change as successful.
These are the actual patterns used in production automation. Not pseudocode. Real structure you can adapt to your environment.
import pynetbox import os nb = pynetbox.api( "https://netbox.company.internal", token=os.environ["NETBOX_TOKEN"] ) def get_bgp_peers(device_name): """Pull all BGP peers for a device from NetBox.""" device = nb.dcim.devices.get(name=device_name) peers = nb.plugins.bgp.bgp_session.filter( device_id=device.id ) return [{ "neighbor_ip": p.remote_address.address.split("/")[0], "remote_asn": p.remote_as.asn, "description": p.description, "local_ip": p.local_address.address.split("/")[0], "route_map_in": p.cf_route_map_in or "RM-DEFAULT-IN", "route_map_out": p.cf_route_map_out or "RM-DEFAULT-OUT" } for p in peers] def build_device_context(device_name): """Build the full data context for template rendering.""" device = nb.dcim.devices.get(name=device_name) return { "hostname": device.name, "local_asn": device.cf_bgp_asn, "router_id": device.primary_ip.address.split("/")[0], "bgp_peers": get_bgp_peers(device_name), "vlans": [v.vid for v in nb.ipam.vlans.filter(site_id=device.site.id)], "role": device.device_role.slug }
! Generated by NetworkForAI automation pipeline ! Device: {{ hostname }} | ASN: {{ local_asn }} ! DO NOT EDIT MANUALLY router bgp {{ local_asn }} bgp router-id {{ router_id }} bgp log-neighbor-changes no bgp default ipv4-unicast {% for peer in bgp_peers %} ! Peer: {{ peer.description }} neighbor {{ peer.neighbor_ip }} remote-as {{ peer.remote_asn }} neighbor {{ peer.neighbor_ip }} description {{ peer.description }} neighbor {{ peer.neighbor_ip }} update-source Loopback0 neighbor {{ peer.neighbor_ip }} password {{ peer.md5_password | default("changeme") }} neighbor {{ peer.neighbor_ip }} timers 10 30 address-family ipv4 unicast neighbor {{ peer.neighbor_ip }} activate neighbor {{ peer.neighbor_ip }} soft-reconfiguration inbound neighbor {{ peer.neighbor_ip }} route-map {{ peer.route_map_in }} in neighbor {{ peer.neighbor_ip }} route-map {{ peer.route_map_out }} out neighbor {{ peer.neighbor_ip }} maximum-prefix 10000 80 exit-address-family {% endfor %}
from jinja2 import Environment, FileSystemLoader from netmiko import ConnectHandler from netbox_client import build_device_context import logging log = logging.getLogger("deploy") def render_config(device_name, template_name): env = Environment(loader=FileSystemLoader("templates/")) template = env.get_template(template_name) context = build_device_context(device_name) return template.render(context) def deploy_to_device(device_name, mgmt_ip, config, dry_run=True): """Push config to device. dry_run=True just prints the diff.""" config_lines = config.strip().splitlines() if dry_run: log.info(f"DRY RUN for {device_name}: config ready, not pushing") return config device = { "device_type": "cisco_ios", "host": mgmt_ip, "username": os.environ["NET_USER"], "password": os.environ["NET_PASS"], "secret": os.environ["NET_ENABLE"] } with ConnectHandler(**device) as conn: conn.enable() output = conn.send_config_set(config_lines) log.info(f"Config pushed to {device_name}") return output def verify_bgp(conn, expected_peer_count): """Post-deploy check: verify BGP sessions are established.""" output = conn.send_command("show bgp summary | include Established") established = output.count("Established") if established < expected_peer_count: raise ValueError( f"Expected {expected_peer_count} BGP sessions, got {established}" ) log.info(f"BGP verification passed: {established} sessions up")
! EVPN/VXLAN Leaf Config | {{ hostname }} vlan 10,20,30 interface nve1 no shutdown source-interface loopback0 host-reachability protocol bgp {% for vni in vxlan_vnis %} member vni {{ vni.id }} ingress-replication protocol bgp {% endfor %} router bgp {{ local_asn }} address-family l2vpn evpn {% for peer in bgp_peers if peer.type == "spine" %} neighbor {{ peer.neighbor_ip }} activate neighbor {{ peer.neighbor_ip }} send-community extended {% endfor %} {% for vni in vxlan_vnis %} vni {{ vni.id }} l2 rd auto route-target import auto route-target export auto {% endfor %}
Every config change goes through the same workflow as a software code change. This creates accountability, reversibility, and a complete history of every decision ever made on the network.
network-configs/
templates/
bgp_neighbors.j2
evpn_leaf.j2
ospf_area.j2
prefix_lists.j2
vlan_config.j2
inventory/
hosts.yaml
group_vars/
spine.yaml
leaf.yaml
pe_routers.yaml
scripts/
deploy.py
verify.py
rollback.py
diff.py
tests/
test_bgp_template.py
test_evpn_template.py
.github/
workflows/
validate.yaml
deploy.yaml
README.mdname: Validate network configs on: pull_request: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Render templates run: python scripts/render_all.py - name: Run Batfish analysis run: python tests/batfish_check.py - name: Template unit tests run: pytest tests/ -v - name: Post diff as PR comment run: python scripts/diff.py --comment
Streaming telemetry replaces SNMP polling for modern network observability. Devices stream operational data in real time using gNMI, which gets collected, processed, and stored for dashboards and alerting.
from pygnmi.client import gNMIclient import json, time TARGET_PATHS = [ "openconfig-bgp:bgp/neighbors", "openconfig-interfaces:interfaces/interface/state/counters", "openconfig-network-instance:network-instances" ] def stream_telemetry(host, port=57400): with gNMIclient( target=(host, port), username=os.environ["NET_USER"], password=os.environ["NET_PASS"], insecure=True ) as gc: for update in gc.subscribe_sync( subscribe={ "subscription": [{"path": p, "mode": "sample", "sample_interval": 10_000_000_000} for p in TARGET_PATHS], "mode": "stream" } ): process_update(update) def process_update(update): """Parse update and write to InfluxDB.""" for notification in update.get("update", []): path = notification["path"] val = notification["val"] write_to_influx(path, val)
Automation only works if the whole team trusts it and follows the same process. These are the collaboration patterns that make production automation sustainable.
| Role | Responsibility in the pipeline | Tools used |
|---|---|---|
| Network engineer | Defines intent in NetBox, opens PRs for config changes, reviews diffs before approving | NetBox, Git, Python |
| Automation engineer | Maintains templates and scripts, owns the CI/CD pipeline, handles tooling bugs | Jinja2, GitHub Actions, Batfish |
| Network architect | Reviews topology changes, approves new template patterns, signs off on major rollouts | Batfish reports, topology diagrams |
| NOC team | Monitors post-deploy telemetry, raises issues if verification fails, handles rollbacks | Grafana, PagerDuty, runbooks |
Fill in the parameters below and get a production-ready BGP neighbor config for Cisco IOS. This demonstrates the same templating logic used in the full pipeline above.
! Fill in the form and click Generate config
This page will grow as new tooling gets built and tested. The goal is to make every section interactive with real working tools, not just documentation.
Paste a running config and get a best-practice audit report. Flags missing authentication, unsafe defaults, and policy gaps.
Paste a template and a JSON variable file, get the rendered output instantly. Useful for testing templates before committing.
Paste a config diff and get an automatically generated rollback config to undo the change cleanly and safely.