├── Images └── directory_structure.jpg ├── LICENSE ├── README.md ├── batfish_network_visualizer_v1.py └── requirements.txt /Images/directory_structure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanduKonala/Batfish-Network-Visualizer/d882dce5e5e4ca787812e6f6d49600d5740c957f/Images/directory_structure.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/PanduKonala/PanduKonala/blob/main/header_.png) 2 |
3 | # Batfish-Network-Visualizer 4 | ## Overview 5 | > An Open Source python tool which helps the user / learners to understand the configurations provided to Batfish by generating a network diagram. This tool uses [Pybatfish](https://github.com/batfish/pybatfish) and [N2G](https://pypi.org/project/N2G/) modules for visualization of the network from configuration files. 6 | ## Prerequisites 7 | 1.[Install Python3](https://www.python.org/downloads/) 8 |
9 | 2.[Install Batfish](https://github.com/batfish/batfish/blob/master/README.md) 10 | 11 | Ignore if you have already installed. 12 | 13 | ## Tool Execution 14 |
15 | Step1: Make sure the Batfish docker is up and running. 16 |
17 | Step2: Intall the requirments.txt file using pip i.e. pip install -r requirements.txt 18 |
19 | Step3: Prepare a directory structure as shown in the image located at Images --> directory_structure.jpg and make sure the configuration files end with "cfg" as an extension. 20 |
21 | Step4: Run the tool --> python3 batfish_network_visualizer_v1.py 22 |
23 | # Caution if theres an errors such as module missing while execution then please install that module using pip --> pip install {module_name} 24 |
25 | Step5: After few seconds the tool will generate the output in the "results" directory which is present along with snapshot directory. 26 |
27 | Step6: Import the file "network_map.drawio" to https://app.diagrams.net for visualization. 28 | 29 | ## Conclusion 30 | > I hope this tool helps many people to understand more about Batfish. Visualization always helps to learn faster and effectively. 31 | > Special Thanks to [Anton Karneliuk](https://karneliuk.com/2021/06/network-analysis-1-setting-up-and-getting-started-with-batfish-in-multivendor-network-with-cisco-arista-and-cumulus/) for an awesome tutorial on Batfish which helped to get me started. 32 | -------------------------------------------------------------------------------- /batfish_network_visualizer_v1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pathlib 3 | import json 4 | import logging 5 | import pandas as pd 6 | from N2G import drawio_diagram 7 | from pybatfish.client.commands import * 8 | from pybatfish.question import bfq 9 | from pybatfish.question.question import load_questions 10 | from pybatfish.datamodel.flow import HeaderConstraints, PathConstraints 11 | from pybatfish.datamodel import * 12 | from colorama import Fore, init 13 | 14 | logging.getLogger("pybatfish").setLevel(logging.ERROR) 15 | init(autoreset=True) 16 | diagram = drawio_diagram() 17 | 18 | #https://github.com/PanduKonala/Batfish-Network-Visulizer 19 | print(Fore.GREEN + "*****************************************************************************" ) 20 | print(Fore.YELLOW + " Batfish Network Visualizer v1.0 " + Fore.CYAN + "Created by Pandu Konala " ) 21 | print(Fore.GREEN + "*****************************************************************************" ) 22 | NETWORK_NAME = "network" 23 | BASE_SNAPSHOT_NAME = "batfish-candidate" 24 | BASE_SNAPSHOT_PATH = "./snapshot" 25 | bf_session.host = "127.0.0.1" 26 | 27 | def initialise_batfish(): 28 | load_questions() 29 | bf_set_network(NETWORK_NAME) 30 | bf_init_snapshot(BASE_SNAPSHOT_PATH, name=BASE_SNAPSHOT_NAME, overwrite=True) 31 | 32 | def analyse_network(report_dir): 33 | 34 | #Batfish Queries Start 35 | parse_status = bfq.fileParseStatus().answer().frame() #Extract the status of configurations 36 | node_properties = bfq.nodeProperties().answer().frame() #Extract Node properties 37 | interface = bfq.interfaceProperties().answer().frame() #Extract Interface properties 38 | vlan_prop = bfq.switchedVlanProperties().answer().frame() #Extract VLAN properties 39 | ip_owners = bfq.ipOwners().answer().frame() #Extract IP Owners 40 | l3edge = bfq.layer3Edges().answer().frame() #Extract L3 edges 41 | mlag = bfq.mlagProperties().answer().frame() #Extract MPLAG properties 42 | ospf_config = bfq.ospfProcessConfiguration().answer().frame() #Extract OSPF configuration 43 | ospf_area_config = bfq.ospfAreaConfiguration().answer().frame() #Extract OSPF area configuration 44 | ospf_interface = bfq.ospfInterfaceConfiguration().answer().frame() #Extract OSPF interface configuration 45 | ospf_session = bfq.ospfSessionCompatibility().answer().frame() #Extract OSPF Session compatability 46 | bgp_config = bfq.bgpProcessConfiguration().answer().frame() #Extract BGP configuration 47 | bgp_peer_config = bfq.bgpPeerConfiguration().answer().frame() #Extract BGP peer configuration 48 | bgp_session = bfq.bgpSessionStatus().answer().frame() #Extract BGP session compatibility 49 | routing = bfq.routes().answer().frame() #Extract Routing table 50 | f5_vip = bfq.f5BigipVipConfiguration().answer().frame() #Extract F5 VIP configuration 51 | named_structure = bfq.namedStructures().answer().frame() #Extract Named Structures 52 | def_structure = bfq.definedStructures().answer().frame() #Extract Structure deginitions 53 | ref_structure = bfq.referencedStructures().answer().frame() #Extract Referenced structures 54 | undefined_references = bfq.undefinedReferences().answer().frame() #Extract Undefined references 55 | unused_structure = bfq.unusedStructures().answer().frame() #Extract Used structures 56 | #Batfish Queries End 57 | 58 | #Result from Batfish Start 59 | analysis_report_file = report_dir + "/" + NETWORK_NAME + "_analysis_result.xlsx" 60 | with pd.ExcelWriter(analysis_report_file) as work_book: 61 | parse_status.to_excel(work_book, sheet_name="parse_satus", engine="xlsxwriter") 62 | node_properties.to_excel( 63 | work_book, sheet_name="node_properties", engine="xlsxwriter" 64 | ) 65 | interface.to_excel( 66 | work_book, sheet_name="interface_properties", engine="xlsxwriter" 67 | ) 68 | vlan_prop.to_excel(work_book, sheet_name="vlan_properties", engine="xlsxwriter") 69 | ip_owners.to_excel(work_book, sheet_name="IPOwners", engine="xlsxwriter") 70 | l3edge.to_excel(work_book, sheet_name="l3edges", engine="xlsxwriter") 71 | mlag.to_excel(work_book, sheet_name="mlag", engine="xlsxwriter") 72 | ospf_session.to_excel(work_book, sheet_name="ospf_session", engine="xlsxwriter") 73 | ospf_config.to_excel(work_book, sheet_name="ospf_config", engine="xlsxwriter") 74 | ospf_area_config.to_excel( 75 | work_book, sheet_name="ospf_area_config", engine="xlsxwriter" 76 | ) 77 | ospf_interface.to_excel( 78 | work_book, sheet_name="ospf_interface", engine="xlsxwriter" 79 | ) 80 | bgp_config.to_excel(work_book, sheet_name="bgp_config", engine="xlsxwriter") 81 | bgp_peer_config.to_excel( 82 | work_book, sheet_name="bgp_peer_config", engine="xlsxwriter" 83 | ) 84 | bgp_session.to_excel(work_book, sheet_name="bgp_session", engine="xlsxwriter") 85 | routing.to_excel(work_book, sheet_name="routing_table", engine="xlsxwriter") 86 | f5_vip.to_excel(work_book, sheet_name="f5_vip", engine="xlsxwriter") 87 | named_structure.to_excel( 88 | work_book, sheet_name="named_structure", engine="xlsxwriter" 89 | ) 90 | def_structure.to_excel( 91 | work_book, sheet_name="defined_structures", engine="xlsxwriter" 92 | ) 93 | ref_structure.to_excel( 94 | work_book, sheet_name="referrenced_structures", engine="xlsxwriter" 95 | ) 96 | undefined_references.to_excel( 97 | work_book, sheet_name="undefined_references", engine="xlsxwriter" 98 | ) 99 | unused_structure.to_excel( 100 | work_book, sheet_name="unused_structure", engine="xlsxwriter" 101 | ) 102 | #Result from Batfish End 103 | 104 | def plot_ospf_graph(): 105 | 106 | ospfneigh = bfq.ospfSessionCompatibility().answer().frame() 107 | if ospfneigh.empty: 108 | print(Fore.RED + " # No OSPF Neighhbors Detected...") 109 | else: 110 | print(Fore.YELLOW + " --> Visualized OSPF Tables!") 111 | ospfneigh_json = json.loads(ospfneigh.to_json(orient="index")) 112 | # print (json.dumps(ospfneigh_json, indent=4)) 113 | mapped_node = [] 114 | mapped_link_list = [] 115 | diagram.add_diagram("OSPF") 116 | for key in ospfneigh_json: 117 | current_link = [] 118 | current_link_reverse = [] 119 | neighbor = ospfneigh_json[key] 120 | node_id = f'{neighbor["Interface"]["hostname"]}' 121 | remote_node_id = f'{neighbor["Remote_Interface"]["hostname"]}' 122 | if node_id not in mapped_node: 123 | diagram.add_node(id=f"{node_id}") 124 | mapped_node.append(node_id) 125 | if remote_node_id not in mapped_node: 126 | diagram.add_node(id=f"{remote_node_id}") 127 | mapped_node.append(remote_node_id) 128 | current_link = [f"{node_id}", f"{remote_node_id}"] 129 | current_link_reverse = [f"{remote_node_id}", f"{node_id}"] 130 | if current_link not in mapped_link_list: 131 | diagram.add_link( 132 | f"{node_id}", 133 | f"{remote_node_id}", 134 | label=f'{node_id}({neighbor["IP"]})(AreaID={neighbor["Area"]})' 135 | f' == {neighbor["Session_Status"]}' 136 | f" == {remote_node_id}" 137 | f'({neighbor["Remote_IP"]})(AreaID={neighbor["Remote_Area"]}', 138 | ) 139 | mapped_link_list.append(current_link) 140 | mapped_link_list.append(current_link_reverse) 141 | 142 | def plot_bgp_graph(): 143 | 144 | bgpneigh = bfq.bgpSessionStatus().answer().frame() 145 | if bgpneigh.empty: 146 | print(Fore.RED + " # No BGP Peers Detected...") 147 | else: 148 | print(Fore.YELLOW + " --> Visualized BGP Tables!") 149 | bgpneigh_json = json.loads(bgpneigh.to_json(orient="index")) 150 | mapped_node = [] 151 | mapped_link_list = [] 152 | diagram.add_diagram("BGP") 153 | for key in bgpneigh_json: 154 | current_link = [] 155 | current_link_reverse = [] 156 | neighbor = bgpneigh_json[key] 157 | node_id = f'{neighbor["Node"]}\n({neighbor["Local_AS"]})' 158 | remote_node_id = f'{neighbor["Remote_Node"]}\n({neighbor["Remote_AS"]})' 159 | if node_id not in mapped_node: 160 | diagram.add_node(id=f"{node_id}") 161 | mapped_node.append(node_id) 162 | if remote_node_id not in mapped_node: 163 | diagram.add_node(id=f"{remote_node_id}") 164 | mapped_node.append(remote_node_id) 165 | current_link = [f"{node_id}", f"{remote_node_id}"] 166 | current_link_reverse = [f"{remote_node_id}", f"{node_id}"] 167 | if current_link not in mapped_link_list: 168 | diagram.add_link( 169 | f"{node_id}", 170 | f"{remote_node_id}", 171 | label=f'{node_id}({neighbor["Local_IP"]})' 172 | f' == {neighbor["Established_Status"]}' 173 | f' == {remote_node_id}({neighbor["Remote_IP"]})', 174 | ) 175 | mapped_link_list.append(current_link) 176 | mapped_link_list.append(current_link_reverse) 177 | 178 | def plot_l3_graph(): 179 | l3edges = bfq.layer3Edges().answer().frame() 180 | if l3edges.empty: 181 | print(Fore.RED + " # No L3 Adjencies Detected...") 182 | else: 183 | print(Fore.YELLOW + " --> Visualized L3 Network!") 184 | l3edges_json = json.loads(l3edges.to_json(orient="index")) 185 | mapped_node = [] 186 | diagram.add_diagram("L3") 187 | for key in l3edges_json: 188 | neighbor = l3edges_json[key] 189 | node_id = f'{neighbor["Interface"]["hostname"]}' 190 | remote_node_id = f'{neighbor["Remote_Interface"]["hostname"]}' 191 | if node_id not in mapped_node: 192 | diagram.add_node(id=f"{node_id}") 193 | mapped_node.append(node_id) 194 | if remote_node_id not in mapped_node: 195 | diagram.add_node(id=f"{remote_node_id}") 196 | mapped_node.append(remote_node_id) 197 | diagram.add_link( 198 | f"{node_id}", 199 | f"{remote_node_id}", 200 | label=f'{node_id}({neighbor["IPs"]})' 201 | f" == VLAN {key}" 202 | f' == {remote_node_id}({neighbor["Remote_IPs"]})', 203 | ) 204 | 205 | def main(): 206 | 207 | initialise_batfish() 208 | report_dir = "results" 209 | pathlib.Path(report_dir).mkdir(exist_ok=True) 210 | print(Fore.CYAN + "\n$ Analysing Configurations...") 211 | analyse_network(report_dir) 212 | print(Fore.CYAN + "\n$ Visualizing Network...") 213 | plot_ospf_graph() 214 | plot_bgp_graph() 215 | plot_l3_graph() 216 | diagram.layout(algo="kk") 217 | diagram_file_name = report_dir + "/" + "network_map.drawio" 218 | diagram.dump_file(filename=diagram_file_name, folder="./") 219 | 220 | print(Fore.GREEN + "\n$ Network Visualization Complete!") 221 | print(Fore.BLUE + "\n*** Open the output file at " + Fore.RED + "https://app.diagrams.net" + Fore.BLUE + " ***"+ "\n") 222 | print(Fore.GREEN + "*****************************************************************************" ) 223 | 224 | if __name__ == "__main__": 225 | main() 226 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | astroid==2.4.2 3 | attrs==20.2.0 4 | batfish==0.0.2 5 | bidict==0.21.2 6 | black==20.8b1 7 | certifi==2020.6.20 8 | chardet==3.0.4 9 | click==7.1.2 10 | colorama==0.4.3 11 | ConfigArgParse==0.15.2 12 | deepdiff==5.0.2 13 | Deprecated==1.2.10 14 | et-xmlfile==1.0.1 15 | idna==2.10 16 | ipaddress==1.0.23 17 | isort==5.5.2 18 | jdcal==1.4.1 19 | lazy-object-proxy==1.4.3 20 | mccabe==0.6.1 21 | mypy-extensions==0.4.3 22 | N2G==0.1.2 23 | netconan==0.11.2 24 | numpy==1.19.2 25 | openpyxl==3.0.5 26 | ordered-set==4.0.2 27 | pandas==0.25.3 28 | passlib==1.7.2 29 | pathspec==0.8.0 30 | pybatfish==2021.7.9.974 31 | pylint==2.6.0 32 | python-dateutil==2.8.1 33 | python-igraph==0.8.2 34 | pytz==2020.1 35 | PyYAML==5.3.1 36 | regex==2020.7.14 37 | requests==2.24.0 38 | requests-toolbelt==0.9.1 39 | simplejson==3.17.2 40 | six==1.15.0 41 | texttable==1.6.3 42 | toml==0.10.1 43 | typed-ast==1.4.1 44 | typing-extensions==3.7.4.3 45 | urllib3==1.25.10 46 | wrapt==1.12.1 47 | xlrd==1.2.0 48 | --------------------------------------------------------------------------------