├── 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 | 
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 |
--------------------------------------------------------------------------------