The Ops Atlas xOps School

Cloud & Infrastructure Overlapping synonym Sources verified

NetworkOps

NetworkOps is an informal variant of NetOps (network operations). This Atlas uses it for network automation, also called NetDevOps: engineering network change through a source of truth, generated configuration, verification before deployment, staged rollout and automatic rollback.

Key takeaways

  • 'NetworkOps' is a rare, informal variant of NetOps, not a discipline. This Atlas uses it for network automation (NetDevOps), the engineering of change, and NetOps for running the network: an editorial split.
  • Automate the whole change path, not just the typing: source of truth, generated configuration, verification, staged rollout, state checks.
  • The source of truth holds intended state that people have vetted; NetBox's own documentation strongly discourages importing live state into it.
  • Verify before deploying with a model (Batfish) or an emulator (containerlab), then stage the rollout so representative devices are reached early.
  • Every deployment needs an exit: a commit-confirmed timer, a management path the change cannot break, and one person who owns reverts.
  • Automation multiplies mistakes as well as work: check the rendered diff against declared target and must-not-change lists, and measure change fail rate, not scripts written.

NetworkOps, as this Atlas uses the word, is the engineering of network change: describing the network you want as data, generating device configuration from it, proving the change safe before it reaches a router, rolling it out in small waves with an automatic way back, and checking that the network then behaves as intended. Its established names are network automation and NetDevOps.

In one line: NetworkOps turns “log in and type” into “change the data, let the pipeline prove it, deploy with a timer”.

Where to start. Beginners: the mental model and Lab 1. Engineers: the playbooks and the reference implementation. Leaders: the decision framework and the judgement call. Last verified 2026-10-18; tool versions are dated “as of October 2026”.

Aspect Summary
Goal Network changes that are verified before they ship, repeatable, small and reversible
People Network engineers learning automation, network automation engineers, architects, change managers
Prerequisites Routing and switching basics (NetOps), Git, YAML/JSON, some Python
In scope Source of truth, YANG/OpenConfig models, NETCONF, RESTCONF, gNMI, templates, Ansible, Nornir, Batfish, emulation, CI/CD, staged rollout, confirmed commits, drift, intent
Out of scope NOC monitoring and fault isolation (NetOps); cloud VPC design (CloudOps); pod networking (KubernetesOps); automation governance in general (AutomationOps)
Outputs An owned source of truth, generated configs in Git, a verification suite, a pipeline with rollback, a drift report
Term status Overlapping synonym of network automation / NetDevOps (note above)

Three public reports show outages that started with a change:

  • Changes are where failures cluster. Of over 100 high-impact failure events in Google’s network, nearly 70 happened while a management operation was in progress on the affected element, though it was not always the cause[6].
  • A harmless-looking diff can be fatal. At Cloudflare in 2022, “a re-ordering of terms caused us to withdraw a critical subset of prefixes” despite a dry run, a stepped rollout and peer review; 19 data centres were offline for up to 75 minutes[7].
  • Process checks fail quietly. At Rogers in 2022, staff removed a policy filter while tidying configuration; core routers were flooded with routes and crashed. The audit missed it, and a risk algorithm rated the step “Low”, so no lab test was required. More than 12 million customers lost service, restored gradually over about 26 hours[8].

Box-by-box CLI work makes this worse: devices drift apart, review means reading CLI text, and nobody can say what the network should look like. In 2002 operators already asked to configure “the network as a whole rather than individual devices”, to use diff and version control on configurations, and to separate distributing a configuration from activating it[9].

What it does not solve. Automation spreads a wrong change faster. Google found partial automation “increased availability in general” but could also “cause large failures”[6]; safety comes from verification, staging and rollback.

What NetworkOps is not: boundaries and naming

Link to section “What NetworkOps is not: boundaries and naming”
  • Not the NOC. Monitoring, fault isolation and carrier escalation belong to NetOps. NetworkOps configures the telemetry NetOps consumes and owns the rollback of a failed change.
  • Not “we wrote some scripts”. Scripts that push commands without a source of truth, verification or rollback are automation debt. Governance of automation in general is on AutomationOps.
  • Not a product. Controllers and “intent-based” platforms implement parts of the practice; intent terms “are often used loosely and inconsistently”[10]. Buying one does not give you owned data or a verification habit.
  • Not GitOps by definition. Network pipelines usually push from a runner; pull-based reconciliation (GitOps) is possible but is not what makes a change safe.
  • Not cloud network design (CloudOps), though the method applies there too.

The label has no traceable origin. The practice has a clear technical history:

  • 2002–2003: requirements. The IAB workshop asked for configuration separate from state, network-wide transactions and diff-able text[9].
  • 2006–2018: protocols and models. NETCONF (RFC 4741 in 2006, RFC 6241 in 2011) brought candidate datastores and confirmed commits[11]; YANG (2010; 1.1 in 2016)[12], RESTCONF (2017)[13] and the NMDA datastores (2018)[14] followed. An IRTF document defined SDN as “the capacity to initialize, control, change, and manage network behavior dynamically via open interfaces” (2015)[15].
  • 2015–2017: practitioners’ tools. NetBox was written at DigitalOcean in 2015 and open-sourced in 2016[16]; Cisco released pyATS publicly in late 2017[4]; Batfish came from Microsoft Research, UCLA and USC[17].
  • 2016 onwards: streaming and provisioning. Operator-led OpenConfig published the first gNMI specification in 2016 (current v0.10.0, 2023)[18][19] and a commit-confirmed extension in 2023[20]; the IETF added YANG-Push[21] and Secure Zero Touch Provisioning[22] in 2019.
  • 2022–2026: intent and community. RFC 9315 on intent-based networking (2022)[10]; the Network Automation Forum’s AutoCon events from 2023. NAF admits automation has been discussed “for a long time: 15 years or longer”, yet “it’s just not taking off like we thought it would”[3].

Mental model: three views of every device. The source of truth says what the network should be, the intended configuration is generated from it, and the operational state is what the device does. NetworkOps lives in the gaps between them.

Figure 1. Three views of a device and where NetworkOps checks the gaps
compared withSource of truth: sites,devices, IPs, peerings,policy data, vetted bypeopleIntended configuration:generated per device,stored in GitGate 1, beforedeployment: valid andsafe? Lint, model checks,emulationRunning and appliedconfiguration on thedeviceOperational state:sessions, routes,countersGate 2, after deployment:does behaviour matchintent? State checks,telemetryGate 3, continuously:does running still equalintended? Drift checkOn mismatch: fix thedata or the device, thenre-render
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 170, "nodeSpacing": 14, "rankSpacing": 24, "padding": 8}}}%%
flowchart TB
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  SOT["Source of truth: sites, devices, IPs, peerings, policy data, vetted by people"]:::core
  INT["Intended configuration: generated per device, stored in Git"]:::core
  G1["Gate 1, before deployment: valid and safe? Lint, model checks, emulation"]:::decision
  RUN["Running and applied configuration on the device"]:::neutral
  OPS["Operational state: sessions, routes, counters"]:::neutral
  G2["Gate 2, after deployment: does behaviour match intent? State checks, telemetry"]:::decision
  G3["Gate 3, continuously: does running still equal intended? Drift check"]:::decision
  FIX["On mismatch: fix the data or the device, then re-render"]:::neutral
  SOT --> INT --> G1 --> RUN
  RUN --> OPS --> G2
  RUN --> G3 --> FIX
  INT -->|"compared with"| G3

Text equivalent: The source of truth (sites, devices, IP addresses, peerings, policy data, vetted by people) generates an intended configuration per device, stored in Git. Gate 1, before deployment, checks it is valid and safe (lint, model checks, emulation). The device then has a running configuration and an operational state (sessions, routes, counters); gate 2 checks that behaviour matches intent. Gate 3 continuously compares the running configuration with the intended one; on a mismatch, the data or the device is fixed and the configuration re-rendered.

Term Meaning
Source of truth (SoT) The authoritative record of intended network data; NetBox “intends to represent the desired state of a network versus its operational state”[16]
Intended configuration “the configuration that the system attempts to apply” (RFC 8342)[14]; here, the generated file in Git
Applied configuration, operational state What is in use; operational state adds system state such as counters[14]
Drift Running configuration that no longer matches intended; RFC 9315’s intent drift is behaviour ceasing to meet intent[10]
YANG model A schema for configuration, state, RPCs and notifications[12]: vendor-native, IETF or OpenConfig[19]
NETCONF, RESTCONF, gNMI Model-driven management protocols over SSH, HTTP and gRPC[11][13][18]
Candidate datastore A working copy that “can be manipulated without impacting the device’s current configuration”[11]
Confirmed commit A commit that reverts unless confirmed in time; NETCONF’s default is 600 s[11], Junos’s 10 minutes[23]
Merge, replace Add lines to the running configuration, or make the device equal the candidate[24]
Wave, blast radius One step of a staged rollout; the devices and services a change can affect
AAA Authentication, authorisation, accounting (TACACS+, RADIUS): who may log in, what they may run, what they did
IPAM, DCIM IP address management; data-centre infrastructure management (racks, devices, cables)
SD-WAN Software-defined WAN: branch routers run as one overlay by a vendor controller
TextFSM A parser that turns CLI output into structured data with templates[25]
MCP (Cloudflare) “Multi-Colo PoP”, a Cloudflare data-centre design[7]; not the Model Context Protocol
Intent “operational goals … and outcomes … defined in a declarative manner without specifying how to achieve or implement them”[10]

NetworkOps sits on the change path between design and the running network. These boundaries are editorial distinctions.

Figure 2. NetworkOps and its neighbours
guardrailsIaC methodscloud constructsverified changestelemetry subscriptionssignalschange requests, faultsmajor incidentsAutomationOps:governance for allautomationInfraOps: IaC lifecycleCloudOps: VPCs, hubs,private accessNetworkOps: engineernetwork change (data,generation, verification,rollback)NetOps: run the network(NOC, faults, capacity)ObservabilityOps:telemetry platformIncidentOps: incidentcommand
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 170, "nodeSpacing": 16, "rankSpacing": 30, "padding": 8}}}%%
flowchart TB
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  AUTO["AutomationOps: governance for all automation"]:::neutral
  INFRA["InfraOps: IaC lifecycle"]:::neutral
  CLOUD["CloudOps: VPCs, hubs, private access"]:::neutral
  NWO["NetworkOps: engineer network change (data, generation, verification, rollback)"]:::core
  NETOPS["NetOps: run the network (NOC, faults, capacity)"]:::neutral
  OBS["ObservabilityOps: telemetry platform"]:::neutral
  INC["IncidentOps: incident command"]:::neutral
  AUTO -->|"guardrails"| NWO
  INFRA -->|"IaC methods"| NWO
  CLOUD -->|"cloud constructs"| NWO
  NWO -->|"verified changes"| NETOPS
  NWO -->|"telemetry subscriptions"| OBS
  OBS -->|"signals"| NETOPS
  NETOPS -->|"change requests, faults"| NWO
  NETOPS -->|"major incidents"| INC

