{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Getting Started with Batfish\n", "\n", "This notebook uses pybatfish, a Python-based SDK for Batfish, to analyze a sample network. It shows how to submit your configurations and other network data for analysis and how to query its vendor-neutral network model. [Other notebooks](https://github.com/batfish/pybatfish/tree/master/jupyter_notebooks) show how to use Batfish for different types of network validation tasks. \n", "\n", "Check out a video demo of an earlier version of this notebook [here](https://www.youtube.com/watch?v=Ca7kPAtfFqo).\n", "\n", "### Initializing a Network and Snapshot\n", "\n", "A *network* is a logical group of routers and links. It can be your entire network or a subset of it. A *snapshot* is a collection of information (configuration files, routing data, up/down status of nodes and links) that represent the network state. Snapshots can contain the actual state of the network or candidate states (e.g, those corresponding to a planned change) that you want to analyze." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# Import packages\n", "%run startup.py\n", "bf = Session(host=\"localhost\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`SNAPSHOT_PATH` below can be updated to point to a custom snapshot directory, see the [Batfish instructions](https://github.com/batfish/batfish/wiki/Packaging-snapshots-for-analysis) for how to package data for analysis.
\n", "More example networks are available in the [networks](https://github.com/batfish/batfish/tree/master/networks) folder of the Batfish repository." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'example_snapshot'" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Assign a friendly name to your network and snapshot\n", "NETWORK_NAME = \"example_network\"\n", "SNAPSHOT_NAME = \"example_snapshot\"\n", "\n", "SNAPSHOT_PATH = \"networks/example\"\n", "\n", "# Now create the network and initialize the snapshot\n", "bf.set_network(NETWORK_NAME)\n", "bf.init_snapshot(SNAPSHOT_PATH, name=SNAPSHOT_NAME, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you used the example we provided, the network you initialized above is illustrated below. You can download/view devices' configuration files [here](https://github.com/batfish/pybatfish/tree/master/jupyter_notebooks/networks/example).\n", "\n", "![example-network](https://raw.githubusercontent.com/batfish/pybatfish/master/jupyter_notebooks/networks/example/example-network.png)\n", "\n", "***" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Querying the Network Model\n", "\n", "Batfish creates a comprehensive vendor-neutral device and network model which can be queried for information about devices, interfaces, VRFs, routes, etc. It offers a set of *questions* to query this model." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# You can also use tab-completion on the Batfish question module - bf.q. -> press TAB key,\n", "# uncomment and try on the following line\n", "# bf.q.\n", "\n", "# In IPython and Jupyter you can use the \"?\" shorthand to get help on a question\n", "?bf.q.nodeProperties\n", "\n", "# help(bf.q.nodeProperties) # in standard Python console" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "\n", "### Getting status of parsed files\n", "\n", "Batfish may ignore certain lines in the configuration. To retrieve the parsing status of snapshot files, use the `fileParseStatus()` question." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "parse_status = bf.q.fileParseStatus().answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`answer()` runs the question and returns the answer in a JSON format. \n", "\n", "`frame()` wraps the answer as [pandas dataframe](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html)." ] }, { "cell_type": "code", "execution_count": 5, "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", " \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", "
File_NameStatusFile_FormatNodes
0configs/as1border1.cfgPASSEDCISCO_IOS['as1border1']
1configs/as1border2.cfgPASSEDCISCO_IOS['as1border2']
2configs/as1core1.cfgPASSEDCISCO_IOS['as1core1']
3configs/as2border1.cfgPASSEDCISCO_IOS['as2border1']
4configs/as2border2.cfgPASSEDCISCO_IOS['as2border2']
5configs/as2core1.cfgPASSEDCISCO_IOS['as2core1']
6configs/as2core2.cfgPASSEDCISCO_IOS['as2core2']
7configs/as2dept1.cfgPASSEDCISCO_IOS['as2dept1']
8configs/as2dist1.cfgPASSEDCISCO_IOS['as2dist1']
9configs/as2dist2.cfgPASSEDCISCO_IOS['as2dist2']
10configs/as3border1.cfgPASSEDCISCO_IOS['as3border1']
11configs/as3border2.cfgPASSEDCISCO_IOS['as3border2']
12configs/as3core1.cfgPASSEDCISCO_IOS['as3core1']
13hosts/host1.jsonPASSEDHOST['host1']
14hosts/host2.jsonPASSEDHOST['host2']
15iptables/host1.iptablesPASSEDIPTABLES['iptables/host1.iptables']
16iptables/host2.iptablesPASSEDIPTABLES['iptables/host2.iptables']
\n", "
" ], "text/plain": [ " File_Name Status File_Format Nodes\n", "0 configs/as1border1.cfg PASSED CISCO_IOS ['as1border1']\n", "1 configs/as1border2.cfg PASSED CISCO_IOS ['as1border2']\n", "2 configs/as1core1.cfg PASSED CISCO_IOS ['as1core1']\n", "3 configs/as2border1.cfg PASSED CISCO_IOS ['as2border1']\n", "4 configs/as2border2.cfg PASSED CISCO_IOS ['as2border2']\n", "5 configs/as2core1.cfg PASSED CISCO_IOS ['as2core1']\n", "6 configs/as2core2.cfg PASSED CISCO_IOS ['as2core2']\n", "7 configs/as2dept1.cfg PASSED CISCO_IOS ['as2dept1']\n", "8 configs/as2dist1.cfg PASSED CISCO_IOS ['as2dist1']\n", "9 configs/as2dist2.cfg PASSED CISCO_IOS ['as2dist2']\n", "10 configs/as3border1.cfg PASSED CISCO_IOS ['as3border1']\n", "11 configs/as3border2.cfg PASSED CISCO_IOS ['as3border2']\n", "12 configs/as3core1.cfg PASSED CISCO_IOS ['as3core1']\n", "13 hosts/host1.json PASSED HOST ['host1']\n", "14 hosts/host2.json PASSED HOST ['host2']\n", "15 iptables/host1.iptables PASSED IPTABLES ['iptables/host1.iptables']\n", "16 iptables/host2.iptables PASSED IPTABLES ['iptables/host2.iptables']" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# View the parse status results\n", "parse_status" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Additional post-processing can be done on this data, like filtering for values in one or multiple columns, reducing the number of columns, etc. using pandas. We show a few examples of Pandas filtering below, some more filtering examples for Batfish answers are [here](https://github.com/batfish/pybatfish/tree/master/jupyter_notebooks/Pandas%20Examples.ipynb), and a general tutorial is [here](http://nbviewer.jupyter.org/github/jvns/pandas-cookbook/blob/v0.2/cookbook/Chapter%203%20-%20Which%20borough%20has%20the%20most%20noise%20complaints%20%28or%2C%20more%20selecting%20data%29.ipynb)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
File_NameStatusFile_FormatNodes
\n", "
" ], "text/plain": [ "Empty DataFrame\n", "Columns: [File_Name, Status, File_Format, Nodes]\n", "Index: []" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# An example: use a filter on the returned dataframe to see which files failed to parse completely\n", "parse_status[parse_status['Status'] != 'PASSED'] # change '!=' to '==' to get the files which passed" ] }, { "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", "
FilenameLineTextParser_ContextComment
\n", "
" ], "text/plain": [ "Empty DataFrame\n", "Columns: [Filename, Line, Text, Parser_Context, Comment]\n", "Index: []" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# View details if some of the files were not parsed completely\n", "bf.q.parseWarning().answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "### Extracting properties of network entities\n", "Entities in the network refer to things like nodes, interfaces, routing processes, and VRFs. Batfish makes it trivial to extract configured properties of such entities in a vendor neutral manner. \n", "\n", "##### Node properties\n", "The nodeProperties question extracts information on nodes in the snapshot." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# Extract the properties of all nodes whose names contain 'border'\n", "node_properties = bf.q.nodeProperties(nodes=\"/border/\").answer().frame()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Index(['Node', 'AS_Path_Access_Lists', 'Authentication_Key_Chains',\n", " 'Community_Match_Exprs', 'Community_Set_Exprs',\n", " 'Community_Set_Match_Exprs', 'Community_Sets', 'Configuration_Format',\n", " 'DNS_Servers', 'DNS_Source_Interface', 'Default_Cross_Zone_Action',\n", " 'Default_Inbound_Action', 'Domain_Name', 'Hostname', 'IKE_Phase1_Keys',\n", " 'IKE_Phase1_Policies', 'IKE_Phase1_Proposals', 'IP6_Access_Lists',\n", " 'IP_Access_Lists', 'IPsec_Peer_Configs', 'IPsec_Phase2_Policies',\n", " 'IPsec_Phase2_Proposals', 'Interfaces', 'Logging_Servers',\n", " 'Logging_Source_Interface', 'NTP_Servers', 'NTP_Source_Interface',\n", " 'PBR_Policies', 'Route6_Filter_Lists', 'Route_Filter_Lists',\n", " 'Routing_Policies', 'SNMP_Source_Interface', 'SNMP_Trap_Servers',\n", " 'TACACS_Servers', 'TACACS_Source_Interface', 'VRFs', 'Zones'],\n", " dtype='object')" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# View what columns (properties) are present in the answer\n", "node_properties.columns" ] }, { "cell_type": "code", "execution_count": 10, "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", "
NodeDomain_NameInterfacesNTP_Servers
0as2border2lab.local['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'GigabitEthernet2/0', 'Loopback0']['18.18.18.18']
1as3border1lab.local['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'Loopback0']['18.18.18.18', '23.23.23.23']
2as3border2lab.local['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'Loopback0']['18.18.18.18', '23.23.23.23']
3as1border2lab.local['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'GigabitEthernet2/0', 'Loopback0']['18.18.18.18', '23.23.23.23']
4as2border1lab.local['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'GigabitEthernet2/0', 'Loopback0']['18.18.18.18', '23.23.23.23']
5as1border1lab.local['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'Loopback0'][]
\n", "
" ], "text/plain": [ " Node Domain_Name \\\n", "0 as2border2 lab.local \n", "1 as3border1 lab.local \n", "2 as3border2 lab.local \n", "3 as1border2 lab.local \n", "4 as2border1 lab.local \n", "5 as1border1 lab.local \n", "\n", " Interfaces \\\n", "0 ['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'GigabitEthernet2/0', 'Loopback0'] \n", "1 ['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'Loopback0'] \n", "2 ['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'Loopback0'] \n", "3 ['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'GigabitEthernet2/0', 'Loopback0'] \n", "4 ['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'GigabitEthernet2/0', 'Loopback0'] \n", "5 ['Ethernet0/0', 'GigabitEthernet0/0', 'GigabitEthernet1/0', 'Loopback0'] \n", "\n", " NTP_Servers \n", "0 ['18.18.18.18'] \n", "1 ['18.18.18.18', '23.23.23.23'] \n", "2 ['18.18.18.18', '23.23.23.23'] \n", "3 ['18.18.18.18', '23.23.23.23'] \n", "4 ['18.18.18.18', '23.23.23.23'] \n", "5 [] " ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# To extract only a subset of properties, use the properties parameter\n", "bf.q.nodeProperties(nodes=\"/border/\", properties=\"Domain_Name,NTP_Servers,Interfaces\").answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Interface properties\n", "To retrieve information about interfaces and the properties of them, use the **interfaceProperties** question" ] }, { "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", " \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", "
InterfaceBandwidthPrimary_AddressVRF
0as1border1[Loopback0]8000000000.01.1.1.1/32default
1as1border2[Loopback0]8000000000.01.2.2.2/32default
2as1core1[Loopback0]8000000000.01.10.1.1/32default
3as2border1[Loopback0]8000000000.02.1.1.1/32default
4as2border2[Loopback0]8000000000.02.1.1.2/32default
5as2core1[Loopback0]8000000000.02.1.2.1/32default
6as2core2[Loopback0]8000000000.02.1.2.2/32default
7as2dept1[Loopback0]8000000000.02.1.1.2/32default
8as2dist1[Loopback0]8000000000.02.1.3.1/32default
9as2dist2[Loopback0]8000000000.02.1.3.2/32default
10as3border1[Loopback0]8000000000.03.1.1.1/32default
11as3border2[Loopback0]8000000000.03.2.2.2/32default
12as3core1[Loopback0]8000000000.03.10.1.1/32default
\n", "
" ], "text/plain": [ " Interface Bandwidth Primary_Address VRF\n", "0 as1border1[Loopback0] 8000000000.0 1.1.1.1/32 default\n", "1 as1border2[Loopback0] 8000000000.0 1.2.2.2/32 default\n", "2 as1core1[Loopback0] 8000000000.0 1.10.1.1/32 default\n", "3 as2border1[Loopback0] 8000000000.0 2.1.1.1/32 default\n", "4 as2border2[Loopback0] 8000000000.0 2.1.1.2/32 default\n", "5 as2core1[Loopback0] 8000000000.0 2.1.2.1/32 default\n", "6 as2core2[Loopback0] 8000000000.0 2.1.2.2/32 default\n", "7 as2dept1[Loopback0] 8000000000.0 2.1.1.2/32 default\n", "8 as2dist1[Loopback0] 8000000000.0 2.1.3.1/32 default\n", "9 as2dist2[Loopback0] 8000000000.0 2.1.3.2/32 default\n", "10 as3border1[Loopback0] 8000000000.0 3.1.1.1/32 default\n", "11 as3border2[Loopback0] 8000000000.0 3.2.2.2/32 default\n", "12 as3core1[Loopback0] 8000000000.0 3.10.1.1/32 default" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Fetch specific properties of Loopback interfaces\n", "bf.q.interfaceProperties(interfaces=\"/loopback/\", properties=\"Bandwidth,VRF,Primary_Address\").answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similar questions extract properties of other entities (e.g., `bgpProcessConfiguration()` extracts properties of BGP processes)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "### Inspecting referential integrity of configuration structures\n", "Network configuratons define and reference named structures like route maps, access control lists (ACLs), prefix lists, etc. Two common indicators of buggy configurations include references to structures that are not defined anywhere (which can lead to disastrous consequences on some platforms) or defined structures that are not referenced anywhere. Batfish makes it easy to flag such instances because it understand the underlying semantics of configuration." ] }, { "cell_type": "code", "execution_count": 12, "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", "
File_NameStruct_TypeRef_NameContextLines
0configs/as2core2.cfgroute-mapfilter-bogonsbgp inbound route-mapconfigs/as2core2.cfg:[110]
\n", "
" ], "text/plain": [ " File_Name Struct_Type Ref_Name Context \\\n", "0 configs/as2core2.cfg route-map filter-bogons bgp inbound route-map \n", "\n", " Lines \n", "0 configs/as2core2.cfg:[110] " ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# List references to undefined structures\n", "bf.q.undefinedReferences().answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The question for listing any unused structures is `unusedStructures()`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "### Inspecting topologies\n", "Nodes in a network form multiple types of topologies that are defined by edges at layer 3 (IP layer) or by routing protocols such as BGP or OSPF. Batfish has questions that return such edges. These questions take `nodes` and `remoteNodes` parameters that can limit the output to a subset of the nodes." ] }, { "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", "
InterfaceIPsRemote_InterfaceRemote_IPs
0as1border1[GigabitEthernet0/0]['1.0.1.1']as1core1[GigabitEthernet1/0]['1.0.1.2']
1as1border1[GigabitEthernet1/0]['10.12.11.1']as2border1[GigabitEthernet0/0]['10.12.11.2']
\n", "
" ], "text/plain": [ " Interface IPs \\\n", "0 as1border1[GigabitEthernet0/0] ['1.0.1.1'] \n", "1 as1border1[GigabitEthernet1/0] ['10.12.11.1'] \n", "\n", " Remote_Interface Remote_IPs \n", "0 as1core1[GigabitEthernet1/0] ['1.0.1.2'] \n", "1 as2border1[GigabitEthernet0/0] ['10.12.11.2'] " ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get layer 3 edges\n", "bf.q.layer3Edges(nodes=\"as1border1\").answer().frame()" ] }, { "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
NodeIPInterfaceAS_NumberRemote_NodeRemote_IPRemote_InterfaceRemote_AS_Number
0as1border110.12.11.1None1as2border110.12.11.2None2
1as1border11.1.1.1None1as1core11.10.1.1None1
\n", "
" ], "text/plain": [ " Node IP Interface AS_Number Remote_Node Remote_IP \\\n", "0 as1border1 10.12.11.1 None 1 as2border1 10.12.11.2 \n", "1 as1border1 1.1.1.1 None 1 as1core1 1.10.1.1 \n", "\n", " Remote_Interface Remote_AS_Number \n", "0 None 2 \n", "1 None 1 " ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get BGP edges \n", "bf.q.bgpEdges(nodes=\"as1border1\").answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "### Exploring Routing and Forwarding\n", "Batfish computes routing and forwarding tables (aka RIBs and FIBs) of the network from snapshot data itself. These tables can be examined to understand the routing and forwarding behavior of the network. \n", "\n", "One way to examine this behavior is using a virtual traceroute. Unlike the live-network traceroute, Batfish shows all possible flow paths in the network and identifies routing entries that cause each hop to be taken." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
 FlowTracesTraceCount
0Start Location: host1
Src IP: 2.128.0.101
Src Port: 49152
Dst IP: 1.0.2.2
Dst Port: 33434
IP Protocol: UDP
ACCEPTED
1. node: host1
  ORIGINATED(default)
  FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)])
  PERMITTED(filter::OUTPUT (EGRESS_FILTER))
  TRANSMITTED(eth0)
2. node: as2dept1
  RECEIVED(GigabitEthernet2/0)
  PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER))
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.34.101.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.101.3)])
  TRANSMITTED(GigabitEthernet0/0)
3. node: as2dist1
  RECEIVED(GigabitEthernet2/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.23.11.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet0/0)
4. node: as2core1
  RECEIVED(GigabitEthernet2/0)
  PERMITTED(blocktelnet (INGRESS_FILTER))
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.12.11.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet0/0)
5. node: as2border1
  RECEIVED(GigabitEthernet1/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER))
  TRANSMITTED(GigabitEthernet0/0)
