Batfish: Network Validation Before You Break Production
Pre-Deployment Validation for Nokia SR OS

Every article so far has been about doing something to the network: pushing configurations, collecting state, reacting to events. This article is about the step that happens before any of that: checking whether a change is safe before it ever touches a device.
That's the job Batfish was built for. It's a static analysis engine for network configuration: it parses your config files, builds a vendor-neutral model of the network (RIBs, FIBs, ACLs, BGP sessions, the works), and lets you ask questions like "will this ACL block traffic it shouldn't" or "is this BGP session actually going to come up" without touching a single live device. No SSH, no NETCONF session, no risk of a bad commit taking down a production network
Batfish in a Multivendor Network
One reason Batfish is interesting for network engineers is its broad platform support.
The current Batfish project lists support for platforms including Cisco IOS, IOS-XE, IOS-XR, NX-OS, Juniper JunOS, Arista EOS, Palo Alto, Fortinet, SONiC, and several others. Nokia SR OS was not supported on Batfish. Only recently Nokia SR OS / SR-SIM with MD-CLI is supported.
However, "supported platform" does not mean every feature of every software release is perfectly modeled.
Batfish validation work continues to uncover vendor-specific behavioral differences, and current issue reports show some features may be partially recognized or modeled differently from real device behavior
For production use, validate the specific features your network depends on.
Why bother with this on top of pyATS/Genie and NAPALM
Worth being precise about what Batfish is not. It's not a live-state collector like NAPALM or Genie, and it's not a config-push tool like Netmiko or Ansible. It doesn't care what the device is doing right now. It cares what the device is configured to do, and what will actually happen once that configuration is active. That distinction matters:
Genie's learn() parses live show-command output. It displays the current operational state as documented in our earlier articles.
Batfish parses the configuration itself and computes forwarding behavior from first principles. It doesn't need the device to be reachable or even up. A snapshot of the configuration file is enough.
Setup
Installing Batfish and Pybatfish
Getting started with Batfish is easy. First, pull and run the latest Docker container:
docker pull batfish/allinone:test-2026.08.20.3666
docker run -d --name batfish \
-v batfish-data:/data \
-p 8888:8888 -p 9997:9997 -p 9996:9996 \
batfish/allinone:test-2026.08.20.3666
test-* tag is by the time you're reading this: they roll frequently.)Confirm the container actually started before moving on
docker ps --filter name=batfish
Then, install Pybatfish using pip:
python3 -m venv batfish-env
source batfish-env/bin/activate
pip install --upgrade pybatfish
Pybatfish requires Python 3, and it is recommend that you install it in a virtual environment
Topology
Our test bed is powered by containerlab
Collecting SR OS configuration for a snapshot
Batfish snapshots are just directories of config files on a disk; no live connectivity required for the analysis itself.
SFTP pulling a saved config file is reliable and the easiest route
On each node, save the running config to a file first:
# From MD-CLI, in the router's own session
admin save cf3:\PE1.cfg
Then pull it off with SFTP:
mkdir -p snapshot/configs
sftp admin@PE1:cf3:/PE1.cfg snapshot/configs/PE1.cfg
Directory layout: Batfish expects the following structure, with configs/ holding device configurations
# sample of the snapshot directory structure
snapshot/
โโโ configs/
โโโ PE1.cfg
โโโ PE2.cfg
โโโ switch-1.cfg
Interacting with the Batfish Service
Batfish Questions
Batfish builds a network model from your configs and other data. You can then query that model or the parsed config settings directly using several categories of Batfish questions.
Before we start, always check the File Parse Status of your saved configurations. I faced an issue in which the Batfish format detector didn't recognize the files as any supported vendor.
The actual cause: The batfish/allinone:latest Docker image didn't have the Nokia SR OS parser.
Getting status of parsed files
Batfish may ignore certain lines in the configuration. To retrieve the parsing status of snapshot files, use the fileParseStatus() question.
Invocation :
from pybatfish.client.session import Session
from pybatfish.datamodel import *
bf = Session(host="localhost")
bf.set_network("multicast-env")
bf.init_snapshot("snapshot", name="multicast", overwrite=True)
bf.q.fileParseStatus().answer().frame()
Output :
File_Name Status File_Format Nodes
0 configs/CE1.cfg PASSED NOKIA_SROS ['switch-1']
1 configs/CE2.cfg PASSED NOKIA_SROS ['switch-2']
2 configs/CE3.cfg PASSED NOKIA_SROS ['switch-3']
3 configs/PE1.cfg PASSED NOKIA_SROS ['pe1']
4 configs/PE2.cfg PASSED NOKIA_SROS ['pe2']
Interface Properties
Returns configuration settings of interfaces; nodes="pe1" is a filter narrowing the question to one node's rows, not a sign that pe1 is its own snapshot. Drop the filter (or use nodes=".*" ) to see every router in the snapshot at once.
Invocation :
interfaces =bf.q.interfaceProperties(nodes="pe1").answer().frame()
Output :
Interface Active Admin_Up All_Prefixes
0 pe1["1/1/c1/1"] True True []
1 pe1["1/1/c3/1"] True True []
2 pe1[ng-mvpn-ldp.toCE1] True True ['172.28.11.5/24']
3 pe1[ng-mvpn-ldp.toCE3] True True ['172.28.13.5/24']
4 pe1[switch-1] True True ['172.28.1.1/27']
5 pe1[switch-2] True True ['172.28.2.1/27']
6 pe1[switch-3] True True ['172.28.3.1/27']
7 pe1[system] True True ['192.10.10.1/32']
8 pe1[toPE2] True True ['192.10.1.2/24']
--------- output Truncated
Or just pull the columns that actually matter day-to-day:
interfaces =bf.q.interfaceProperties(nodes="pe1").answer().frame()
interfaces[["Interface", "Primary_Address", "Active", "VRF", "MTU"]]
BGP Peer Configuration
Returns configuration settings for BGP peerings. Reports configuration settings for each configured BGP peering on each node in the network. This question reports peer-specific settings. Settings that are process-wide are reported by the bgpProcessConfiguration question.
Invocation :
peers = bf.q.bgpPeerConfiguration(nodes="pe1").answer().frame()
peers[["Local_AS", "Remote_AS", "Remote_IP", "Local_IP"]]
Output :
Local_AS Remote_AS Remote_IP Local_IP
0 65000 65000 192.10.10.2 None
1 65000 65001 172.28.13.4 None
2 65000 65001 172.28.11.4 None
bgpPeerConfiguration outputs are compared side by side. A mismatch shows up fast across a multi-PE snapshot by pulling the same question with nodes=".*" and diff Remote_ASIP Owners
Returns where IP addresses are attached in the network. For each device, lists the mapping from IPs to corresponding interface(s) and VRF(s)
Invocation :
result = bf.q.ipOwners(nodes="pe1").answer().frame()
result.head(5)
Output :
Node VRF Interface IP Mask
0 pe1 default toPE2 192.10.1.2 24
1 switch-1 default system 192.10.10.3 32
2 pe1 default switch-3 172.28.3.1 27
3 pe2 ng-mvpn-ldp ng-mvpn-ldp.toCE3 172.28.14.5 24 T
4 switch-2 ng-mvpn-ldp ng-mvpn-ldp.toPE2 172.28.12.4 24
Routing and Forwarding Tables
This category of questions allows you to query the RIBs and FIBs computed by Batfish.
Invocation :
routes = bf.q.routes().answer().frame()
routes[routes["Node"] == "pe1"]
Output :
Node VRF Network Next_Hop
0 pe1 default 172.28.1.0/27 interface switch-1
1 pe1 default 172.28.2.0/27 interface switch-2
2 pe1 default 172.28.3.0/27 interface switch-3
3 pe1 default 192.10.1.0/24 interface toPE2
4 pe1 default 192.10.10.1/32 interface system
5 pe1 default 192.10.10.2/32 ip 192.10.1.1
6 pe1 ng-mvpn-ldp 172.28.11.0/24 interface ng-mvpn-ldp.toCE1
7 pe1 ng-mvpn-ldp 172.28.13.0/24 interface ng-mvpn-ldp.toCE3
........ output truncated
Traceroute
Performs a virtual traceroute in the network from a starting node. A destination IP and ingress (source) node must be specified. Other IP headers are given default values if unspecified. Unlike a real traceroute, this traceroute is directional. That is, for it to succeed, the reverse connectivity is not needed. This feature can help debug connectivity issues by decoupling the two directions.
Invocation :
result = bf.q.traceroute(
startLocation="pe1",
headers=HeaderConstraints(dstIps="192.10.10.2")
).answer().frame()
Output :
Flow
0 start=pe1 [172.28.1.1:49152->192.10.10.2:33434... FORWARDED
1 start=pe1 [172.28.2.1:49152->192.10.10.2:33434... FORWARDED
2 start=pe1 [172.28.3.1:49152->192.10.10.2:33434... FORWARDED
3 start=pe1 [192.10.1.2:49152->192.10.10.2:33434... FORWARDED
4 start=pe1 [192.10.10.1:49152->192.10.10.2:3343... FORWARDED
5 start=pe1 vrf=ng-mvpn-ldp [172.28.11.5:49152->... NO_ROUTE
6 start=pe1 vrf=ng-mvpn-ldp [172.28.13.5:49152->... NO_ROUTE
......... output truncated
This is the one that tends to sell people on Batfish. It simulates a flow through the modeled network and shows every hop, including ones a live traceroute would never reveal (like a policy silently dropping the packet two hops earlier than expected):
Reachability
Searches across all flows that match the specified conditions and returns examples of such flows. This question can be used to ensure that certain services are globally accessible and parts of the network are perfectly isolated from each other.
Invocation :
bf.q.reachability(
pathConstraints=PathConstraints(startLocation="pe1", endLocation="pe2"),
headers=HeaderConstraints(dstIps="192.10.10.2/32")
).answer().frame()
Output :
0 start=pe1 [172.28.1.1->192.10.10.2 ICMP (type=... [((ORIGINATED(default), FORWARDED(Forwarded ou... 1
Final Thoughts
Everything above only scratches one corner of what Batfish can actually do. The questions covered here are parse status, BGP session/RIB checks, traceroute; these are the ones that mapped directly onto the SR OS troubleshooting done in this article, but Batfish's question library is much larger, and different use cases lean on entirely different parts of it:
Pre-change validation / CI merge gates: the pattern built out here: diff a candidate snapshot against a baseline before a config ever reaches a device
Security and compliance auditing: checking every ACL/firewall filter across the network at once for mistakes: rules that never apply, rules that are too permissive, or filters missing where they're expected
End-to-end reachability and blast-radius analysis: "if this link/node fails, what's still reachable" or "can subnet A reach subnet B under any circumstance," computed once for the whole network rather than traced link-by-link
Design/architecture review before a build: modeling a planned topology from draft configs before any hardware or lab is stood up, to sanity-check routing and reachability on paper first
One article's worth of examples doesn't come close to covering all use cases. Everything demonstrated here was scoped narrowly to what came up while troubleshooting one lab topology.
Where it actually sits in the automation stack: Batfish isn't a replacement for anything already built in this series. It's the layer that runs before all of it. A realistic pipeline would look like this :
config change (git commit)
โ
โผ
Batfish validation โ this article: parse status,
(pre-deployment) ACL/BGP sanity checks against a snapshot
โ (pass)
โผ
Netmiko/pySROS push โ router-backups-pipeline, pysros script
(deployment)
โ
โผ
Ansible EDA reaction โ ansible-eda-sros:event-driven resose
(post-deploy events) to things like the CHASSIS cardFailure
โ
โผ
pyATS/Genie / gNMIc โ live operational-state verification,
telemetry checks clab-monitoring-stack dashboards
(post-deployment)