Text equivalent: AutomationOps (guardrails for all automation), InfraOps (infrastructure-as-code methods) and CloudOps (cloud network constructs) feed NetworkOps, which engineers network change. NetworkOps hands verified changes to NetOps, which runs the network, and configures the telemetry subscriptions ObservabilityOps collects. Signals flow to NetOps, which sends change requests and faults back to NetworkOps and escalates major incidents to IncidentOps.

Neighbour Choose that page for…
NetOps Monitoring, fault isolation or capacity on the same network
AutomationOps Governing automation across domains (ownership, kill switches, audit)
InfraOps The infrastructure-as-code lifecycle for servers and cloud
GitOps Pull-based reconciliation from Git
CloudOps VPCs, transit hubs and private endpoints
ObservabilityOps Pipelines that store and query telemetry
EdgeOps Fleets of edge nodes rather than the network
KubernetesOps CNI plugins or pod addressing

This charter is this Atlas’s proposal; no body publishes an official one.

Mission. Make network change boring: described as data, verified before it ships, rolled out in small waves, reversible without a site visit, and confirmed by evidence.

  1. Change the data, not the device. The unit of change is one pull request: the data change (or its source-of-truth branch), templates, rendered configurations and a change record.
  2. People vet the source of truth. NetBox says “automated import of live network state is strongly discouraged”[16]. Discovered state feeds a reconciliation report, never the SoT, or drift gets written into the record.
  3. Generate; never hand-edit. Every line on a device comes from a template or model; a hand edit is drift.
  4. Verify with two kinds of evidence. A model checks the whole network but only the syntax it parses; an emulator runs real software but only at lab scale. Know each one’s blind spots.
  5. Take the smallest safe step. Put a representative device in an early wave. Cloudflare’s stagger “did not include an MCP data center until the final step”[7].
  6. Every change has an exit. A confirmed-commit timer, a management path the change cannot break, one owner for reverts. Meta and Rogers both lost that path in their outages[26][8]; designing it is NetOps work.
  7. Name the blast radius twice. Declare the devices to change and those to leave alone, separately from the data that generates the change, and refuse to run if what would change disagrees. Google adopted this after a wildcard drained “hundreds of devices when the intention was to drain two”[6].
  8. Prove the outcome. Done means sessions, routes and reachability match expectations, not that a command returned success.

Non-goals. Removing human approval from high-risk changes; replacing the NOC; automating every change. One-off work may be cheaper by hand if its result goes back into the source of truth.

Seven blocks, connected in the reference architecture; tool options are in the tooling landscape.

Block Job Watch-out
Source of truth Intended data: sites, devices, interfaces, IPs, VLANs, circuits, policies Ownership and data quality decide success, not the tool
Generation Data → configuration text or model payloads (Jinja2, NetBox config templates[27]) Partial templates force merge mode
Device interface SSH CLI, NETCONF, RESTCONF or gNMI CLI output needs parsing (TextFSM, ntc-templates[25])
Orchestration Run tasks across many devices Parallelism amplifies mistakes
Verification Prove a change safe before deployment Unsupported syntax; lab-scale gaps
Pipeline Review, test, approve, deploy Runner trust and credential scope
Assurance Behaviour and drift checks after deployment (streaming telemetry[21], NAPALM validate[28]) Alert fatigue if every difference pages someone

Usually a capability inside the network team, not a separate team. A typical split (R responsible, A accountable, C consulted, I informed):

Activity Network engineer Automation engineer SoT data steward Change manager NOC (NetOps) Security
Edit SoT data for a change R C A I I C (policy objects)
Change templates or models C R/A I I I C
Maintain verification suite C R/A I I C C
Approve a normal change R (author) C I A C C (security-relevant)
Run deployment I A (pipeline) I I R (watches) I
Post-change checks R C I I R I
Decide rollback C C I I A (incident lead) I
Reconcile drift R C A I C C

Two rules matter most. One accountable steward per data domain (IPAM, circuits, BGP policy): an unowned source of truth decays. The incident lead owns reverts: at Cloudflare, engineers “walked over each other’s changes, reverting the previous reverts”[7].

  • Day 0 – design. Choose the data model and an owner per field, the generation method, the interface per platform and the verification suite. Import the current network once, review it by hand, and mark it as the baseline.
  • Day 1 – provision. New devices boot, fetch an image and an initial configuration; SZTP standardises a secure way to do this from “a factory-default state”[22]. Vendor implementations differ.
  • Day 2 – change and assure. Most of the work: everyday changes, drift detection, OS upgrades (rings and maintenance groups are on PatchOps) and decommissioning, which is also a SoT change.
Change class Example Path Approval
Standard (pre-approved) Add a VLAN to an access port, add a BGP peer from an approved template Pipeline with full verification Automatic when every check passes and the blast radius is under the limit
Normal New routing policy, firewall rule set, fabric expansion Pipeline, peer review, lab or model evidence attached Change manager; high-risk changes go to a change board
Emergency Mitigate an attack or a carrier failure Break-glass access, logged Retrospective review; the source of truth is updated within one working day

Governance rules this Atlas recommends:

  • Score risk per change, not per programme. Rogers’ algorithm rated a step “Low” because earlier phases had gone well, so no lab test was required[8]. Earlier success is not evidence about a different change.
  • Cap changes per maintenance window. Several changes in one window made it “difficult to decide which network change ticket to roll back” at Rogers[8].
  • Review the evidence, not the CLI. Reviewers read the verification report (sessions, reachability, filter decisions) beside the diff, because diffs hide meaning.

Review cadences are in metrics. The loop mirrors the four phases of NIST’s security-focused configuration management: planning, implementing configurations, controlling change, monitoring[29].

The reference design has two trust zones. CI runs without device credentials. Inside the management network, only an approval-gated deploy runner can write to devices; a separate drift job only reads.

Figure 3. Reference architecture for network change
Zone 2: management networkZone 1: no device credentialsrendered from the branchapproved mergeread running configSource of truth: data editedon a branchGit: templates, generatedconfigs, change recordCI: guard, lint, modelchecks, emulationDeploy runner: restrictedrunner group, environmentsecrets after approval;commits with a timer, runspost-checks, confirmsDrift job: read-only accountNetwork devices (candidate,confirmed commit)Telemetry collector (gNMI,YANG-Push)Out-of-band access thatdoes not depend on thedata network
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 190, "nodeSpacing": 18, "rankSpacing": 30, "padding": 8, "subGraphTitleMargin": {"top": 4, "bottom": 14}}}}%%
flowchart TB
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  subgraph Z1["Zone 1: no device credentials"]
    direction TB
    SOT["Source of truth: data edited on a branch"]:::core
    GIT["Git: templates, generated configs, change record"]:::core
    CI["CI: guard, lint, model checks, emulation"]:::core
  end
  subgraph Z2["Zone 2: management network"]
    direction TB
    DEP["Deploy runner: restricted runner group, environment secrets after approval; commits with a timer, runs post-checks, confirms"]:::risk
    DRIFT["Drift job: read-only account"]:::neutral
    DEV["Network devices (candidate, confirmed commit)"]:::neutral
    TEL["Telemetry collector (gNMI, YANG-Push)"]:::neutral
  end
  OOB["Out-of-band access that does not depend on the data network"]:::risk
  SOT -->|"rendered from the branch"| GIT
  GIT --> CI
  CI -->|"approved merge"| DEP
  DEP --> DEV
  DRIFT -->|"read running config"| DEV
  DEV --> TEL
  OOB -.-> DEV

Text equivalent: Zone 1 holds no device credentials: source-of-truth data edited on a branch is rendered into Git (templates, generated configurations, change record), and CI runs the guard, linting, model checks and emulation. After an approved merge, Zone 2, the management network, takes over: a deploy runner in a restricted runner group, given environment secrets only after approval, commits to the devices with a timer, runs the post-checks and confirms. A drift job reads running configurations with a read-only account. Devices stream telemetry to a collector. An out-of-band path reaches the devices independently of the data network.

Three design choices carry most of the safety:

  • Generated configurations are committed to Git: reviewers see the real diff, and every deployment has a last-known-good commit.
  • Only the deploy runner holds write credentials, and post-checks run there before it confirms. GitHub warns that self-hosted runners “can be persistently compromised by untrusted code”[30]; the pipeline names the settings that keep other workflows off it.
  • The out-of-band path is independent. Xona Partners’ assessment for the CRTC calls for “secure alternative access to crucial remote network elements that is not dependent on the data network”[8]; NetOps covers its design.

Alternatives (vendor controller, YAML-only source of truth, pull-based reconciliation): see deep comparisons.

In NetBox, an edit is live intent at once: other consumers read it before anyone approves it, reviewers see generated configs without the data change or its author, and a scheduled export bundles several people’s edits into one diff. Give data the path code takes:

  1. Stage the edit. NetBox Labs’ Branching plugin “allows you to make copies of NetBox’s data model and alter them independently”; API requests pick a branch with the X-NetBox-Branch header[31]. With a YAML source of truth in Git, the data change is part of the pull request.
  2. Render from the branch, synced with the main data first so the render does not undo other merged changes. Commit the generated configs to the change’s pull request with a change record naming target and must-not-change devices (the guard checks both).
  3. Merge the data after the last wave is confirmed. A failed change leaves the branch unmerged, so the source of truth still describes the network; a merged branch can also be reverted until it is archived or deleted[31].

Edits made outside a branch show up as drift between the source of truth and Git (playbook 5).

End-to-end workflow: one change from request to evidence

Link to section “End-to-end workflow: one change from request to evidence”

Two properties matter: a failed check before deployment touches no device, and a failed check after deployment needs no human action to revert, because the deployer does not confirm. The incident lead alone decides on further reverts.

Figure 4. Change workflow with happy and exception paths
failpassfailpassyesnoRequest: ticket or pull requestEdit SoT data or templates ona branchCI gate: render, lint, modelchecks, emulationFail: fix data or template; nodevice touchedReview diff and verificationreport; approve by changeclassDeploy next wave with aconfirmed-commit timerPost-change checksFail: do not confirm, devicereverts; halt waves; pageincident leadPass: confirm commitMore waves?Attach evidence; merge theSoT branch; close
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 210, "nodeSpacing": 18, "rankSpacing": 24, "padding": 8}}}%%
flowchart TB
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  A["Request: ticket or pull request"]:::core
  B["Edit SoT data or templates on a branch"]:::core
  C["CI gate: render, lint, model checks, emulation"]:::decision
  X["Fail: fix data or template; no device touched"]:::risk
  D["Review diff and verification report; approve by change class"]:::core
  E["Deploy next wave with a confirmed-commit timer"]:::core
  F["Post-change checks"]:::decision
  Y["Fail: do not confirm, device reverts; halt waves; page incident lead"]:::risk
  G["Pass: confirm commit"]:::core
  H["More waves?"]:::decision
  I["Attach evidence; merge the SoT branch; close"]:::core
  A --> B --> C
  C -->|"fail"| X
  C -->|"pass"| D --> E --> F
  F -->|"fail"| Y
  F -->|"pass"| G --> H
  H -->|"yes"| E
  H -->|"no"| I