6. node: as1border1
  RECEIVED(GigabitEthernet1/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)])
  TRANSMITTED(GigabitEthernet0/0)
7. node: as1core1
  RECEIVED(GigabitEthernet1/0)
  ACCEPTED(GigabitEthernet0/0)

ACCEPTED
1. node: host1
  ORIGINATED(default)
  FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)])
  PERMITTED(filter::OUTPUT (EGRESS_FILTER))
  TRANSMITTED(eth0)
2. node: as2dept1
  RECEIVED(GigabitEthernet2/0)
  PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER))
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.34.101.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.101.3)])
  TRANSMITTED(GigabitEthernet0/0)
3. node: as2dist1
  RECEIVED(GigabitEthernet2/0)
  FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.23.21.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet1/0)
4. node: as2core2
  RECEIVED(GigabitEthernet3/0)
  FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.12.12.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet1/0)
5. node: as2border1
  RECEIVED(GigabitEthernet2/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER))
  TRANSMITTED(GigabitEthernet0/0)
6. node: as1border1
  RECEIVED(GigabitEthernet1/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)])
  TRANSMITTED(GigabitEthernet0/0)
7. node: as1core1
  RECEIVED(GigabitEthernet1/0)
  ACCEPTED(GigabitEthernet0/0)

ACCEPTED
1. node: host1
  ORIGINATED(default)
  FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)])
  PERMITTED(filter::OUTPUT (EGRESS_FILTER))
  TRANSMITTED(eth0)
