{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Uncovering Configuration and Behavior Drift" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When debugging network issues, it is important to understand how the network is different today compared to yesterday or to the desired golden state. A text diff of device configs is one way to do this, but it tends to be too noisy. It will show differences that you may not care about (e.g., changes in whitespace or timestamps), and it is hard to control what is reported. More importantly, text diffs also do not tell you about the impact of change on network behavior, such as if new traffic will be permitted or if some BGP edges will go down.\n", "\n", "Batfish parses and builds a vendor-neutral model of device configs and behavior. This model enables you to learn how two snapshots of the network differ exactly along the aspects you care about. The behavior modeling of Batfish also lets you understand the full impact of these changes. This notebook illustrates this capability. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We focus on the following differences across three categories. \n", "\n", " 1. Configuration settings\n", " 1. Node-level properties\n", " 1. Interface-level properties\n", " 1. Properties of BGP peers \n", " 1. Structures and references\n", " 1. Structures defined in device configs \n", " 1. Undefined references\n", " 1. Network behavior\n", " 1. BGP adjacencies\n", " 1. ACL lines with treat flows differently \n", "\n", "\n", "These are examples of different types of changes that you can analyze using Batfish. You may be interested in a different aspects of your network, and you should be able to adapt the code below to suit your needs.\n", "\n", "Text diff will help with the configuration settings category at best. The other two categories require understanding the structure of the config and the network behavior it induces. To illustrate this point, the text diff of example configs that we use in this notebook is below. " ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\r\n", "-----------configs/as1border1.cfg---------\r\n", "@@ -21,7 +21,7 @@\r\n", " !\r\n", " !\r\n", " no ip domain lookup\r\n", "-ip domain name lab.local\r\n", "+ip domain name lab.localp\r\n", " no ipv6 cef\r\n", " !\r\n", " !\r\n", "\r\n", "-----------configs/as1border2.cfg---------\r\n", "@@ -11,7 +11,7 @@\r\n", " !\r\n", " !\r\n", " ntp server 18.18.18.18\r\n", "-ntp server 23.23.23.23\r\n", "+ntp server 18.18.18.19\r\n", " !\r\n", " !\r\n", " no aaa new-model\r\n", "\r\n", "-----------configs/as2border2.cfg---------\r\n", "@@ -59,13 +59,14 @@\r\n", " duplex auto\r\n", " !\r\n", " interface GigabitEthernet0/0\r\n", "- ip address 10.23.21.2 255.255.255.0\r\n", "- ip access-group OUTSIDE_TO_INSIDE in\r\n", "- ip access-group INSIDE_TO_AS3 out\r\n", "- media-type gbic\r\n", "- speed 1000\r\n", "- duplex full\r\n", "- negotiation auto\r\n", "+ shutdown\r\n", "+! ip address 10.23.21.2 255.255.255.0\r\n", "+! ip access-group OUTSIDE_TO_INSIDE in\r\n", "+! ip access-group INSIDE_TO_AS3 out\r\n", "+! media-type gbic\r\n", "+! speed 1000\r\n", "+! duplex full\r\n", "+! negotiation auto\r\n", " !\r\n", " interface GigabitEthernet1/0\r\n", " ip address 2.12.22.1 255.255.255.0\r\n", "\r\n", "-----------configs/as2core1.cfg---------\r\n", "@@ -60,6 +60,7 @@\r\n", " duplex auto\r\n", " !\r\n", " interface GigabitEthernet0/0\r\n", "+ description \"To as2border1 GigabitEthernet1/0\"\r\n", " ip address 2.12.11.2 255.255.255.0\r\n", " media-type gbic\r\n", " speed 1000\r\n", "@@ -67,6 +68,7 @@\r\n", " negotiation auto\r\n", " !\r\n", " interface GigabitEthernet1/0\r\n", "+ description \"To as2border2 GigabitEthernet2/0\"\r\n", " ip address 2.12.21.2 255.255.255.0\r\n", " negotiation auto\r\n", " !\r\n", "\r\n", "-----------configs/as2dept1.cfg---------\r\n", "@@ -84,6 +84,7 @@\r\n", " neighbor as2 remote-as 2\r\n", " neighbor 2.34.101.3 peer-group as2\r\n", " neighbor 2.34.201.3 peer-group as2\r\n", "+ neighbor 2.34.209.3 peer-group as2\r\n", " !\r\n", " address-family ipv4\r\n", " bgp dampening\r\n", "@@ -96,7 +97,6 @@\r\n", " neighbor as2 route-map dept_to_as2 out\r\n", " neighbor 2.34.101.3 activate\r\n", " neighbor 2.34.201.3 activate\r\n", "- maximum-paths eibgp 5\r\n", " exit-address-family\r\n", " !\r\n", " ip forward-protocol nd\r\n", "\r\n", "-----------configs/as2dist1.cfg---------\r\n", "@@ -82,13 +82,13 @@\r\n", " bgp log-neighbor-changes\r\n", " neighbor as2 peer-group\r\n", " neighbor as2 remote-as 2\r\n", "- neighbor dept peer-group\r\n", "- neighbor dept remote-as 65001\r\n", "+ neighbor dept2 peer-group\r\n", "+ neighbor dept2 remote-as 65001\r\n", " neighbor 2.1.2.1 peer-group as2\r\n", " neighbor 2.1.2.1 update-source Loopback0\r\n", " neighbor 2.1.2.2 peer-group as2\r\n", " neighbor 2.1.2.2 update-source Loopback0\r\n", "- neighbor 2.34.101.4 peer-group dept\r\n", "+ neighbor 2.34.101.4 peer-group dept2\r\n", " !\r\n", " address-family ipv4\r\n", " bgp dampening\r\n", "@@ -113,6 +113,7 @@\r\n", " no ip http server\r\n", " no ip http secure-server\r\n", " !\r\n", "+access-list 102 permit tcp host 2.128.0.0 host 255.255.0.0\r\n", " access-list 102 permit ip host 2.128.0.0 host 255.255.0.0\r\n", " access-list 105 permit ip host 1.0.1.0 host 255.255.255.0\r\n", " access-list 105 permit ip host 1.0.2.0 host 255.255.255.0\r\n", "@@ -128,6 +129,9 @@\r\n", " match community dept_community\r\n", " set local-preference 350\r\n", " !\r\n", "+route-map dept_to_as2dist permit 200\r\n", "+ match community dept_community_new\r\n", "+ set local-preference 350\r\n", " !\r\n", " !\r\n", " control-plane\r\n", "\r\n", "-----------configs/as2dist2.cfg---------\r\n", "@@ -118,6 +118,7 @@\r\n", " access-list 105 permit ip host 1.0.2.0 host 255.255.255.0\r\n", " access-list 105 permit ip host 3.0.1.0 host 255.255.255.0\r\n", " access-list 105 permit ip host 3.0.2.0 host 255.255.255.0\r\n", "+access-list 105 permit ip host 3.0.3.0 host 255.255.255.0\r\n", " !\r\n", " route-map as2dist_to_dept permit 100\r\n", " match ip address 105\r\n", "\r\n", "-----------configs/as3border1.cfg---------\r\n", "@@ -120,6 +120,10 @@\r\n", " !\r\n", " ip prefix-list default_list seq 5 permit 0.0.0.0/0\r\n", " !\r\n", "+ip prefix-list bogons seq 5 permit 10.0.0.0/8\r\n", "+ip prefix-list bogons seq 10 permit 172.16.0.0/16\r\n", "+ip prefix-list bogons seq 15 permit 192.168.0.0/16\r\n", "+! \r\n", " ip prefix-list inbound_route_filter seq 5 deny 3.0.0.0/8 le 32\r\n", " ip prefix-list inbound_route_filter seq 10 permit 0.0.0.0/0 le 32\r\n", " access-list 101 permit ip host 1.0.1.0 host 255.255.255.0\r\n", "\r\n", "-----------configs/as3core1.cfg---------\r\n", "@@ -51,9 +51,6 @@\r\n", " !\r\n", " !\r\n", " !\r\n", "-interface Loopback0\r\n", "- ip address 3.10.1.1 255.255.255.255\r\n", "-!\r\n", " interface Ethernet0/0\r\n", " no ip address\r\n", " shutdown\r\n", "@@ -77,6 +74,9 @@\r\n", " interface GigabitEthernet3/0\r\n", " ip address 90.90.90.2 255.255.255.0\r\n", " negotiation auto\r\n", "+!\r\n", "+interface Loopback0\r\n", "+ ip address 3.10.1.1 255.255.255.255\r\n", " !\r\n", " router ospf 1\r\n", " network 3.0.0.0 0.255.255.255 area 1\r\n" ] } ], "source": [ "# Use recursive diff, followed by some pretty printing hacks\n", "!diff -ur networks/drift/reference networks/drift/snapshot | sed -e 's;diff.*snapshot/\\(configs.*cfg\\);^-----------\\1---------;g' | tr '^' '\\n' | grep -v networks/drift" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, it is difficult to grasp the nature and impact of the change from this output, not to mention that it is impossible to build automation on top of it (e.g., to alert on certain types of differences). We show next how Batfish offers a meaningful view of these differences and their impact on network behavior. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'reference'" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Import packages, helpers, and load questions\n", "%run startup.py\n", "from drift_helper import diff_frames, diff_properties\n", "bf = Session(host=\"localhost\")\n", "\n", "# Initialize both the snapshot and the reference that we want to use\n", "NETWORK_NAME = \"my_network\"\n", "SNAPSHOT_PATH = \"networks/drift/snapshot\"\n", "REFERENCE_PATH = \"networks/drift/reference\"\n", "\n", "bf.set_network(NETWORK_NAME)\n", "bf.init_snapshot(SNAPSHOT_PATH, name=\"snapshot\", overwrite=True)\n", "bf.init_snapshot(REFERENCE_PATH, name=\"reference\", overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Configuration settings\n", "\n", "Let first uncover differences in configuration settings, starting with node-level properties.\n", "\n", "#### 1A. Node-level properties\n", "\n", "We focus on three example properties: 1) NTP servers, 2) Domain name, and 3) VRFs that exist on the device. The complete list of node properties extracted by Batfish is [here](https://batfish.readthedocs.io/en/latest/notebooks/configProperties.html#Node-Properties).\n", "\n", "We will compute the property differences between across snapshots using Batfish questions. Batfish makes its models available via a [set of questions](https://batfish.readthedocs.io/en/latest/questions.html). When questions are run in [differential mode](https://pybatfish.readthedocs.io/en/latest/notebooks/differentialQuestions.html), it outputs how the answer differ across two snapshots. " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
 NodeKeyPresenceSnapshot_Domain_NameReference_Domain_NameSnapshot_NTP_ServersReference_NTP_ServersSnapshot_VRFsReference_VRFs
0as1border1In bothlab.localplab.localdefaultdefault
1as1border2In bothlab.locallab.local18.18.18.19

18.18.18.18
23.23.23.23

18.18.18.18
defaultdefault
\n" ], "text/plain": [ " Node KeyPresence Snapshot_Domain_Name Reference_Domain_Name \\\n", "0 as1border1 In both lab.localp lab.local \n", "1 as1border2 In both lab.local lab.local \n", "\n", " Snapshot_NTP_Servers Reference_NTP_Servers \\\n", "0 [] [] \n", "1 ['18.18.18.19', '18.18.18.18'] ['23.23.23.23', '18.18.18.18'] \n", "\n", " Snapshot_VRFs Reference_VRFs \n", "0 ['default'] ['default'] \n", "1 ['default'] ['default'] " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Properties of interest\n", "NODE_PROPERTIES = [\"NTP_Servers\" , \"Domain_Name\", \"VRFs\"]\n", "\n", "# Compute the difference across two snapshots and return a Pandas DataFrame\n", "node_diff = bf.q.nodeProperties(\n", " properties=\",\".join(NODE_PROPERTIES)\n", " ).answer(\n", " snapshot=\"snapshot\", \n", " reference_snapshot=\"reference\"\n", " ).frame()\n", "\n", "# Print the DataFrame\n", "show(node_diff.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The output above shows all property differences for all nodes. There is a row per node. We see that on `as1border1` the domain name has changed, and on `as1border2` the set of NTP servers has changes. There is no other difference for any other node for the chosen properties.\n", "\n", "This structured output can be transformed and fed into any type of automation, e.g., to alert you when an important property has changed. We can also generate readable drift reports using the helper function we defined above." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Differences for Node=as1border1\n", " Domain_Name: lab.local -> lab.localp\n", "\n", "Differences for Node=as1border2\n", " NTP_Servers: ['23.23.23.23', '18.18.18.18'] -> ['18.18.18.19', '18.18.18.18']\n" ] } ], "source": [ "# Print readable messages on the differences\n", "diff_properties(node_diff, \"Node\", [\"Node\"], NODE_PROPERTIES)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1B. Interface-level properties\n", "\n", "We next check if any interface-level properties have changed. We again focus on three example settings: 1) whether the interface is active, 2) description, and 3) primary IP address. The complete list of interface settings extracted by Batfish are [here](https://batfish.readthedocs.io/en/latest/notebooks/configProperties.html#Interface-Properties)." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Differences for Interface=as2border2[GigabitEthernet0/0]\n", " Active: True -> False\n", " Primary_Address: 10.23.21.2/24 -> None\n", "\n", "Differences for Interface=as2core1[GigabitEthernet0/0]\n", " Description: None -> \"To as2border1 GigabitEthernet1/0\"\n", "\n", "Differences for Interface=as2core1[GigabitEthernet1/0]\n", " Description: None -> \"To as2border2 GigabitEthernet2/0\"\n" ] } ], "source": [ "# Properties of interest\n", "INTERFACE_PROPERTIES = ['Active', 'Description', 'Primary_Address']\n", "\n", "# Compute the difference across two snapshots and return a Pandas DataFrame\n", "interface_diff = bf.q.interfaceProperties(\n", " properties=\",\".join(INTERFACE_PROPERTIES)\n", " ).answer(\n", " snapshot=\"snapshot\", \n", " reference_snapshot=\"reference\"\n", " ).frame()\n", "\n", "# Print a readable version of the differences\n", "diff_properties(interface_diff, \"Interface\", [\"Interface\"], INTERFACE_PROPERTIES)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that the interface `GigabitEthernet0/0` on `as2border2` has been shutdown and its address assignment has been eliminated. We also see that the description has been added for two interfaces on `as2core1`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1C. BGP peer properties\n", "\n", "We next check properties of BGP peers, focusing on four example properties: 1) description, 2) peer group, 3) Import policies applied to the peer, and 4) Export policies applied to the peer. The complete list of BGP peers properties is [here](https://batfish.readthedocs.io/en/latest/notebooks/configProperties.html#BGP-Peer-Configuration)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "BgpPeers only in snapshot\n", " Node=as2dept1, VRF=default, Local_Interface=None, Remote_IP=2.34.209.3\n", "\n", "Differences for Node=as2dist1, VRF=default, Local_Interface=None, Remote_IP=2.34.101.4\n", " Peer_Group: dept -> dept2\n", " Import_Policy: ['dept_to_as2dist'] -> []\n", " Export_Policy: ['as2dist_to_dept'] -> []\n" ] } ], "source": [ "# Properties of interest\n", "BGP_PEER_PROPERTIES = ['Remote_AS', 'Description', 'Peer_Group', 'Import_Policy', 'Export_Policy']\n", "\n", "# Compute the difference across two snapshots and return a Pandas DataFrame\n", "bgp_peer_diff = bf.q.bgpPeerConfiguration(\n", " properties=\",\".join(BGP_PEER_PROPERTIES)\n", " ).answer(\n", " snapshot=\"snapshot\", \n", " reference_snapshot=\"reference\"\n", " ).frame()\n", "\n", "#Print readable messages on the differences\n", "diff_properties(bgp_peer_diff, \"BgpPeer\", [\"Node\", \"VRF\", \"Local_Interface\", \"Remote_IP\"], BGP_PEER_PROPERTIES)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The output shows that a new peer has been defined on `as2dept1` with remote IP address `2.34.209.3`; and the peer group has changed for an an existing peer on `as2dist1`, which then also led to its import and export policies changing. This correlated change in import/export policies are invisible in the text diff." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Structures and references\n", "\n", "Batfish models include all structures defined in device configs (e.g., ACLs, prefix-lists) and how they are referenced in other parts of the config. You can use these models to learn if structures have been defined or deleted, which represents a major change in the configuration. \n", "\n", "#### 2A. Structures defined in configs\n", "\n", "The `definedStructures` question is the basis for learning about structures defined in the config." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
 Structure_TypeStructure_NameSource_Lines
0extended ipv4 access-list lineOUTSIDE_TO_INSIDE: permit ip any anyFileLines(filename='configs/as2border1.cfg', lines=[137])
1bgp peer-groupas2FileLines(filename='configs/as1border1.cfg', lines=[81])
2extended ipv4 access-list lineblocktelnet: deny tcp any any eq telnetFileLines(filename='configs/as2core1.cfg', lines=[124])
3interfaceGigabitEthernet1/0FileLines(filename='configs/as1core1.cfg', lines=[69, 70, 71])
4extended ipv4 access-listOUTSIDE_TO_INSIDEFileLines(filename='configs/as2border2.cfg', lines=[132, 133, 134])
\n" ], "text/plain": [ " Structure_Type Structure_Name \\\n", "0 extended ipv4 access-list line OUTSIDE_TO_INSIDE: permit ip any any \n", "1 bgp peer-group as2 \n", "2 extended ipv4 access-list line blocktelnet: deny tcp any any eq telnet \n", "3 interface GigabitEthernet1/0 \n", "4 extended ipv4 access-list OUTSIDE_TO_INSIDE \n", "\n", " Source_Lines \n", "0 configs/as2border1.cfg:[137] \n", "1 configs/as1border1.cfg:[81] \n", "2 configs/as2core1.cfg:[124] \n", "3 configs/as1core1.cfg:[69, 70, 71] \n", "4 configs/as2border2.cfg:[132, 133, 134] " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Extract defined structures from both snapshots as a Pandas DataFrame\n", "snapshot_structures = bf.q.definedStructures().answer(snapshot=\"snapshot\").frame()\n", "reference_structures = bf.q.definedStructures().answer(snapshot=\"reference\").frame()\n", "\n", "# Show me what the information looks like by printing the first few rows\n", "show(snapshot_structures.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The output snippet shows how Batfish captures the exact lines in each file where each structure is defined. We can process this information from the two snapshots to produce a report on all differences." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "DefinedStructures only in snapshot\n", " File_Name=configs/as3border1.cfg, Structure_Name=bogons, Structure_Type=ipv4 prefix-list\n", " File_Name=configs/as2dist1.cfg, Structure_Name=dept2, Structure_Type=bgp peer-group\n", " File_Name=configs/as2dist1.cfg, Structure_Name=dept_to_as2dist 200, Structure_Type=route-map-clause\n", " File_Name=configs/as2dist2.cfg, Structure_Name=105: permit ip host 3.0.3.0 host 255.255.255.0, Structure_Type=extended ipv4 access-list line\n", " File_Name=configs/as2dist1.cfg, Structure_Name=102: permit tcp host 2.128.0.0 host 255.255.0.0, Structure_Type=extended ipv4 access-list line\n", "\n", "DefinedStructures only in reference\n", " File_Name=configs/as2dist1.cfg, Structure_Name=dept, Structure_Type=bgp peer-group\n" ] } ], "source": [ "# Remove the line numbers but keep the filename. We don't care about where in the file structure are defined.\n", "snapshot_structures_without_lines = snapshot_structures[['Structure_Type', 'Structure_Name']].assign(\n", " File_Name=snapshot_structures[\"Source_Lines\"].map(lambda x: x.filename))\n", "reference_structures_without_lines = reference_structures[['Structure_Type', 'Structure_Name']].assign(\n", " File_Name=reference_structures[\"Source_Lines\"].map(lambda x: x.filename))\n", "\n", "# Print a readable message on the differences\n", "diff_frames(snapshot_structures_without_lines, \n", " reference_structures_without_lines, \n", " \"DefinedStructure\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can easily see in this output that a BGP peer group named `dept2` was newly defined on `as2dist1` and a prefix-list named `bogons` was defined on as2border1. We also see that the peer group named `dept` was removed from `as2dist1`. The peer group change is related to what we saw earlier with a peer property changing. This view shows that the entire structure has been removed and defined." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 2B. Undefined structure references\n", "\n", "References to undefined structures are symptoms of configuration errors. Using the `undefinedReferences` question, Batfish can help you understand if new undefined references have been introduced or old ones have been cleared. " ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
 File_NameStruct_TypeRef_NameContextLines
0configs/as2core2.cfgroute-mapfilter-bogonsbgp inbound route-mapFileLines(filename='configs/as2core2.cfg', lines=[110])
1configs/as2dist1.cfgcommunity-listdept_community_newroute-map match community-listFileLines(filename='configs/as2dist1.cfg', lines=[133])
2configs/as2dist1.cfgundeclared bgp peer-groupdeptbgp peer-group referenced before definedFileLines(filename='configs/as2dist1.cfg', lines=[99, 100, 101])
\n" ], "text/plain": [ " File_Name Struct_Type Ref_Name \\\n", "0 configs/as2core2.cfg route-map filter-bogons \n", "1 configs/as2dist1.cfg community-list dept_community_new \n", "2 configs/as2dist1.cfg undeclared bgp peer-group dept \n", "\n", " Context \\\n", "0 bgp inbound route-map \n", "1 route-map match community-list \n", "2 bgp peer-group referenced before defined \n", "\n", " Lines \n", "0 configs/as2core2.cfg:[110] \n", "1 configs/as2dist1.cfg:[133] \n", "2 configs/as2dist1.cfg:[99, 100, 101] " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Extract undefined references from both snapshots as a Pandas DataFrame\n", "snapshot_undefined_references=bf.q.undefinedReferences().answer(snapshot=\"snapshot\").frame()\n", "reference_undefined_references= bf.q.undefinedReferences().answer(snapshot=\"reference\").frame()\n", "\n", "# Show me all undefined references in the snapshot\n", "show(snapshot_undefined_references)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The output shows that there are three undefined references in the snapshot. Let us find out which ones were newly introduced relative to the reference." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "UndefinedRefeferences only in snapshot\n", " Ref_Name=dept_community_new, File_Name=configs/as2dist1.cfg, Struct_Type=community-list, Context=route-map match community-list\n", " Ref_Name=dept, File_Name=configs/as2dist1.cfg, Struct_Type=undeclared bgp peer-group, Context=bgp peer-group referenced before defined\n" ] } ], "source": [ "# Remove Lines since we don't care about where it was referenced\n", "snapshot_undefined_references_without_lines = snapshot_undefined_references.drop(columns=['Lines'])\n", "reference_undefined_references_without_lines = reference_undefined_references.drop(columns=['Lines'])\n", "\n", "# Print a readable message on the differences\n", "diff_frames(snapshot_undefined_references_without_lines, \n", " reference_undefined_references_without_lines, \n", " \"UndefinedRefeference\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We thus see that, of the three undefined references that we saw earlier, two were newly introduced and one exists in both snapshots. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Network behavior\n", "\n", "We now turn our attention to behavioral differences between network snapshots, starting with changes in BGP adjacencies.\n", "\n", "#### 3A. BGP adjacencies\n", "\n", "The `bgpEdges` question of Batfish enables you to learn about all BGP adjacencines in the network, as follows." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
 NodeIPInterfaceAS_NumberRemote_NodeRemote_IPRemote_InterfaceRemote_AS_Number
0as1border21.2.2.2None1as1core11.10.1.1None1
1as1core11.10.1.1None1as1border11.1.1.1None1
2as2dist22.1.3.2None2as2core22.1.2.2None2
3as3border23.2.2.2None3as3core13.10.1.1None3
4as2dist22.34.201.3None2as2dept12.34.201.4None65001
\n" ], "text/plain": [ " Node IP Interface AS_Number Remote_Node Remote_IP \\\n", "0 as1border2 1.2.2.2 None 1 as1core1 1.10.1.1 \n", "1 as1core1 1.10.1.1 None 1 as1border1 1.1.1.1 \n", "2 as2dist2 2.1.3.2 None 2 as2core2 2.1.2.2 \n", "3 as3border2 3.2.2.2 None 3 as3core1 3.10.1.1 \n", "4 as2dist2 2.34.201.3 None 2 as2dept1 2.34.201.4 \n", "\n", " Remote_Interface Remote_AS_Number \n", "0 None 1 \n", "1 None 1 \n", "2 None 2 \n", "3 None 3 \n", "4 None 65001 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Get the edges from both snapshots as Pandas DataFrames\n", "snapshot_bgp_edges = bf.q.bgpEdges().answer(snapshot=\"snapshot\").frame()\n", "reference_bgp_edges = bf.q.bgpEdges().answer(snapshot=\"reference\").frame()\n", "\n", "# Show me the schema by printing the first few rows\n", "show(snapshot_bgp_edges.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that Batfish knows which BGP edges in the snapshot come up and shows key information about them. We can use the answer to this question to learn which edges exist only in the snapshot or only in the refrence." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "BgpEdges only in reference\n", " Node=as2border2, Remote_Node=as3border1\n" ] } ], "source": [ "# Retain only columns we care about for this analysis\n", "snapshot_bgp_edges_nodes = snapshot_bgp_edges[['Node', 'Remote_Node']]\n", "reference_bgp_edges_nodes = reference_bgp_edges[['Node', 'Remote_Node']]\n", "\n", "# DataFrames contain one edge per direction; keep only one direction\n", "snapshot_bgp_bidir_edges_nodes = snapshot_bgp_edges_nodes[\n", " snapshot_bgp_edges_nodes['Node'] < snapshot_bgp_edges_nodes['Remote_Node']\n", " ]\n", "reference_bgp_bidir_edges_nodes = reference_bgp_edges_nodes[\n", " reference_bgp_edges_nodes['Node'] < reference_bgp_edges_nodes['Remote_Node']\n", " ]\n", "\n", "# Print a readable message on the differences\n", "diff_frames(snapshot_bgp_bidir_edges_nodes, \n", " reference_bgp_bidir_edges_nodes, \n", " \"BgpEdge\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One BGP edge exists only in the reference, that is, it disappeared in the snapshot. We can find more about this edge, like so:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
 NodeIPInterfaceAS_NumberRemote_NodeRemote_IPRemote_InterfaceRemote_AS_Number
20as2border210.23.21.2None2as3border110.23.21.3None3
\n" ], "text/plain": [ " Node IP Interface AS_Number Remote_Node Remote_IP \\\n", "20 as2border2 10.23.21.2 None 2 as3border1 10.23.21.3 \n", "\n", " Remote_Interface Remote_AS_Number \n", "20 None 3 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Find the matching edge in the reference edges answer from before\n", "missing_snapshot_edge = reference_bgp_edges[\n", " (reference_bgp_edges['Node']==\"as2border2\") \n", " & (reference_bgp_edges['Remote_Node']==\"as3border1\")\n", " ]\n", "\n", "# Print the edge information\n", "show(missing_snapshot_edge)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Do you recall the interface on as2border2 that was shut earlier? This BGP edge was removed because of that interface shutdown (which you confirm using IP of the interface---`10.23.21.2/24`)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3B. ACL behavior\n", "\n", "To compute the behavior differences between ACLs, we use the [compare filters question](https://pybatfish.readthedocs.io/en/latest/notebooks/differentialQuestions.html#Compare-Filters). It returns pairs of lines, one from the filter definition in each snapshot, that match the same flow(s) but treat them differently (i.e. one permits and the other denies the flow)." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
 NodeFilter_NameLine_IndexLine_ContentLine_ActionReference_Line_IndexReference_Line_Content
0as2dist21054permit ip host 3.0.3.0 host 255.255.255.0PERMITEnd of ACL
\n" ], "text/plain": [ " Node Filter_Name Line_Index Line_Content \\\n", "0 as2dist2 105 4 permit ip host 3.0.3.0 host 255.255.255.0 \n", "\n", " Line_Action Reference_Line_Index Reference_Line_Content \n", "0 PERMIT End of ACL " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# compute behavior differences between ACLs\n", "compare_filters = bf.q.compareFilters().answer(\n", " snapshot='snapshot',\n", " reference_snapshot='reference'\n", " ).frame()\n", "\n", "# print the result\n", "show(compare_filters)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that the only difference in the ACL behaviors of the two snapshots is for ACL `105` on `as2dist`. Line `permit ip host 3.0.3.0 host 255.255.255.0` in the snapshot permits some flows that were being denied in the reference snapshhot because of the implicit deny at the end of the ACL. Thus, we have permitted flows that were not being permitted before.\n", "\n", "If you were paying attention to the text diff above, the result above may surprise you. The text diff (relevant snippet repeated below) showed that ACL `102` on `as2dist1` changed as well." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@ -113,6 +113,7 @@\r\n", " no ip http server\r\n", " no ip http secure-server\r\n", " !\r\n", "+access-list 102 permit tcp host 2.128.0.0 host 255.255.0.0\r\n", " access-list 102 permit ip host 2.128.0.0 host 255.255.0.0\r\n", " access-list 105 permit ip host 1.0.1.0 host 255.255.255.0\r\n", " access-list 105 permit ip host 1.0.2.0 host 255.255.255.0\r\n" ] } ], "source": [ "!diff -ur networks/drift/reference/configs/as2dist1.cfg networks/drift/snapshot/configs/as2dist1.cfg | grep -A 7 '@@ -113,6 +113,7 @@'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You may have expected a behahvior diff corresponding to this change, but Batfish analysis reveals that that didn't happen. The added line is permitting TCP traffic between two hosts for which IP traffic was already permitted, so no new traffic was permitted. So, either this change was unnecessary or someone mistyped the host addresses. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Summary\n", "\n", "Batfish enables you to easily understand how your device configs differ from a historial reference or golden versions. It provides structured information about not only changes to settings in configs but also about changes in network behavior. This information provides important context beyond simple text diffs and can be inserted into an automated pipeline that alerts on important changes. \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "### Get involved with the Batfish community\n", "\n", "Join our community on [Slack](https://join.slack.com/t/batfish-org/shared_invite/enQtMzA0Nzg2OTAzNzQ1LTcyYzY3M2Q0NWUyYTRhYjdlM2IzYzRhZGU1NWFlNGU2MzlhNDY3OTJmMDIyMjQzYmRlNjhkMTRjNWIwNTUwNTQ) and [GitHub](https://github.com/batfish/batfish). " ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.15" } }, "nbformat": 4, "nbformat_minor": 4 }