Text equivalent: A request (ticket or pull request) leads to edits of source-of-truth data or templates on a branch. CI renders, lints, and runs model checks and emulation; any failure stops the change before a device is touched. Otherwise a reviewer reads the diff and verification report, and approval follows the change class. Each wave is deployed with a confirmed-commit timer. If its post-change checks pass, the commit is confirmed and the next wave follows; after the last wave, evidence is attached, the source-of-truth branch merged and the change closed. If checks fail, the commit is not confirmed, so the device reverts; remaining waves halt and the incident lead is paged.

Playbook Trigger Steps Done when Way back
1 Baseline the SoT Programme start, or a new site Import once; stewards review each field; render; diff against running; fix data or device until the diff is empty Rendered equals running on every device in scope Pre-import backup of every config
2 Pre-change verification Every pull request Render; lint; parse check (Batfish initIssues); invariants (sessions up, required flows permitted, forbidden flows denied); differential questions No new init issues; invariants hold; differences explained in the PR None needed: nothing deployed
3 Staged rollout Approved change Canary of 1–2 representative devices, then a site, then a region; confirm each wave only after its checks pass All waves confirmed; evidence attached Unconfirmed wave reverts itself; the incident lead rolls back earlier waves
4 Confirmed commit Every deployment, where supported Commit with a timer (5–10 minutes); post-checks over a path the change cannot break; confirm Confirmed inside the window Timer expiry restores the previous configuration[11][23]
5 Drift reconciliation Scheduled diff finds running ≠ intended Classify: emergency fix not yet in SoT, unauthorised change, or template gap; update SoT or redeploy Diff empty, or exception recorded with an expiry date Redeploy intended config via the pipeline
6 Blast-radius guard Every change Declare targets and must-not-change devices in the change record; compare with the devices whose rendered configs changed; refuse on any mismatch; cap devices per wave Guard passed, or a named approver overrode it Abort before the first wave

Confirmed commit on each platform. NETCONF’s <persist> lets a different session confirm, which suits automation that reconnects[11]. gNMI has a commit-confirmed extension (v0.1.0)[20] that Nokia SR Linux supports[32]. NAPALM’s commit_config(revert_in=…)[24] is listed for EOS, Junos and IOS, not the IOS XR or NX-OS drivers[33]. Where it is missing, use smaller waves and a tested rollback.

Merge or replace. Merge adds your lines to whatever is on the device; replace makes the device equal your file. In Ansible, cli_config with replace replaces the whole running configuration[34]; resource-module states (merged, replaced, overridden) make the same choice per resource, such as interfaces or BGP, and overridden could “remove your access to the device”[35]. See the judgement call.

Only the IETF RFCs below are standards. OpenConfig is an operator-led open-source project[19], and RFC 9315 is an informational IRTF document.

Interface or model Status Strengths Limits
CLI over SSH Vendor-specific Works everywhere; what engineers know Unstructured output; atomicity varies by platform[33]; ordering-sensitive
NETCONF + YANG IETF standard (RFC 6241, RFC 7950) Candidate datastore, validate, confirmed commit, locks[11] XML verbosity; per-platform model coverage varies
RESTCONF IETF standard (RFC 8040) HTTP tooling; a subset of NETCONF[13] No explicit locks; each edit is committed at once
gNMI + OpenConfig/native models OpenConfig specification One transaction per SetRequest; streaming in ON_CHANGE, SAMPLE or TARGET_DEFINED mode[18] Model and extension support vary by platform
YANG-Push IETF standard (RFC 8641) Subscriptions to datastore changes[21] Check platform and collector support
Intent-based networking IRTF concepts (RFC 9315) Vocabulary: fulfilment, assurance, intent drift[10] Not a protocol; products interpret it freely

Models change the review question from “are these commands right?” to “is this data right, and did verification prove its effect?”, which is cheaper to answer and easier to automate.

Choosing and reading models. OpenConfig models are vendor-neutral[19]; native ones can cover more. Nokia calls its SR Linux models “a more complete representation” of the platform, including features “that the OpenConfig data models do not describe”[36]. This Atlas suggests OpenConfig where every platform in a domain implements the module, native models elsewhere; the gNMI Capabilities reply lists the models a device supports[36]. Read a path as a walk down the tree: in Lab 3’s /interface[name=ethernet-1/1]/oper-state, interface is a list, [name=…] selects one entry by its key, and oper-state is the leaf read[37].

A NETCONF change, step by step. The safety features are explicit operations: lock the candidate, edit it, validate it, commit with a timer, and confirm later from any session that presents the persist ID[11].

Example status: Python syntax-checked; not executed (needs ncclient 0.7 and a server with :candidate, :validate and :confirmed-commit:1.1)[38].

"""NETCONF change: lock, edit the candidate, validate, confirmed commit, confirm."""
import os

from ncclient import manager

CONFIG = """<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
    <interface><name>eth1</name><description>to core1</description></interface>
  </interfaces>
</config>"""
DEVICE = {"host": "router1.example.net", "port": 830, "hostkey_verify": True,
          "username": os.environ["NET_USER"], "password": os.environ["NET_PASSWORD"]}

with manager.connect(**DEVICE) as m, m.locked("candidate"):
    m.discard_changes()                      # start from a clean candidate
    m.edit_config(target="candidate", config=CONFIG)
    m.validate(source="candidate")           # the server checks the whole candidate
    m.commit(confirmed=True, timeout="300", persist="chg-0142")

# ... run post-checks over a path the change cannot break, then confirm:
with manager.connect(**DEVICE) as m:
    m.commit(persist_id="chg-0142")         # without this, the device reverts

Controller-managed domains. SD-WAN and many data-centre fabrics run through a vendor controller that renders and pushes device configuration itself: the SDN pattern, in which a controller “manipulates the controlled entity via an interface”[15]. The controller then owns rendering, transactions and rollback; the source of truth still owns the inputs (sites, prefixes, policy) and feeds the controller’s API through the same reviewed pipeline; verification checks behaviour, not device text. RFC 9315 splits intent-based systems into fulfilment and assurance[10]: keep some assurance independent of the controller that fulfils.

Choose by job, then by platform. Versions as of October 2026[39].

Job Representative open-source options Strengths Limits
Source of truth NetBox[16], Nautobot[40] IPAM/DCIM models, APIs, config rendering Data entry and ownership effort
Task runner Ansible (ansible.netcommon)[34], Nornir[41] Ansible: declarative states, wide coverage. Nornir: plain, testable Python Ansible: logic in YAML gets awkward. Nornir: more code to write
Driver layer NAPALM[24], Netmiko[42] Merge, replace, diff, commit confirm; any SSH CLI Feature gaps per platform[33]
Model-driven clients gnmic[43], ncclient[38] Transactions, streaming Requires model knowledge
Model-based verification Batfish[17] Whole-network what-if analysis without devices Unsupported vendors or syntax; SR Linux is not on its list[44]
Emulation containerlab[45] Real network operating systems in containers Lab scale; some images need vendor accounts
Testing pyATS[4], NAPALM validate[28] State assertions before and after change Test upkeep

Lock-in and upkeep. Open-source tools move fast. One Nornir inventory plugin for NetBox last released in September 2021, before NetBox’s v2 API tokens[39][46]. Pin versions, test upgrades in CI, and keep the SoT schema independent of any one runner.

The evidence on this page is about rule-based automation. AI assistants can draft templates, explain diffs or summarise a verification report; treat their output as an untrusted pull request that passes the same gates, and never give a model write access to devices or the SoT outside the pipeline. Automation “adds an additional layer of complexity”[6]; AI adds another. Event analysis is on AIOps.

Beginners can start with the labs and return here later.

Assumptions. About 60 Arista EOS and Juniper Junos devices; NetBox 4.7[47] with the Branching plugin; GitHub Enterprise Cloud (see the plan note below); Nornir with NAPALM; Batfish in CI. Replace runs only on platforms whose playbook-1 baseline diff is empty; the rest merge. Syntax-checked, not executed except the guard: a shape, not a product.

network-repo/
  sot-export.sh            # NetBox branch -> snapshot/configs/*.cfg, run for each change
  change.yaml              # change record: targets and must_not_change hosts
  snapshot/configs/        # generated configs, committed so reviewers see the diff
  guard.py                 # blast-radius guard: rendered diff vs change.yaml
  checks/check_change.py   # Batfish invariants and differential questions (Lab 2)
  validate/<host>.yaml     # NAPALM post-change expectations per host
  inventory/hosts.yaml     # Nornir hosts: platform, wave, full_template_coverage
  requirements-*.txt       # pinned versions with hashes (pip-compile --generate-hashes)
  deploy.py                # one wave: load with a timer, check, confirm or let it revert
  .github/workflows/network-change.yml

Export from the source of truth. NetBox renders each device’s configuration from its template and context data, here inside the change’s branch (untested; inferred from the rule that the branch “must be specified with each REST API request”[31]). A token without “write enabled” can only make GET requests, but render-config is a POST[46][27]: give the export job a write-enabled v2 token whose user holds only device view and the render_config action, restricted to the job’s IP.

Example status: bash -n syntax-checked; not executed (requires NetBox 4.5 or later for v2 tokens, the Branching plugin, curl and jq).

#!/usr/bin/env bash
# Render every active device's configuration from a NetBox branch into snapshot/configs/.
set -euo pipefail
: "${NETBOX_URL:?}" "${NETBOX_TOKEN:?}" "${NETBOX_BRANCH:?}"  # branch = its schema ID
H=(-H "Authorization: Bearer ${NETBOX_TOKEN}" -H "X-NetBox-Branch: ${NETBOX_BRANCH}")
mkdir -p snapshot/configs
curl -sf "${H[@]}" \
  "${NETBOX_URL}/api/dcim/devices/?status=active&brief=true&limit=1000" \
  | jq -r '.results[] | "\(.id) \(.name)"' \
  | while read -r id name; do
      curl -sf -X POST "${H[@]}" \
        -H "Content-Type: application/json" -H "Accept: text/plain" \
        --data '{}' "${NETBOX_URL}/api/dcim/devices/${id}/render-config/" \
        > "snapshot/configs/${name}.cfg"
    done
git status --short snapshot/configs

The change record and the guard. Each pull request carries a change.yaml from its author (example status: YAML parsed):

change_id: NW-2026-0142
targets: [spine1, leaf1]               # hosts this change is meant to touch
must_not_change: [leaf2, border1]      # hosts that must keep their config

guard.py adapts Google’s rule of requiring both a positive and a negative list[6] to what will actually change: the hosts whose rendered configurations differ (derived from the data) must equal the author’s targets, and none may be in must_not_change. A template edit that touches every device fails the pull request.

Example status: executed on 2026-10-18 in a scratch Git repository (Python 3.8.10; PyYAML 5.3.1, again with 6.0.2): GUARD ok and exit 0 when the diff matched the record; exit 1 and one GUARD line per problem when it did not.