2. node: as2dept1
  RECEIVED(GigabitEthernet2/0)
  PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER))
  FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.34.201.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.201.3)])
  TRANSMITTED(GigabitEthernet1/0)
3. node: as2dist2
  RECEIVED(GigabitEthernet2/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.23.22.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet0/0)
4. node: as2core2
  RECEIVED(GigabitEthernet2/0)
  FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.12.12.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet1/0)
5. node: as2border1
  RECEIVED(GigabitEthernet2/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER))
  TRANSMITTED(GigabitEthernet0/0)
6. node: as1border1
  RECEIVED(GigabitEthernet1/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)])
  TRANSMITTED(GigabitEthernet0/0)
7. node: as1core1
  RECEIVED(GigabitEthernet1/0)
  ACCEPTED(GigabitEthernet0/0)

ACCEPTED
1. node: host1
  ORIGINATED(default)
  FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)])
  PERMITTED(filter::OUTPUT (EGRESS_FILTER))
  TRANSMITTED(eth0)
2. node: as2dept1
  RECEIVED(GigabitEthernet2/0)
  PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER))
  FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.34.201.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.201.3)])
  TRANSMITTED(GigabitEthernet1/0)
3. node: as2dist2
  RECEIVED(GigabitEthernet2/0)
  FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.23.12.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet1/0)
