Skip to main content

Command Palette

Search for a command to run...

Batfish: Network Validation Before You Break Production

Pre-Deployment Validation for Nokia SR OS

Updated
โ€ข9 min readโ€ขView as Markdown
Batfish: Network Validation Before You Break Production

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
๐Ÿ’ก
(Substitute whatever the current newest 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"]]
๐Ÿ’ก
Active: True here doesn't mean "this interface is up right now". Batfish has no live connection to the device. It's a purely static-analysis conclusion drawn from the configuration alone.

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
๐Ÿ’ก
An incorrect AS on one PE is invisible in isolation but obvious the moment two nodes' 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_AS

IP 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)