"""Blast-radius guard: the rendered diff must match the change record's two lists."""
import pathlib
import subprocess
import sys

import yaml


def changed_hosts(base, head):
    """Hosts whose generated configuration differs between two commits."""
    out = subprocess.run(
        ["git", "diff", "--name-only", base, head, "--", "snapshot/configs"],
        check=True, capture_output=True, text=True).stdout
    return {pathlib.Path(p).stem for p in out.splitlines()}


def check(base, head, record="change.yaml"):
    """Return (problems, changed hosts); no problems means the guard passed."""
    rec = yaml.safe_load(pathlib.Path(record).read_text())
    targets, keep = set(rec["targets"]), set(rec["must_not_change"])
    changed = changed_hosts(base, head)
    problems = []
    if changed & keep:
        problems.append(f"must-not-change hosts would change: {sorted(changed & keep)}")
    if changed != targets:
        problems.append(f"diff changes {sorted(changed)}; "
                        f"record declares {sorted(targets)}")
    return problems, changed


if __name__ == "__main__":
    found, _ = check(sys.argv[1], sys.argv[2])
    print("\n".join(f"GUARD {p}" for p in found) or "GUARD ok")
    sys.exit(1 if found else 0)

The pipeline. Pull requests are verified on a GitHub-hosted Linux runner without device credentials; the Batfish service container is reached on localhost[48]. Only a push to main deploys, from a self-hosted runner in the management network, and four settings make that boundary real:

  • Environment secrets. NET_USER and NET_PASSWORD belong to the production environment, not the repository; environment secrets are “only available to workflow jobs that reference the environment”, after approval[49].
  • Protection rules. Required reviewers, “prevent self-review”, a main-only deployment branch rule, administrator bypass disallowed (allowed by default)[49].
  • A restricted runner group. Limit it to this repository and to network-change.yml at main (“Selected workflows”)[50]; otherwise any workflow on any branch can run code on a runner that reaches every device. Prefer just-in-time runners, which “perform at most one job”[30].
  • The plan. On Free, Pro and Team, required reviewers and the bypass setting work only for public repositories[49], and workflow-restricted runner groups are documented for Enterprise Cloud[50]. A private repository needs GitHub Enterprise, or a deployer outside Actions with its own approval check.

Example status: YAML parsed; not executed. Action SHAs are those of the v7.0.1 and v7.0.0 tags (as of October 2026).

name: network-change
on:
  pull_request:
    paths: ["snapshot/**", "checks/**", "validate/**", "change.yaml"]
  push:
    branches: [main]
    paths: ["snapshot/**", "change.yaml"]
permissions:
  contents: read