4. node: as2core1
  RECEIVED(GigabitEthernet3/0)
  PERMITTED(blocktelnet (INGRESS_FILTER))
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.12.11.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  TRANSMITTED(GigabitEthernet0/0)
5. node: as2border1
  RECEIVED(GigabitEthernet1/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)])
  PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER))
  TRANSMITTED(GigabitEthernet0/0)
6. node: as1border1
  RECEIVED(GigabitEthernet1/0)
  FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)])
  TRANSMITTED(GigabitEthernet0/0)
7. node: as1core1
  RECEIVED(GigabitEthernet1/0)
  ACCEPTED(GigabitEthernet0/0)
4
\n" ], "text/plain": [ " Flow \\\n", "0 start=host1 [2.128.0.101:49152->1.0.2.2:33434 UDP] \n", "\n", " Traces \\\n", "0 [((ORIGINATED(default), FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)]), PERMITTED(filter::OUTPUT (EGRESS_FILTER)), TRANSMITTED(eth0)), (RECEIVED(GigabitEthernet2/0), PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER)), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.34.101.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.101.3)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet2/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.23.11.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet2/0), PERMITTED(blocktelnet (INGRESS_FILTER)), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.12.11.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER)), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), ACCEPTED(GigabitEthernet0/0))), ((ORIGINATED(default), FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)]), PERMITTED(filter::OUTPUT (EGRESS_FILTER)), TRANSMITTED(eth0)), (RECEIVED(GigabitEthernet2/0), PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER)), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.34.101.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.101.3)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet2/0), FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.23.21.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet1/0)), (RECEIVED(GigabitEthernet3/0), FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.12.12.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet1/0)), (RECEIVED(GigabitEthernet2/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER)), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), ACCEPTED(GigabitEthernet0/0))), ((ORIGINATED(default), FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)]), PERMITTED(filter::OUTPUT (EGRESS_FILTER)), TRANSMITTED(eth0)), (RECEIVED(GigabitEthernet2/0), PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER)), FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.34.201.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.201.3)]), TRANSMITTED(GigabitEthernet1/0)), (RECEIVED(GigabitEthernet2/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.23.22.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet2/0), FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.12.12.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet1/0)), (RECEIVED(GigabitEthernet2/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER)), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), ACCEPTED(GigabitEthernet0/0))), ((ORIGINATED(default), FORWARDED(Forwarded out interface: eth0 with resolved next-hop IP: 2.128.0.1, Routes: [static (Network: 0.0.0.0/0, Next Hop: interface eth0 ip 2.128.0.1)]), PERMITTED(filter::OUTPUT (EGRESS_FILTER)), TRANSMITTED(eth0)), (RECEIVED(GigabitEthernet2/0), PERMITTED(RESTRICT_HOST_TRAFFIC_IN (INGRESS_FILTER)), FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.34.201.3, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 2.34.201.3)]), TRANSMITTED(GigabitEthernet1/0)), (RECEIVED(GigabitEthernet2/0), FORWARDED(Forwarded out interface: GigabitEthernet1/0 with resolved next-hop IP: 2.23.12.2, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet1/0)), (RECEIVED(GigabitEthernet3/0), PERMITTED(blocktelnet (INGRESS_FILTER)), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 2.12.11.1, Routes: [ibgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 10.12.11.1, Routes: [bgp (Network: 1.0.2.0/24, Next Hop: ip 10.12.11.1)]), PERMITTED(INSIDE_TO_AS1 (EGRESS_FILTER)), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), FORWARDED(Forwarded out interface: GigabitEthernet0/0 with resolved next-hop IP: 1.0.1.2, Routes: [ospf (Network: 1.0.2.0/24, Next Hop: interface GigabitEthernet0/0 ip 1.0.1.2)]), TRANSMITTED(GigabitEthernet0/0)), (RECEIVED(GigabitEthernet1/0), ACCEPTED(GigabitEthernet0/0)))] \n", "\n", " TraceCount \n", "0 4 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Do a traceroute from host1 to 1.0.2.2\n", "tr_frame = bf.q.traceroute(startLocation=\"host1\", headers=HeaderConstraints(dstIps=\"1.0.2.2\")).answer().frame()\n", "\n", "# Display results using customizations to handle large string values\n", "\n", "show(tr_frame)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Another way to understand the routing behavior in detail is to examine the routing tables directly. " ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "# Fetch the routing table of all VRFs on all nodes in the snapshot\n", "routes_all = bf.q.routes().answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(For a large network, the first time you run a question that needs the dataplane, fetching the answer can take a few minutes. Subsequent questions are quick as the generated dataplane is saved by Batfish.)\n", "\n", "As used above, the routes() question can generate a lot of results. You may restrict the output using parameters to the question---to restrict the results to **core** routers, use **nodes = \"/core/\"**, and to restrict results to the prefix **90.90.90.0/24**, use **network=90.90.90.0/24\". " ] }, { "cell_type": "code", "execution_count": 17, "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", "
NodeVRFNetworkNext_HopNext_Hop_IPNext_Hop_InterfaceProtocolMetricAdmin_DistanceTag
0as3core1default90.90.90.0/24interface GigabitEthernet2/0AUTO/NONE(-1l)GigabitEthernet2/0connected00None
1as3core1default90.90.90.0/24interface GigabitEthernet3/0AUTO/NONE(-1l)GigabitEthernet3/0connected00None
\n", "
" ], "text/plain": [ " Node VRF Network Next_Hop \\\n", "0 as3core1 default 90.90.90.0/24 interface GigabitEthernet2/0 \n", "1 as3core1 default 90.90.90.0/24 interface GigabitEthernet3/0 \n", "\n", " Next_Hop_IP Next_Hop_Interface Protocol Metric Admin_Distance Tag \n", "0 AUTO/NONE(-1l) GigabitEthernet2/0 connected 0 0 None \n", "1 AUTO/NONE(-1l) GigabitEthernet3/0 connected 0 0 None " ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get all routes for the network 90.90.90.0/24 on core routers\n", "bf.q.routes(nodes=\"/core/\", network=\"90.90.90.0/24\").answer().frame()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "***\n", "That's it for now! Feel free to explore further by adding cells and running other questions, or play with [other notebooks](https://github.com/batfish/pybatfish/tree/master/jupyter_notebooks)." ] }, { "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": { "hide_input": false, "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": 2 }