jobs:
  verify:
    runs-on: ubuntu-latest
    services:
      batfish:
        image: batfish/allinone
        ports: ["9996:9996"]
    env:
      BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - run: pip install --require-hashes -r requirements-ci.txt
      - run: python guard.py "$BASE" HEAD
      - run: git worktree add reference "$BASE"
      - run: >
          python checks/check_change.py
          --reference reference/snapshot --candidate snapshot
  deploy-wave-1:
    if: github.event_name == 'push'
    needs: verify
    runs-on:
      group: network-deploy
    environment: production
    concurrency: network-production
    env:
      BASE: ${{ github.event.before }}
      CHANGE_ID: ${{ github.sha }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
      - run: python3 -m venv .venv
      - run: .venv/bin/pip install --require-hashes -r requirements-deploy.txt
      - run: .venv/bin/python deploy.py 1
        env:
          NET_USER: ${{ secrets.NET_USER }}
          NET_PASSWORD: ${{ secrets.NET_PASSWORD }}

Later waves copy deploy-wave-1 and need the previous wave; concurrency prevents parallel deployments. Actions are pinned to commit SHAs, as GitHub recommends[30], and packages install only with matching hashes[51]. deploy.py repeats the guard and touches only changed hosts in its wave. It loads each configuration with a revert timer, polls the post-checks until a deadline shorter than the timer (new BGP sessions take time), and confirms only if they pass; otherwise the timer restores the previous configuration[52][24].

Example status: Python syntax-checked; not executed (requires devices whose NAPALM driver supports commit confirm).

"""Deploy one wave: guard, load with a revert timer, check until a deadline, confirm."""
import os
import sys
import time

from nornir import InitNornir
from nornir_napalm.plugins.tasks import (
    napalm_configure,
    napalm_confirm_commit,
    napalm_validate,
)

from guard import check

CHECK_SECONDS = 180                   # sessions can take minutes to come up
POLL_SECONDS = 15
REVERT_SECONDS = CHECK_SECONDS + 120  # the timer always outlasts the checks


def deploy(task):
    with open(f"snapshot/configs/{task.host.name}.cfg") as fh:
        config = fh.read()
    # Replace only where templates own the whole config (playbook 1 diff is empty).
    task.run(task=napalm_configure, configuration=config,
             replace=task.host.get("full_template_coverage", False),
             revert_in=REVERT_SECONDS, commit_message=os.environ["CHANGE_ID"])
    deadline = time.monotonic() + CHECK_SECONDS
    while True:
        report = task.run(task=napalm_validate,
                          src=f"validate/{task.host.name}.yaml")
        if report.result.get("complies"):
            break
        if time.monotonic() > deadline:
            raise RuntimeError("checks failed: not confirming, device will revert")
        time.sleep(POLL_SECONDS)
    task.run(task=napalm_confirm_commit)


def main(wave):
    problems, changed = check(os.environ["BASE"], "HEAD")
    if problems:
        sys.exit("guard: " + "; ".join(problems))
    nr = InitNornir(config_file="nornir.yaml")
    hosts = nr.inventory.hosts
    lost = sorted(h for h in changed if h not in hosts or hosts[h].get("wave") is None)
    if lost:
        sys.exit(f"guard: changed hosts without inventory entry or wave: {lost}")
    nr.inventory.defaults.username = os.environ["NET_USER"]
    nr.inventory.defaults.password = os.environ["NET_PASSWORD"]
    targets = nr.filter(
        filter_func=lambda h: h.name in changed and str(h.get("wave")) == wave)
    names = sorted(targets.inventory.hosts)
    if not names:
        print(f"wave {wave}: no changed hosts")
        return
    result = targets.run(task=deploy)
    if result.failed_hosts:
        sys.exit(f"wave {wave} failed on {sorted(result.failed_hosts)}; halting")
    print(f"wave {wave} confirmed on {names}")


if __name__ == "__main__":
    main(sys.argv[1])

The validation file states what must be true afterwards, in NAPALM’s getter format[28] (example status: YAML parsed; not executed):

- get_bgp_neighbors:
    global:
      peers:
        10.0.1.0:
          is_up: true
- get_interfaces:
    Ethernet1:
      is_up: true

Lab 1 was executed. Labs 2 and 3 were not (they need Docker, not run on the author’s shared production host); their commands are syntax-checked against the cited documentation.

Lab 1 (beginner): from source of truth to checked configuration

Link to section “Lab 1 (beginner): from source of truth to checked configuration”

Objective. Generate configurations from data, reject bad data, and block a change that touches more devices than intended. Prerequisites. Python 3.11 or later (3.10 and older are past end of life[53]), pip install jinja2 pyyaml. Cost and risk. None; no device is touched.

Example status: executed on 2026-10-18 with Python 3.8.10, under Jinja2 2.10.1 with PyYAML 5.3.1 and again under Jinja2 3.1.6 with PyYAML 6.0.2 (identical outputs and files); not run on Python 3.11 or later, which the author’s host lacks.

sot.yaml, the source of truth for a three-node fabric:

devices:
  spine1:
    asn: 65100
    loopback: 10.255.0.1/32
    links:
      - {interface: Ethernet1, ip: 10.0.1.0/31, peer: leaf1}
      - {interface: Ethernet2, ip: 10.0.1.2/31, peer: leaf2}
  leaf1:
    asn: 65101
    loopback: 10.255.0.11/32
    links:
      - {interface: Ethernet1, ip: 10.0.1.1/31, peer: spine1}
  leaf2:
    asn: 65102
    loopback: 10.255.0.12/32
    links:
      - {interface: Ethernet1, ip: 10.0.1.3/31, peer: spine1}

device.j2, an EOS-style template (Jinja2):

hostname {{ name }}
!
interface Loopback0
   ip address {{ dev.loopback }}
{% for l in dev.links %}
!
interface {{ l.interface }}
   description to {{ l.peer }}
   no switchport
   ip address {{ l.ip }}
{% endfor %}
!
router bgp {{ dev.asn }}
   router-id {{ dev.loopback.split('/')[0] }}
{% for n in neighbors %}
   neighbor {{ n.ip }} remote-as {{ n.asn }}
   neighbor {{ n.ip }} description {{ n.name }}
{% endfor %}
   network {{ dev.loopback }}

render.py checks the data (private ASNs, unique IPs, matching /31 ends), derives BGP neighbours, writes only changed files, and refuses to write if more devices would change than --max-changed allows:

"""Render device configs from a YAML source of truth, after pre-flight checks."""
import argparse
import ipaddress
import pathlib
import sys

import jinja2
import yaml


def check(sot):
    """Return (errors, neighbours) for every device in the source of truth."""
    errors, seen_ips, nbrs = [], {}, {}
    devs = sot["devices"]
    for name, dev in devs.items():
        nbrs[name] = []
        if not 64512 <= dev["asn"] <= 65534:
            errors.append(f"{name}: ASN {dev['asn']} is not a private 2-byte ASN")
        for link in dev["links"]:
            ip = ipaddress.ip_interface(link["ip"])
            if str(ip.ip) in seen_ips:
                errors.append(f"{name}: {ip.ip} already used on {seen_ips[str(ip.ip)]}")
            seen_ips[str(ip.ip)] = name
            peer = devs.get(link["peer"])
            if peer is None:
                errors.append(f"{name}: peer {link['peer']} "
                              "is not in the source of truth")
                continue
            back = [p for p in peer["links"] if p["peer"] == name]
            if len(back) != 1:
                errors.append(f"{name}: {link['peer']} has no single link back")
                continue
            pip = ipaddress.ip_interface(back[0]["ip"])
            if pip.network != ip.network or pip.ip == ip.ip:
                errors.append(f"{name}: {ip} and {link['peer']} {pip} are not one /31")
                continue
            nbrs[name].append({"ip": str(pip.ip), "asn": peer["asn"],
                               "name": link["peer"]})
    return errors, nbrs


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--sot", default="sot.yaml")
    ap.add_argument("--out", default="out")
    ap.add_argument("--max-changed", type=int, default=None,
                    help="refuse to write if more devices would change than this")
    args = ap.parse_args()
    sot = yaml.safe_load(pathlib.Path(args.sot).read_text())
    errors, nbrs = check(sot)
    if errors:
        print("\n".join(f"ERROR {e}" for e in errors))
        return 1
    env = jinja2.Environment(loader=jinja2.FileSystemLoader("."),
                             undefined=jinja2.StrictUndefined,
                             trim_blocks=True, lstrip_blocks=True,
                             keep_trailing_newline=True)
    tpl = env.get_template("device.j2")
    out = pathlib.Path(args.out)
    rendered = {n: tpl.render(name=n, dev=d, neighbors=nbrs[n])
                for n, d in sot["devices"].items()}
    changed = [n for n, txt in rendered.items()
               if not (out / f"{n}.cfg").exists()
               or (out / f"{n}.cfg").read_text() != txt]
    if args.max_changed is not None and len(changed) > args.max_changed:
        print(f"BLOCKED {len(changed)} devices would change "
              f"(limit {args.max_changed}): {', '.join(changed)}")
        return 2
    out.mkdir(exist_ok=True)
    for n in changed:
        (out / f"{n}.cfg").write_text(rendered[n])
    sessions = sum(len(v) for v in nbrs.values()) // 2
    print(f"OK {len(rendered)} devices, {sessions} BGP sessions, "
          f"{len(changed)} changed: {', '.join(changed) or 'none'}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Steps. (1) python3 render.py. (2) Run it again. (3) Change the spine’s asn to 65200 in a copy (sot-asn.yaml) and run python3 render.py --sot sot-asn.yaml --max-changed 1. (4) In another copy (sot-bad.yaml), give leaf2’s link the address 10.0.1.1/31 and run python3 render.py --sot sot-bad.yaml.

Expected output (from the run):

OK 3 devices, 2 BGP sessions, 3 changed: spine1, leaf1, leaf2
OK 3 devices, 2 BGP sessions, 0 changed: none
BLOCKED 3 devices would change (limit 1): spine1, leaf1, leaf2
ERROR spine1: 10.0.1.2/31 and leaf2 10.0.1.1/31 are not one /31
ERROR leaf2: 10.0.1.1 already used on leaf1
ERROR leaf2: 10.0.1.1/31 and spine1 10.0.1.2/31 are not one /31

Validation. out/leaf1.cfg contains neighbor 10.0.1.0 remote-as 65100. Step 3 shows why the guard matters: one spine ASN touches every peer. Exit codes 0, 0, 2 and 1 let CI gate on the result. Troubleshooting. UndefinedError: the template uses a field the data lacks (StrictUndefined makes that fail loudly). Teardown. Delete the folder.

Lab 2 (intermediate): catch a bad change with Batfish

Link to section “Lab 2 (intermediate): catch a bad change with Batfish”

Objective. Catch two faults no syntax check sees: a BGP neighbour typo and an access-list reordering (an ordering error, like the policy-term reorder in Cloudflare’s outage). Prerequisites. Docker; Python 3.11 or later (current pybatfish needs 3.10 or later[39]); python3 -m pip install --upgrade pybatfish; Batfish started with docker run --name batfish -v batfish-data:/data -p 8888:8888 -p 9996:9996 batfish/allinone[44]. Cost and risk. Local only. The lab uses Cisco IOS syntax, which Batfish supports[44].

Create snapshots/reference/configs/edge1.cfg:

hostname edge1
!
interface GigabitEthernet0/0
 ip address 10.0.12.1 255.255.255.252
 ip access-group MGMT-IN in
!
interface Loopback0
 ip address 192.0.2.1 255.255.255.255
!
ip access-list extended MGMT-IN
 permit tcp 198.51.100.0 0.0.0.255 host 192.0.2.1 eq 22
 deny   tcp any host 192.0.2.1 eq 22
 permit ip any any
!
router bgp 65001
 bgp router-id 192.0.2.1
 neighbor 10.0.12.2 remote-as 65002
 address-family ipv4
  network 192.0.2.1 mask 255.255.255.255
  neighbor 10.0.12.2 activate
 exit-address-family
!
end

and snapshots/reference/configs/core1.cfg:

hostname core1
!
interface GigabitEthernet0/0
 ip address 10.0.12.2 255.255.255.252
!
interface Loopback0
 ip address 192.0.2.2 255.255.255.255
!
router bgp 65002
 bgp router-id 192.0.2.2
 neighbor 10.0.12.1 remote-as 65001
 address-family ipv4
  network 192.0.2.2 mask 255.255.255.255
  neighbor 10.0.12.1 activate
 exit-address-family
!
end

Copy reference to candidate. In the candidate edge1.cfg, swap the first two MGMT-IN lines, and change neighbor 10.0.12.2 remote-as 65002 to remote-as 65020. Then run checks/check_change.py:

Example status: Python syntax-checked; not executed. Question names, parameters and columns are from the pybatfish documentation[44].

"""Pre-change checks with Batfish: candidate snapshot against the reference."""
import argparse
import sys

from pybatfish.client.session import Session
from pybatfish.datamodel.flow import HeaderConstraints

ap = argparse.ArgumentParser()
ap.add_argument("--reference", default="snapshots/reference")
ap.add_argument("--candidate", default="snapshots/candidate")
ap.add_argument("--host", default="localhost")
args = ap.parse_args()

bf = Session(host=args.host)
bf.set_network("networkops-lab")
bf.init_snapshot(args.reference, name="reference", overwrite=True)
bf.init_snapshot(args.candidate, name="candidate", overwrite=True)
failures = []

ref_issues = len(bf.q.initIssues().answer(snapshot="reference").frame())
new_issues = len(bf.q.initIssues().answer(snapshot="candidate").frame())
if new_issues > ref_issues:
    failures.append(f"init issues rose from {ref_issues} to {new_issues}")

sessions = bf.q.bgpSessionStatus().answer(snapshot="candidate").frame()
for _, row in sessions[sessions["Established_Status"] != "ESTABLISHED"].iterrows():
    failures.append(f"BGP {row['Node']} -> {row['Remote_IP']}: "
                    f"{row['Established_Status']}")

ssh = HeaderConstraints(srcIps="198.51.100.0/24", dstIps="192.0.2.1",
                        applications=["ssh"])
denied = bf.q.searchFilters(headers=ssh, action="deny", filters="MGMT-IN")
denied = denied.answer(snapshot="candidate").frame()
if len(denied):
    failures.append(f"MGMT-IN denies admin SSH, e.g. {denied.iloc[0]['Flow']}")

changed = bf.q.compareFilters().answer(snapshot="candidate",
                                       reference_snapshot="reference").frame()
print(f"{len(changed)} filter line pair(s) now treat some flows differently")
for f in failures:
    print("FAIL", f)
print("PASS" if not failures else f"{len(failures)} check(s) failed")
sys.exit(1 if failures else 0)

Expected output (from the documentation, not observed). Run with --candidate snapshots/reference, the script prints PASS. Against the candidate it should report NOT_COMPATIBLE sessions for edge1 and core1 (one side misconfigured), an MGMT-IN denies admin SSH failure with an example flow, at least one changed filter line pair, and exit code 1. Validation. Undo one fault at a time; only the other should remain. Troubleshooting. Connection refused on 9996: the container is not running (docker logs batfish). Rows in initIssues mean Batfish did not fully understand a line; read them before trusting other answers. Teardown. docker rm -f batfish && docker volume rm batfish-data.

Lab 3 (advanced): model-driven change with commit confirmed

Link to section “Lab 3 (advanced): model-driven change with commit confirmed”

Objective. Use gNMI against two emulated Nokia SR Linux routers: read state, stream telemetry, and make a change that reverts unless confirmed. Prerequisites. A Linux VM with Docker and sudo; containerlab (curl -sL https://containerlab.dev/setup | sudo -E bash -s "all")[45]; gnmic (bash -c "$(curl -sL https://get-gnmic.openconfig.net)")[43]. The SR Linux image is public; default credentials admin / NokiaSrl1![45]. Cost and risk. A lab VM only, never a production host; read installer scripts before running them.

Example status: YAML parsed and bash -n syntax-checked; not executed. Commit-confirmed support per the SR Linux 25.10 guide[32].

nwops.clab.yml:

name: nwops
topology:
  nodes:
    srl1:
      kind: nokia_srlinux
      image: ghcr.io/nokia/srlinux:25.10.5
    srl2:
      kind: nokia_srlinux
      image: ghcr.io/nokia/srlinux:25.10.5
  links:
    - endpoints: ["srl1:ethernet-1/1", "srl2:ethernet-1/1"]
#!/usr/bin/env bash
set -euo pipefail
sudo containerlab deploy -t nwops.clab.yml
G=(gnmic -a clab-nwops-srl1 --skip-verify -u admin -p 'NokiaSrl1!' -e json_ietf)
IF='/interface[name=ethernet-1/1]'

# 1. Read: host name and link state
"${G[@]}" get --path /system/name/host-name
"${G[@]}" get --path "${IF}/oper-state"

# 2. Change with a 120-second rollback timer, then read it back
"${G[@]}" set --update-path "${IF}/description" --update-value "to srl2" \
  --commit-id lab3a --commit-request --rollback-duration 120s
"${G[@]}" get --path "${IF}/description"

# 3. Do nothing for over two minutes, then read again: the change should be gone
sleep 150
"${G[@]}" get --path "${IF}/description"

# 4. Repeat; confirm only if the read-back passes (production: check behaviour too)
"${G[@]}" set --update-path "${IF}/description" --update-value "to srl2" \
  --commit-id lab3b --commit-request --rollback-duration 120s
"${G[@]}" get --path "${IF}/description" --values-only | grep 'to srl2'
"${G[@]}" set --commit-id lab3b --commit-confirm

In a second terminal, stream state changes on srl1. After step 4, disable the interface and enable it again:

gnmic -a clab-nwops-srl1 --skip-verify -u admin -p 'NokiaSrl1!' -e json_ietf \
  subscribe --path '/interface[name=ethernet-1/1]/oper-state' --stream-mode on-change
#!/usr/bin/env bash
set -euo pipefail
G=(gnmic -a clab-nwops-srl1 --skip-verify -u admin -p 'NokiaSrl1!' -e json_ietf)
P='/interface[name=ethernet-1/1]/admin-state'
"${G[@]}" set --update-path "$P" --update-value disable   # the stream should show down
sleep 10
"${G[@]}" set --update-path "$P" --update-value enable    # and then up again

Expected output. Step 1 returns the node’s host name and up (containerlab enables interfaces named in links[45]). Step 2 shows the description and step 3 none; in step 4 the read-back finds it, so the script confirms and it persists (a miss would stop the script before the confirm). The subscription prints the current value first, then one update per change, as gNMI requires for ON_CHANGE[18]: down after the disable, up after the enable (SR Linux’s admin-state takes enable and disable[32]). Validation. During a running timer, send a plain set: the target must refuse it with FAILED_PRECONDITION[20], so nobody can stack changes onto an unconfirmed one. A commit cannot be amended once issued, so send the whole change in one request[20]. Troubleshooting. TLS errors: --skip-verify is for labs only. unknown host: use the address from containerlab inspect. Teardown. sudo containerlab destroy -t nwops.clab.yml --cleanup removes the containers and the lab directory[45].

A. Edge routing-policy change (real case: Cloudflare, June 2022). Context: standardising BGP communities on site-local prefixes, with a dry run, a stepped rollout and peer review; yet the steps “weren’t small enough”[7]. NetworkOps adds: a differential check of advertised routes, not just a text diff; a first wave that includes an MCP site; a commit-confirmed timer, which Cloudflare listed as a follow-up. Success: no unexpected withdrawals in the differential report. Pitfall: trusting reviewers to spot reordering in a diff.

B. Core upgrade under a risk score (real case: Rogers, July 2022). Context: phase six of seven; a filter removed during clean-up, and core routers that normally received about 10,000 routes got “over 900,000” from one distribution router[8]. NetworkOps adds: an invariant (“every distribution-to-core session has an inbound filter and a prefix limit”); lab or model tests required by change type, whatever the track record. Success: the invariant fails the pull request. Pitfall: treating a risk score as evidence (governance).

C. Branch rollout from the source of truth (illustrative scenario). Context: 120 new branch sites in six months, each with two switches and an SD-WAN edge. Method: stewards create the site in the SoT, prefixes are allocated, configs rendered and verified, devices boot with zero-touch provisioning, waves follow opening dates. Trade-off: weeks of template work, repaid only if sites are alike. Success: over 95 % first-time-right after ten sites. Pitfall: local hand-made exceptions, which become drift.

D. Firewall and ACL requests as standard changes (illustrative scenario). Context: 40 rule requests a week, each reviewed by hand. Method: an intake form records source, destination, port and owner as SoT policy objects; CI checks “must allow” and “must deny” flows with searchFilters and reports changed lines with compareFilters[44]; additions matching an approved pattern are auto-approved. Success: lead time falls from days to hours, fail rate flat. Pitfall: rules without an owner or expiry.

Measure outcomes of change, not activity. The first two adapt DORA’s definitions to network changes[54]; the rest are this Atlas’s proposals.

KPI Formula Data source Pitfall
Change fail rate Changes needing rollback, hotfix or an incident within 24 h ÷ changes deployed Change records, pipeline logs Counting only pipeline changes hides manual ones
Failed change recovery time Median time from failed check or alert to service restored Incident timeline Confirmed-commit reverts are fast; count them too
Pre-deployment catch rate Defects stopped by verification ÷ (stopped + found after deployment) PR checks, post-incident reviews False-alarm blocks inflate it; track overrides
Pipeline coverage Changes through the pipeline ÷ all changes, including emergency and manual AAA accounting vs change records 100 % is not the goal; unrecorded changes are the problem
Drift rate and age Devices where running ≠ intended ÷ devices checked; median days drifted Scheduled diff A noisy diff (timestamps, counters) gets ignored
SoT completeness Devices with every required field valid ÷ devices in the SoT SoT validation report Completeness without accuracy; sample-audit against devices
Standard-change lead time Median request → verified in production Ticket and pipeline timestamps Speed without a fail-rate guard

Worked calculation (illustrative). One quarter: 420 changes, 380 through the pipeline and 40 by hand (12 of them emergencies). Nine needed rollback: two pipeline changes and seven manual ones.

  • Pipeline coverage = 380 ÷ 420 = 90.5 %.
  • Change fail rate = 9 ÷ 420 = 2.1 % overall, but 0.5 % (2 ÷ 380) through the pipeline and 17.5 % (7 ÷ 40) by hand. The split tells you where to invest; the blended figure hides it.
  • Verification blocked 31 pull requests with real defects, and 2 defects escaped: catch rate = 31 ÷ 33 = 93.9 %.
  • 14 of 600 devices drifted: drift rate = 2.3 %. Review the oldest first.

Review cadence. Weekly: fail rate and escapes. Monthly: drift and SoT completeness. Quarterly: coverage, lead time and the maturity self-assessment. “Number of playbooks” is a vanity metric unless paired with fail rate.

  • Credentials. Automation has its own AAA accounts, separate for read (telemetry, drift) and write, so accounting logs show who changed what. Rotate them.
  • Runner trust. Self-hosted runners are “not run in an isolated container, even if they use environments”[49], so restrict the deploy runner as the pipeline describes.
  • Source-of-truth integrity. Stewards write; tokens are v2, tied to users with minimal object permissions, IP-restricted and write-disabled wherever GET suffices[46]. Keep NetBox patched.
  • Management plane. SSH for NETCONF (mandatory to implement; TLS is an option)[11][55], TLS for gNMI[18], HTTPS for RESTCONF[13]. Verify host keys and certificates; --skip-verify is for labs only.
  • Audit evidence. Each change record links the pull request, verification report, approval, deployment log and post-checks, matching NIST SP 800-128’s change-control and monitoring phases[29]. Sector rules differ; ask your compliance team.
  • Cost drivers. People’s time dominates: data modelling, whole-config templates and check upkeep. Tooling is mostly open source; expect CI minutes, a SoT server (NetBox needs PostgreSQL 15+[16]), lab hosts, and vendor accounts for some images (Arista cEOS requires registration[45]).
  • Unit economics. Track engineer-hours per change by class. Automation pays where a class is frequent and alike (VLANs, peers, ACL entries), rarely for one-off redesigns.
  • Control-plane capacity as an invariant. Prefix limits, route-table headroom and CPU protection belong in verification. Rogers’ core routers “were not configured with such overload protection mechanisms”[8].
  • Sustainability. Minor here: tear down emulation labs after use (containerlab destroy), and right-size the CI runners.

Watch the system that makes changes. Signals: fail rate by path; share of changes with complete evidence; drift exceptions past expiry; break-glass and bypass events; age of the last rollback drill; verification and guard overrides. Drill quarterly: push a known-bad change (for example Lab 2’s ACL swap) through the real pipeline against a lab or canary target. Confirm that verification blocks it, and that an unconfirmed commit reverts within its timer.

Symptom Likely cause Diagnosis Remediation Prevention
Pipeline green, traffic lost after deploy Missing invariant or model blind spot Differential routes and flows, reference vs production; initIssues Roll back; add the invariant One new check per escaped defect
Commit reverted although the change was fine Confirmation path crossed the changed link; sessions slower than the check deadline; job timed out Device commit log; check and job durations vs timer Re-deploy; confirm out of band or via a persistent session[11] Poll checks to a deadline; derive the timer from it; separate management path
Someone’s pending confirmed commit became permanent Another tool edited the device; with RESTCONF, “any new commit will act as the confirming commit”[13] AAA accounting for concurrent sessions Restore the reference config One writer per device; NETCONF <persist>, so RESTCONF edits fail with 409[13]; gNMI refuses plain Sets during a timer[20]
Device rejects rendered config Syntax differs by platform or version Device error; compare versions Fix the template branch Emulate the exact version in CI
Drift returns after each deploy A second writer (controller, script, person) Accounting logs: who wrote it Remove or integrate the writer Single source of change per domain
Replace removed management access Template did not cover management config Out-of-band console Let the timer revert Templates own the whole config; never use overridden without coverage[35]
Change hit the wrong devices Filter, wildcard or template error Rendered diff vs change record Stop waves; roll back extras Guard with target and must-not-change lists[6]

Illustrative scenario, modelled on the failure class in Cloudflare’s report[7]. Wave 2 of a routing-policy change reaches four edge routers at 10:02. At 10:04 reachability probes for three customer prefixes fail.

  1. Stop the line (minute 0–2). Halt the pipeline so no further wave starts. Do not confirm wave 2; its timer runs.
  2. Name one incident lead (minute 1). Only the lead orders reverts, so nobody reverts a revert (IncidentOps).
  3. Let the timer work, or cancel it (minute 2–7). Unconfirmed devices restore their previous configuration themselves; with gNMI the lead can cancel by commit ID[20]. If confirmed wave 1 contributes, roll it back from last-known-good configs in Git.
  4. Verify recovery (minute 7–15). Probes green, advertised routes equal the reference snapshot, sessions established.
  5. Preserve evidence. Commit IDs, pipeline and device logs, the verification report that passed.
  6. Fix the system, not the person (days 1–5). Add the missing invariant (“these prefixes are advertised to every transit peer”), put a router of the new design in wave 1, and record the drill.

Exit criteria. Service restored, reference and production agree, and the new check fails on the bad change in CI.

  • Scripts without a source of truth. Fast at first; then every script carries its own copy of the network.
  • Discovery writes the SoT. Today’s mistakes become tomorrow’s intent; NetBox discourages it[16].
  • A bypass everyone uses. If the emergency path is easier than the pipeline, it becomes the normal path; measure bypasses.
  • Big-bang replace. The whole estate on full-config replace before templates cover everything.
  • “Green means safe.” Models miss what they cannot parse, and labs miss scale; staging and timers cover the rest.
  • Myth: intent-based networking removes configuration work. RFC 9315 describes fulfilment and assurance, and intent drift to watch[10]. Someone still models the intent and checks the outcome.
  • Myth: you need software developers. You need network engineers who read Python and YAML, and a few who write it well.
Use it when… Hold back when…
Changes of the same kind recur weekly (VLANs, peers, ACL entries, sites) The estate is small and changes rare; backups, peer review and a written procedure may do
Outages or near-misses trace back to changes Every device is unique; templates would be as long as the configs
Several engineers change the same devices Nobody will own the data; an unowned SoT is worse than none
You need audit evidence for each change Platforms have neither commit confirm nor a tested rollback, or will soon be replaced; fix backups and rollback first (missing APIs alone are no reason: template the CLI)
You are about to grow fast (new sites, fabric expansion) A vendor controller already runs the domain well; feed it from the SoT

Hidden responsibilities. Templates follow every OS upgrade, the verification suite stays current, and the SoT is a production service with backups and access reviews.

Figure 5. Should you build a NetworkOps pipeline, and which kind?
yesyesnoyesnonoyesnoAre changes frequent,or has a changecaused an outage?Not yet: configbackups, peer review,written rollbackWill someone owneach data domain?Fix ownership first; aSoT without ownersdecaysDoes a vendorcontroller already runthis domain well?Feed the controllerfrom the SoT; addverification evidenceDo devices offerNETCONF or gNMIwith confirmedcommit?Model-driven pipelinewith confirmedcommitsCLI templating viaNAPALM or Ansible;small waves; testedrollback
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 150, "nodeSpacing": 14, "rankSpacing": 24, "padding": 8}}}%%
flowchart TB
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  Q1["Are changes frequent, or has a change caused an outage?"]:::decision
  N1["Not yet: config backups, peer review, written rollback"]:::neutral
  Q2["Will someone own each data domain?"]:::decision
  N2["Fix ownership first; a SoT without owners decays"]:::risk
  Q3["Does a vendor controller already run this domain well?"]:::decision
  C1["Feed the controller from the SoT; add verification evidence"]:::core
  Q4["Do devices offer NETCONF or gNMI with confirmed commit?"]:::decision
  M1["Model-driven pipeline with confirmed commits"]:::core
  M2["CLI templating via NAPALM or Ansible; small waves; tested rollback"]:::core
  Q1 -->|"yes"| Q2
  Q2 -->|"yes"| Q3
  Q3 -->|"no"| Q4
  Q4 -->|"yes"| M1
  Q1 -->|"no"| N1
  Q2 -->|"no"| N2
  Q3 -->|"yes"| C1
  Q4 -->|"no"| M2

Text equivalent: Are changes frequent, or has a change caused an outage? If not, rely on configuration backups, peer review and a written rollback for now. If so, will someone own each data domain? If not, fix ownership first: a source of truth without owners decays. If owners exist and a vendor controller already runs the domain well, feed it from the source of truth and add verification evidence. Otherwise, if devices offer NETCONF or gNMI with confirmed commit, build a model-driven pipeline with confirmed commits; if not, template the CLI through NAPALM or Ansible, with small waves and a tested rollback.

Assumptions. “Frequent” means, in this Atlas’s suggestion, a change class that recurs at least weekly. Mixed estates choose per platform; one pipeline can serve both.

This Atlas’s model, not an industry standard. Move up when the exit criterion has held for a quarter.

Figure 6. NetworkOps maturity progression (Atlas model)
0 Manual: CLI by hand, configs in personalfilesExit: nightly config backups in Git for everydevice1 Scripted: read-only automation, backups,inventoriesExit: a SoT with named owners covers thein-scope estate2 Generated: configs rendered from the SoT;drift reportedExit: drift under 5 % and every change viapull request3 Verified: CI checks, staged waves,confirmed commits, evidenceExit: fail rate tracked by path; quarterlybad-change drill passes4 Assured: telemetry-based checks, narrowclosed-loop fixes
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 300, "rankSpacing": 18, "padding": 8}}}%%
flowchart TB
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  L0["0 Manual: CLI by hand, configs in personal files"]:::risk
  E0["Exit: nightly config backups in Git for every device"]:::decision
  L1["1 Scripted: read-only automation, backups, inventories"]:::neutral
  E1["Exit: a SoT with named owners covers the in-scope estate"]:::decision
  L2["2 Generated: configs rendered from the SoT; drift reported"]:::neutral
  E2["Exit: drift under 5 % and every change via pull request"]:::decision
  L3["3 Verified: CI checks, staged waves, confirmed commits, evidence"]:::core
  E3["Exit: fail rate tracked by path; quarterly bad-change drill passes"]:::decision
  L4["4 Assured: telemetry-based checks, narrow closed-loop fixes"]:::core
  L0 --> E0 --> L1 --> E1 --> L2 --> E2 --> L3 --> E3 --> L4

Text equivalent: Five levels with exit gates. Level 0, manual (CLI by hand, configs in personal files): exit when every device has nightly config backups in Git. Level 1, scripted (read-only automation, backups, inventories): exit when an owned source of truth covers the in-scope estate. Level 2, generated (configs rendered from it, drift reported): exit when drift is under 5 % and every change goes through a pull request. Level 3, verified (CI checks, staged waves, confirmed commits, evidence): exit when fail rate is tracked by path and the quarterly bad-change drill passes. Level 4, assured: telemetry-based checks, narrow closed-loop fixes.

For a team at level 0–1 starting with one domain, such as data-centre leaf switches.

Window Actions Milestone
Days 1–30 Nightly backups to Git; one domain, one change class; stewards named; NetBox up; one reviewed import Rendered-vs-running diff for the domain
Days 31–60 Whole-config templates; Lab 1-style checks and the guard in CI; Batfish snapshot of production; separate CI and deploy credentials First change class shipped by pull request, verified, pushed by hand
Days 61–90 Deploy job with waves, confirmed commits, post-checks; drift report; first bad-change drill; KPIs Standard change approved automatically; fail rate measured by path

Quick wins: backups in Git; a read-only “what would change” report. Dependencies: out-of-band access to every device in scope; AAA accounts for automation.

Before any change (author)

  • A pull request with the data change (or its SoT branch), not a device session
  • Rendered diff reviewed; the guard passed against targets and must_not_change
  • Verification passed: no new parse issues, invariants hold, differences explained
  • First wave contains a device of every affected design
  • Confirmation path independent of the change
  • Rollback source named (last-known-good commit); incident lead on call

Pipeline readiness (automation engineer)

  • No device credentials in CI; device secrets in the production environment, not the repository
  • Runner group limited to the deploy workflow at main; just-in-time runners where possible
  • Plan allows required reviewers on a private repository; admin bypass off; self-review prevented
  • Actions pinned to commit SHAs; packages installed with hashes
  • Revert timer derived from the check deadline; replace only with full template coverage
  • One deployment at a time per domain (concurrency or lock)
  • SoT token scoped, IP-restricted, rotated; out-of-band access tested this quarter

Quarterly governance audit

  • Bad-change drill run and passed
  • Drift exceptions past expiry closed
  • Break-glass events reconciled into the SoT
  • Tool versions pinned and upgraded in CI first

Network change record (one per pull request):

Change ID:            NW-2026-____
Class:                standard | normal | emergency
Summary:              what changes, in one sentence
Blast radius:         change.yaml targets + must_not_change; guard result
Waves:                wave 1 (canary, representative): ...; wave 2: ...
Verification:         link to CI report (init issues, invariants, differential)
Timer:                confirmed commit ___ s; confirmation path: ___
Post-checks:          sessions, routes, probes expected after each wave
Rollback:             last-known-good commit ___; incident lead ___
Approvals:            reviewer ___; change manager ___ (normal/emergency)
Evidence (after):     deploy log, post-check results, drift diff = empty

Data ownership charter (one per data domain):

Domain:               IPAM | circuits | BGP policy | VLANs | ...
Steward (accountable): name / team
Writers:              who may write, through which path (UI, API, pipeline)
Required fields:      list; validation rule for each
Quality target:       completeness >= __ %; monthly sample audit of __ devices
Discovery data:       allowed only as a reconciliation report, never written directly
Review:               monthly with the network lead

Architecture decision record: merge or replace for platform X

Context:      platform, OS versions, template coverage %, commit-confirm support
Options:      merge | replace | replace for some sections only
Decision:     ...
Consequences: drift handling, rollback method, lock-out risk and mitigation
Review date:  ...
Approach Best for Strength Weakness
CLI templating (Ansible, Nornir + NAPALM or Netmiko) Mixed and legacy estates Works with what you have Ordering and parsing fragility; rollback varies by platform[33]
Model-driven (NETCONF, gNMI with YANG/OpenConfig) Modern platforms Transactions, validation, streaming[11][18] Model coverage gaps; steeper learning
Vendor controller or intent platform One vendor’s fabric or SD-WAN Integrated workflows and assurance Lock-in; SoT and verification still needed upstream
Pull-based reconciliation (GitOps style) Small, homogeneous estates with strong checks Continuous drift correction A bad commit reaches everything unless staged

Is NetworkOps the same as NetDevOps? As this Atlas uses it, yes; “NetDevOps” and “network automation” are the common names.

Do I need NETCONF or gNMI to start? No. CLI templating with good verification and small waves is a valid start; model-driven interfaces add transactions and confirmed commits.

NetBox or YAML files in Git? YAML suffices for tens of devices and one team. Move to NetBox or Nautobot when several teams edit data, you need IP allocation, or other systems consume it.

Does Batfish replace a lab? No. It analyses the whole network but only supported syntax[44]; a lab runs real software at small scale. They catch different faults.

Can the network fix itself? For narrow, reversible actions with clear signals (draining a flapping link), at maturity level 4; broad automatic changes are how automation causes large failures.

What if a change must happen during an outage? Emergency class: break-glass, logged, reconciled into the SoT within a working day, reviewed.

  • Established now: model-driven configuration with transactions, streaming telemetry, Batfish analysis, container-based emulation, staged rollout with confirmed commits.
  • Maturing: intent assurance (continuous checks for intent drift)[10], and emulated twins of production for every change; the Rogers assessment asks for test scenarios that “replicate the production network”[8].
  • Research: Google’s paper names specifying intent, collecting the network’s “ground truth” and reconciling the two as an underexplored area of management-plane design[6].
  • Vendor marketing to read critically: “self-driving” and AI-run networks. Ask which actions run without a human, what verifies them first, and how they roll back.
Stage Do this Proof of progress
Prerequisites Routing, switching, BGP basics (NetOps); Git; YAML You can explain a BGP session state
Day 1 Lab 1; read RFC 3535 §3 (two pages)[9] Lab 1 outputs reproduced
Week 1 Lab 2; write one invariant for your own network in Batfish A check that fails on a deliberate mistake
Month 1 Lab 3; NetBox with one site; render a config; a Nornir dry run against a lab A pull request that renders, verifies and deploys to a lab
Advanced YANG and OpenConfig models; gNMI streaming; pipeline security You can defend your rollback design in a review
Portfolio A public repository: SoT, templates, checks, lab topology, CI Someone else can run it from the README
  • Decide: one domain and one recurring change class first; data stewards named; merge or replace per platform.
  • Build: SoT → generated configs in Git → verification (model, lab) → staged waves → confirmed commit → state checks.
  • Guard: no device secrets in CI; a restricted deploy runner; one writer per device; the rendered diff checked against two lists; an out-of-band path the change cannot break.
  • Measure: change fail rate split by path, pre-deployment catch rate, drift rate and age, SoT completeness.
  • Watch out: discovery writing the SoT, bypasses becoming normal, risk scores that learn from success, green meaning safe.
  • Next action: nightly config backups in Git this week; everything else builds on them.

A judgement call: replace the whole configuration or merge changes?

Link to section “A judgement call: replace the whole configuration or merge changes?”

Situation (illustrative). 300 devices: 200 EOS and Junos devices whose NAPALM drivers support replace and commit confirm, and 100 older ones. Templates cover about 70 % of each configuration; the rest is local history. The automation lead wants full replace (A) to eliminate drift; a senior engineer prefers merge (B) because it cannot delete what templates do not know.

Reasoning. (1) Worst failure: under A, the uncovered 30 % is deleted on first deploy, possibly including management access (Ansible’s warning about overridden[35]). Under B, drift and stale config persist. (2) Does the timer rescue A? Partly: a confirmed commit reverts a lock-out[11], but only where supported[33] and only if confirmation waits for evidence. (3) Cost of A: full template coverage per platform (assumed: weeks each). (4) What would change the answer: full coverage and commit confirm everywhere make A clearly better.

Decision: B now, A per platform as coverage reaches 100 %. Run replace as a nightly dry run (compare_config); its diff lists the lines templates still miss. Uncertainty: how long the older devices stay; if replacement is near, keep them on merge.

After this page you can explain the label and its boundary with NetOps; design a source of truth with owners; choose merge or replace; build a verification gate; roll out in waves with confirmed commits; and measure change outcomes.

1. A colleague says “NetworkOps is our NOC team’s new name”. What is accurate and what is not?

The label is informal and overlaps with NetOps, NetDevOps and network automation[5][4]. In this Atlas the NOC’s work is NetOps and NetworkOps is change engineering, an editorial boundary, not a standard.

2. Why does NetBox discourage importing live network state, and what do you do with discovered data instead?

The SoT represents desired state; importing live state records mistakes as intent[16]. Discovery produces a reconciliation report; stewards then fix the data or the device.

3. A pipeline deploys and then confirms automatically in the same job, with no checks in between. What is lost?

The value of the timer: it helps only if confirmation depends on post-change evidence gathered over a path the change cannot break. Otherwise a lock-out is confirmed too.

4. Cloudflare’s change had a dry run, peer review and a stepped rollout. Name two controls that would have changed the outcome.

A stagger that reached an MCP site early, and an automated commit-confirm rollback; Cloudflare’s follow-ups cover both[7]. A differential check of advertised prefixes could also have shown the withdrawals before deployment, if the model parsed that policy.

5. In a quarter, 6 of 300 pipeline changes and 6 of 30 manual changes failed. What is the fail rate, and what does it suggest?

Overall 12 ÷ 330 = 3.6 %; pipeline 2 %, manual 20 %. The blend hides where risk lives: ask why 30 changes bypassed the pipeline before tuning it.

6. During a gNMI commit-confirmed window, a teammate’s script sends a normal Set. What happens, and why is that good?

The target returns FAILED_PRECONDITION[20], so unreviewed changes cannot be stacked onto an unconfirmed one and make the revert ambiguous.

The word “NetworkOps” adds nothing; the practice behind it adds a great deal. Operators asked for it in 2002[9]: treat the network as one system described by data, change it through transactions, keep a way back. The three outages on this page all had review or audit steps. What they lacked was verification of meaning, representative first steps, and an exit that did not depend on the network being changed. Build those, measure what fails, and the tooling choice gets easy. Product facts are dated October 2026; re-check them.

Changelog. 2026-10-18: first draft (r0), researched and written by the managing author; Lab 1 executed, Labs 2–3 and the reference implementation syntax-checked only. 2026-10-18 (r1): revised after the independent review: label status, runner and secret settings, a source-of-truth change path, a guard that checks the rendered diff (executed), post-check polling, NETCONF example, controller domains, transport and chronology fixes, pinned actions. 2026-10-18, post-score polish: Branching revert precondition; Lab 2’s Cloudflare comparison made precise; branch rendering marked untested; Lab 3 confirms only after a read-back; OpenConfig-or-native and path-reading paragraph; SDN sentence; beginner pointer; figure 3 label moved into its node (no crossing); Lab 1 and the guard re-run with Jinja2 3.1.6 and PyYAML 6.0.2; restatements trimmed.


  1. NetworkOps_Platform — E-Conners-Lab on GitHub, created 2026-02-18. Checked 2026-10-18. ↩︎

  2. GitHub repository search: “networkops” in repository names — GitHub REST API, 26 results on 2026-10-18. Checked 2026-10-18. ↩︎

  3. Network Automation Forum — NAF (AutoCon events). Checked 2026-10-18. ↩︎ ↩︎

  4. The Anatomy of NetDevOps — Siming Yuan, Cisco Blogs, 2019-05-22. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎

  5. What is NetOps? — Red Hat, 2023-08-28. Checked 2026-10-18. ↩︎ ↩︎

  6. Evolve or Die: High-Availability Design Principles Drawn from Google’s Network Infrastructure — Govindan, Minei, Kallahalla, Koley and Vahdat, ACM SIGCOMM 2016. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  7. Cloudflare outage on June 21, 2022 — Tom Strickx and Jeremy Hartman, Cloudflare, 2022-06-21. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  8. Assessment of Rogers Networks for Resiliency and Reliability Following the 8 July 2022 Outage – Executive Summary — Xona Partners for the CRTC, July 2024 (read via the Internet Archive copy of 2025-12-20; the live page blocked automated access). Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  9. RFC 3535: Overview of the 2002 IAB Network Management Workshop — IETF, May 2003. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎

  10. RFC 9315: Intent-Based Networking — Concepts and Definitions — IRTF NMRG (informational), October 2022. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  11. RFC 6241: Network Configuration Protocol (NETCONF) — IETF, June 2011. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  12. RFC 7950: The YANG 1.1 Data Modeling Language — IETF, August 2016. Checked 2026-10-18. ↩︎ ↩︎

  13. RFC 8040: RESTCONF Protocol — IETF, January 2017. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  14. RFC 8342: Network Management Datastore Architecture (NMDA) — IETF, March 2018. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  15. RFC 7426: SDN Layers and Architecture Terminology — IRTF (informational), January 2015. Checked 2026-10-18. ↩︎ ↩︎

  16. Introduction to NetBox — NetBox Labs documentation. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  17. Batfish — The Batfish open source project. Checked 2026-10-18. ↩︎ ↩︎

  18. gRPC Network Management Interface (gNMI) specification v0.10.0 — OpenConfig, 2023-05-25; first revision committed 2016-11-07 (file history). Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  19. OpenConfig — OpenConfig project. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎

  20. gNMI Commit Confirmed Extension v0.1.0 — OpenConfig, 2023-09-20. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  21. RFC 8641: Subscription to YANG Notifications for Datastore Updates — IETF, September 2019. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  22. RFC 8572: Secure Zero Touch Provisioning (SZTP) — IETF, April 2019. Checked 2026-10-18. ↩︎ ↩︎

  23. Commit the Configuration — Junos OS CLI User Guide, Juniper Networks. Checked 2026-10-18. ↩︎ ↩︎

  24. NAPALM NetworkDriver API — NAPALM project. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎

  25. ntc-templates — Network to Code on GitHub. Checked 2026-10-18. ↩︎ ↩︎

  26. More details about the October 4 outage — Santosh Janardhan, Engineering at Meta, 2021-10-05. Checked 2026-10-18. ↩︎

  27. Configuration Rendering — NetBox Labs documentation. Checked 2026-10-18. ↩︎ ↩︎

  28. Validating deployments — NAPALM project. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  29. NIST SP 800-128: Guide for Security-Focused Configuration Management of Information Systems — NIST, 2011, updated 2019-10-10. Checked 2026-10-18. ↩︎ ↩︎

  30. Secure use reference — GitHub Docs. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  31. NetBox Branching and Using the REST API — NetBox Labs documentation (plugin v1.2.2). Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  32. SR Linux System Management Guide 25.10: gNMI — Nokia. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  33. NAPALM supported devices and configuration support matrix — NAPALM project. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  34. ansible.netcommon.cli_config module (collection 8.7.1) — Ansible documentation. Checked 2026-10-18. ↩︎ ↩︎

  35. Network resource modules — Ansible documentation. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  36. SR Linux System Management Guide 25.10: YANG data models — Nokia. Checked 2026-10-18. ↩︎ ↩︎

  37. Schema path encoding conventions for gNMI v0.4.0 — OpenConfig, 2017-06-21. Checked 2026-10-18. ↩︎

  38. ncclient — ncclient project on GitHub (v0.7.1 released 2026-03-15). Checked 2026-10-18. ↩︎ ↩︎

  39. PyPI release pages: nornir, napalm, pybatfish, netmiko, nornir-netbox — Python Package Index. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  40. Nautobot documentation — Nautobot (v3.2.7 released 2026-10-13). Checked 2026-10-18. ↩︎

  41. Nornir documentation 3.6.0 — nornir-automation. Checked 2026-10-18. ↩︎

  42. Netmiko — Kirk Byers on GitHub. Checked 2026-10-18. ↩︎

  43. gnmic with set and subscribe — OpenConfig (v0.49.0 released 2026-09-16). Checked 2026-10-18. ↩︎ ↩︎

  44. pybatfish documentation: setup, interacting, questions, supported devices — The Batfish project. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  45. containerlab quick start, Nokia SR Linux kind, Arista cEOS kind, destroy command — containerlab (v0.81.1 released 2026-10-15). Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  46. REST API: tokens, write restriction, client IP restriction — NetBox Labs documentation. Checked 2026-10-18. ↩︎ ↩︎ ↩︎

  47. NetBox v4.7.3 release — netbox-community on GitHub, 2026-10-13. Checked 2026-10-18. ↩︎

  48. Use Docker service containers — GitHub Docs. Checked 2026-10-18. ↩︎

  49. Deployments and environments — GitHub Docs. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎

  50. Managing access to self-hosted runners using groups — GitHub Docs (Enterprise Cloud). Checked 2026-10-18. ↩︎ ↩︎

  51. Secure installs — pip documentation. Checked 2026-10-18. ↩︎

  52. nornir_napalm tasks — nornir-automation on GitHub (v0.6.0). Checked 2026-10-18. ↩︎

  53. Status of Python versions — Python Developer’s Guide. Checked 2026-10-18. ↩︎

  54. DORA’s software delivery performance metrics — DORA, updated 2026-01-05. Checked 2026-10-18. ↩︎

  55. RFC 7589: Using the NETCONF Protocol over TLS with Mutual X.509 Authentication — IETF, June 2015. Checked 2026-10-18. ↩︎

Related Ops

NetOps

NetOps (network operations) is the day-to-day work of running a network: watching its signals, isolating faults, escalating to carriers, running change windows and planning capacity. The label is shorthand; vendors also use it for NetDevOps.

AutomationOps

AutomationOps is not a standard term. This Atlas uses it for running operational automation as a governed discipline: choosing what to automate, making it idempotent and safe to repeat, limiting how far one run can reach, and owning it until it is retired.

InfraOps

InfraOps is informal shorthand for infrastructure operations. This Atlas uses it for running infrastructure through code: declaring it, reviewing every change as a plan, applying it from a pipeline and keeping the tool's state in step with reality.

GitOps

GitOps is a way of operating software systems in which the desired state is declared in a versioned, immutable store such as Git, and software agents continuously pull it and reconcile the running system to match.

CloudOps

CloudOps (cloud operations) is running workloads on public cloud through the provider's control plane: account hierarchy, guardrails, quotas, Regions, provider signals and managed-service change. The label is common, but no neutral body defines it.

EdgeOps

EdgeOps is this Atlas's name for operating compute at many remote sites, such as stores, plants and cell sites: onboarding nodes without experts, changing software in staged waves, keeping sites working through network outages and recovering them remotely.

ObservabilityOps

ObservabilityOps is this Atlas's name for running observability as an internal product: the telemetry pipeline, standards, budgets and alert hygiene that let engineers answer new questions about production at a predictable cost. The label itself is not standardised.

KubernetesOps

KubernetesOps is this Atlas's name for operating Kubernetes clusters and the workloads on them: keeping the control plane, nodes and add-ons healthy and current, making workloads schedule and scale correctly, and diagnosing failures.

Practise and get certified

From xOps School

Practise NetworkOps and get certified

The NetworkOps track on xOps School (part of the Coffee Pass) has study notes, a 25-question practice quiz with explanations, and a verifiable certificate when you score 70% or more. Sign in with Google to start.

For teams

Instructor-led training

Live batches for your team, with completion certificates and reports for L&D/HR.

Source: master.md (the editorial source of this page) · built 2026-10-18 · diagrams rendered with @mermaid-js/mermaid-cli 11.17.0 (mermaid 11.17.2)