├── .gitignore ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── donate │ ├── BCH.png │ └── BTC.png ├── images │ ├── diagram_example.png │ ├── diagram_example1.png │ ├── diagram_example2.png │ ├── diagram_example3.png │ ├── get-hosts_example1.png │ └── tracemac_example1.png └── template_module.py ├── modules ├── checkconfig.py ├── diagram.py ├── get-arp-table.py ├── get-hosts.py ├── get-mac-addresses.py ├── newconfig.py ├── showconfig.py └── tracemac.py ├── natlas-cli.py ├── natlas.conf ├── natlas ├── __init__.py ├── _version.py ├── config.py ├── mac.py ├── natlas.py ├── network.py ├── node.py ├── node_stack.py ├── node_vss.py ├── output.py ├── output_catalog.py ├── output_diagram.py ├── snmp.py └── util.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.egg 3 | *.egg-info 4 | dist 5 | *~ 6 | *.log 7 | *.swp 8 | *.swo 9 | IDEAS 10 | modules/test.py 11 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | --------------------- 2 | natlas 3 | Michael Laforest 4 | mjlaforest@gmail.com 5 | 6 | CHANGE LOG 7 | -------------------- 8 | 9 | v0.11 - 10 | - Rebranded to natlas 11 | - Reorganized into framework with dynamically loadable modules 12 | - Added Python version check 13 | - Better error info when loading improperly formatted config files 14 | - Added ACL match criteria for discovery: platform, serial, software 15 | - FIX: Platform / IOS / Serial sometimes not captured if SNMP was not available but CDP/LLDP had it 16 | - FIX: SNMP timeout could cause crash 17 | 18 | modules 19 | - Renamed: config -> newconfig 20 | - Added: showconfig 21 | - Added: checkconfig 22 | - Added: get-mac-table 23 | - Added: get-arp-table 24 | - Added: get-hosts 25 | - Added: tracemac 26 | - Removed: output_stdout 27 | 28 | API 29 | - Created the initial natlas API 30 | - Added: config_generate() 31 | - Added: config_validate() 32 | - Added: config_load() 33 | - Added: snmp_add_credential() 34 | - Added: set_discover_maxdepth() 35 | - Added: set_verbose() 36 | - Added: discover_network() 37 | - Added: new_node() 38 | - Added: query_node() 39 | - Added: write_diagram() 40 | - Added: write_catalog() 41 | - Added: get_switch_vlans() 42 | - Added: get_switch_macs() 43 | - Added: get_discovered_nodes() 44 | - Added: get_node_ip() 45 | - Added: get_arp_table() 46 | - Added: get_neighbors() 47 | 48 | v0.10 - 5/5/2018 49 | - Ported to Python 3 50 | - Cleaned up and refactored code 51 | - Improved discovery 52 | - Improved discovery console output 53 | - Fixed VSS chassis detection; now finds correct serial# and platform 54 | - All output referencing node IP now uses best IP instead of first found 55 | - Try all known IPs for a node until one works; no longer fails on unreacable IPs (eg, VRFs/ACL blocks/etc) 56 | - Default depth is now 100 57 | - Added runtime to stdout 58 | - Renamed 'graph' to 'diagram' 59 | 60 | - Cisco ACL-style node matching (replaced config subnets/exclude with discover) 61 | - Added 'leaf' option to stop discovery beyond a matching node 62 | - Added 'include' option to stop discovery at a matching node 63 | 64 | - Config option diagram/node_text replaces the below: 65 | - include_svi 66 | - include_lo 67 | - include_serials 68 | 69 | diagram 70 | - Changed -f option to -o 71 | - If a LAG spans multiple devices (eg, Nexus) override expand_lag for that device 72 | - Output can create multiple files. Ex: -o "file.{svg|png}" will create file.svg and file.png 73 | 74 | getmacs 75 | - Added as new module 76 | 77 | v0.9 - 78 | graph 79 | - Fixed bug with allowed VLAN lists not showing correct ranges. 80 | - Fixed bug where certain SNMP lookups could return empty sets and cause an exception 81 | 82 | v0.8 - 9/21/2015 83 | - Internal code changes. 84 | - Trunks show native VLAN (if mismatched then both P and C will be displayed) 85 | - Trunks show allowed VLANs (if mismatched then both P and C will be displayed) (standard range only) 86 | - Added support for LLDP 87 | - Renamed config option "collapse_stackwise" to "expand_stackwise" 88 | - Renamed config option "collapse_vss" to "expand_vss" 89 | 90 | v0.7 - 7/25/2015 91 | graph 92 | - Added config option "include_svi" to include SVI info for nodes 93 | - Added config option "include_lo" to include loopback info for nodes 94 | - Added config option "include_serials" to include serial numbers in diagram for nodes 95 | - Added config option "get_stack_members" to include additional stack member info (serials, mac's etc). 96 | This info is also included in the catalog option, with each stack member being a line item. 97 | - Added config option "get_vss_members" to include additional VSSS member info. 98 | - Added config option "collapse_stackwise" and "collapse_vss". When =0 clusters will be expanded. 99 | - Removed SNMP platform detection and now use CDP. This means a low depth may not get the root platform. 100 | 101 | v0.6 - 7/19/2015 102 | graph 103 | - Changed from PyDot to PyDot2 104 | - Fixed bug with CDP neighbors that do not advertise an IP (reported by Kent Coble) 105 | - Added config file option to change font size for node text 106 | - Added config file option to change font size for link text 107 | - Changed internal crawl method 108 | - Improved platform identification 109 | - Added IOS version identification 110 | 111 | config 112 | - Added command line module option to output a default config file 113 | 114 | v0.4 - 7/10/2015 115 | - Improved speed. 116 | - Cleaned up code base. 117 | - Made PyNetAddr optional (change USE_NETADDR=1 to USE_NETADDR=0 to disable) 118 | 119 | v0.3 - 7/8/2015 120 | - Added -C option to write a catalog of the discovery to a csv file. 121 | - Runs for ~1/4 the time. 122 | 123 | v0.2 - 7/7/2015 124 | 125 | - Config: Changed "ignore" to "exclude". Now a list of CIDR's instead of specific IP addresses (requested by /u/LiftedKilt) 126 | - Config: Added "subnets" block. List of CIDR's of allowed subnets to be crawled. (requeted by /u/1n5aN1aC) 127 | - Fixed bug related to unnumbered layer 3 interface (reported by /u/chazmosis) 128 | - Fixed bug related to Nexus host format (reported by /u/endemic) 129 | - Added OID for Nexus platform (thanks to /u/endemic) 130 | 131 | v0.1 - 7/6/2015 132 | 133 | - Initial release 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG 2 | include LICENSE 3 | include README.md 4 | include natlas.conf 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # natlas 2 | 3 | natlas - Network Atlas 4 | Michael Laforest `` 5 | 6 | Automated discovery and diagram tools using SNMP, CDP, and LLDP. 7 | 8 | ```# ./natlas-cli.py diagram -r 10.75.0.1 -o .\network.svg```


*The above command will generate the diagram to the right.* | ![natlas-Diagram Main][diag-main] 9 | :--- | --- 10 | 11 | # Support 12 | 13 | If you use any of these tools or find them useful please consider donating. 14 | 15 | Donation Method | Address | QR Code 16 | --- | --- | --- 17 | Bitcoin Cash (BCH) | 1HSycjR3LAZxuLG34aEBbQdUSayPkh8XsH | ![1HSycjR3LAZxuLG34aEBbQdUSayPkh8XsH](https://raw.github.com/MJL85/natlas/master/docs/donate/BCH.png "Bitcoin Cash (BCH)") 18 | Bitcoin (BTC) | 1HY3jPYVfE6YZbuYTYfMpazvSKRXjZDMbS | ![1HY3jPYVfE6YZbuYTYfMpazvSKRXjZDMbS](https://raw.github.com/MJL85/natlas/master/docs/donate/BTC.png "Bitcoin (BTC)") 19 | 20 | # About 21 | 22 | natlas is a python application framework which includes a discovery engine, an API, and a front-end cli for running available modules. 23 | 24 | ##### For Users 25 | The `natlas-cli.py` program allows you to browse and execute any natlas module, including, among others, the network discovery and diagram module. 26 | 27 | ##### For Developers 28 | 29 | Copy `docs/template_module.py` to a new file in the `modules/` directory. 30 | Edit the function `mod_load()` to set the name and help properties for your module. 31 | The entry function for any module is `mod_entry()`. 32 | When creating a new module, natlas will create a new object and pass it to mod_entry(). From there the natlas API is available and includes functions such as: 33 | - discover_network() 34 | - query_node() 35 | - get_switch_vlans() 36 | 37 | Once saved, your module will automatically be listed in `natlas-cli.py list` and runnable. 38 | 39 | # Modules 40 | | Module | Description | 41 | | --- | --- | 42 | | diagram | Discovers a network and generates a diagram based on CDP and LLDP neighbor information. | 43 | | get-mac-table | Collect a list of all MAC addresses on the discovered network and generate a report. | 44 | | get-arp-table | Collect a list of all ARP entries. | 45 | | get-hosts | Determine all hosts connected to one or all switches in the network. Includes MAC, IP, DNS name of each host, along with what switch and port it was found on. Can be exported to CSV. | 46 | | tracemac | Trace a MAC address through a layer 2 network. | 47 | | newconfig, showconfig, checkconfig | Modules to create, display, and validate natlas configuration files. | 48 | 49 | # Network Discovery 50 | 51 | The discovery process uses SNMP, CDP, and LLDP to discover the network topology and details about each node. Each discovered node will be evaluated against the `discover` ACL (defined in the config file) to determine how to proceed; the ACL may allow discovery, stop discovery here, or include it as a leaf in the diagram. 52 | 53 | 54 | 55 | 64 | 68 | 69 | 70 | 79 | 80 |
56 | The discover ACL is defined as 57 |
"discover" : [
 58 | 	ACE1,
 59 | 	ACE2,
 60 | 	...
 61 | 	ACEn,
 62 | ]
63 |
65 | An ACE is defined as 66 |
<permit|deny|leaf|include|;> < [host REGEX] | [ip CIDR] >
67 |
71 | Example 72 |
"discover" : [
 73 | 	"deny ip 10.50.12.55",
 74 | 	"deny host ^SEP.*",
 75 | 	"permit ip 10.50.12.0/24",
 76 | 	"leaf host ^Switch2$",
 77 | 	"permit ip any"
 78 | ]	
81 | 82 | --- 83 | 84 | | ACE Match Type| Include Node | Collect Node Information | Allow Discovery of Adjacencies | 85 | | --- |:---:|:---:|:---:| 86 | | **permit** | yes | yes | yes | 87 | | **leaf** | yes | yes | | 88 | | **include** | yes | | | 89 | | **deny** | | | | 90 | 91 | --- 92 | 93 | | ACE Parameter | Description | Example | 94 | | --- | --- | --- | 95 | | host REGEX | The host can match any regular expression string. The host string is what is reported from CDP or LLDP. | `host Router-.*` | 96 | | ip CIDR | The ip can be matched against and CIDR. | `ip 10\\.50\\.31\\.0/24` | 97 | | platform REGEX | The system platform or hardware model. | `platform .*3850.*` | 98 | | software REGEX | The system software version or IOS. | `software ^15\\.3` | 99 | 100 | # Command Reference 101 | 102 | ### Diagram 103 | ``` 104 | # natlas-cli.py diagram -r 105 | -o 106 | [-d ] 107 | [-c ] 108 | [-t ] 109 | [-C ] 110 | ``` 111 | | Option | Description | 112 | | --- | --- | 113 | | `-r ` | IP address of the network node to start on. | 114 | | `-o ` | The file that the output will be written to.
Common file extensions: `.png`, `.pdf`, `.svg` | 115 | | `-c ` | The JSON configuration file to use. | 116 | | `-d ` | The maximum hop depth to discover, starting at the root node specified by `-r` | 117 | | `-t ` | The title to give your generated network diagram. | 118 | | `-C ` | If specified, natlas will generate a comma separated (CSV) catalog file with a list of all devices discovered. | 119 | 120 | ### get-mac-table 121 | ``` 122 | # natlas-cli.py get-mac-table -n -c [-m ] [-p ] [-v ] 123 | ``` 124 | | Option | Description | 125 | | --- | --- | 126 | | `-n ` | IP address of the network node to get MAC addresses from. | 127 | | `-c ` | SNMPv2 community string. | 128 | | `-m ` | Filter MAC addresses by this regex string. | 129 | | `-p ` | Filter MAC addresses to only those on ports matching this regex string. | 130 | | `-v ` | Filter MAC addresses to only those on VLANs matching this regex string. | 131 | 132 | ### get-arp-table 133 | ``` 134 | # natlas-cli.py get-arp-table -n -c [-s | -d] [-i ] [-v ] [-m ] 135 | ``` 136 | | Option | Description | 137 | | --- | --- | 138 | | `-n ` | IP address of the network node to get ARP entries from. | 139 | | `-c ` | SNMPv2 community string. | 140 | | `-s` | Include static entries only | 141 | | `-d` | Include dyntamic entries only | 142 | | `-i ` | Include entries with IP addresses that match regex pattern | 143 | | `-v ` | Include entries with VLANs that match regex pattern | 144 | | `-m ` | Include entries with MAC addreses that match regex pattern | 145 | 146 | ### get-hosts 147 | 148 | get-hosts can either collect information from a single node or can do a network discovery and collect information from all discovered nodes. 149 | ``` 150 | # natlas-cli.py get-hosts -r -c [-o ] [-d ] 151 | 152 | # natlas-cli.py get-hosts -n [-r ] -C [-v ] [-p ] [-o ] 153 | ``` 154 | | Option | Description | 155 | | --- | --- | 156 | | `-r ` | IP address to begin a network discovery. | 157 | | `-c ` | natlas configuration file to use. | 158 | | `-o ` | Output CSV file path. | 159 | | `-d ` | Maximum network discovery depth. | 160 | | --- | --- | 161 | | `-n ` | IP address of single layer2 or layer3 node to collect information from. | 162 | | `-r ` | IP address of the layer3 device to collect ARP entries from. If this is omitted then the IP from -n will be used. | 163 | | `-C ` | SNMPv2 community string. | 164 | | `-v ` | Include entries with VLANs that match regex pattern | 165 | | `-p ` | Include entries on ports that match regex pattern | 166 | | `-o ` | Output CSV file path. | 167 | 168 | ### tracemac 169 | 170 | ``` 171 | # natlas-cli.py tracemac -n -m 172 | ``` 173 | | Option | Description | 174 | | --- | --- | 175 | | `-n ` | IP address of node to begin layer 2 MAC trace. | 176 | | `-m ` | MAC address to locate in the network. | 177 | 178 | ### Config 179 | | | | 180 | | --- | --- | 181 | | `# natlas-cli.py newconfig` | Generate a new config file | 182 | | `# natlas-cli.py showconfig [-c ]` | Display the config file | 183 | | `# natlas-cli.py checkconfig [-c ]` | Validate the contents of the config file | 184 | 185 | # Configuration File 186 | The configuration file defines common parameters in a JSON format. 187 | ``` 188 | { 189 | "snmp" : [ 190 | { "community":"private", "ver":2 }, 191 | { "community":"public", "ver":2 } 192 | ], 193 | "domains" : [ 194 | ".company.net", 195 | ".company.com" 196 | ], 197 | "discover" : [ 198 | "permit ip 10.0.0.0/8", 199 | "permit host Router[1,2]", 200 | "deny ip any", 201 | ], 202 | "diagram" : { 203 | "node_text_size" : 10, 204 | "link_text_size" : 9, 205 | "title_text_size" : 15, 206 | "get_stack_members" : 0, 207 | "get_vss_members" : 0, 208 | "expand_stackwise" : 0, 209 | "expand_vss" : 0, 210 | "expand_lag" : 1, 211 | "group_vpc" : 0 212 | } 213 | 214 | } 215 | ``` 216 | 217 | | Block / Variable | Description | 218 | | --- | --- | 219 | | `snmp` | Defines a list of SNMP credentials. When connecting to a node, each of these credentials is tried in order until one is successful. | 220 | | `discover` | Defines a Cisco-style ACL. See the `Network Discovery` section. | 221 | | `diagram` | Defines values used by the diagram module. Detailed below in the *Diagram block* table. | 222 | 223 | ### Diagram block 224 | | Variable | Type | Default Value | Description | 225 | | --- | --- | --- | --- | 226 | | `node_text_size` | integer | `10` | Node text size. | 227 | | `link_text_size` | integer | `9` | Link text size. | 228 | | `title_text_size` | integer | `15` | Diagram title text size. | 229 | | `get_stack_members` | bool | `0` | If set to `1`, nodes will include details about stackwise members. | 230 | | `get_vss_members` | bool | `0` | If set to `1`, nodes will include details about VSS members. | 231 | | `expand_stackwise` | bool | `0` | If set to `1`, nodes belonging to stackwise groups will be expanded to show each member as a node. | 232 | | `expand_vss` | bool | `0` | If set to `1`, nodes belonging to VSS groups will be expanded to show each member as a node. | 233 | | `expand_lag` | bool | `1` | If set to `1`, each link between nodes will be shown. If set to `0`, links of the same logical link channel will be grouped and only the channel link will be shown. | 234 | | `group_vpc` | bool | `0` | If set to `1`, VPC peers will be grouped together on the diagram, otherwise they will not be clustered. | 235 | 236 | # Diagram 237 | natlas will attempt to collect the following information and include it in the generated diagram: 238 | + All devices (via CDP and LLDP) 239 | + Interface names 240 | + IP addresses 241 | + VLAN memberships 242 | + Etherchannel memberships (LACP only) 243 | + Identify trunk links 244 | + Identify switched links 245 | + Identify routed links 246 | + BGP Local AS 247 | + OSPF Router ID 248 | + HSRP Virtual IP 249 | + HSRP Priority 250 | + VSS Domain 251 | + Stackwise membership 252 | + VPC peerlink information 253 | 254 | #### Diagram Formatting 255 | + Nodes 256 | + Circle nodes represent layer 2 switches. 257 | + Diamond nodes represent layer 3 switches or routers. 258 | + If a node has multiple borders then either VSS or StackWise is enabled. 259 | + VSS - Will always have a double border. 260 | + StackWise - The number of borders denotes the number of switches in the stack. 261 | + If the configuration specifies, VSS/VPC/Stackwise nodes will be grouped in larger squares. 262 | + Links 263 | + Links are shown with arrowed lines. The end with no arrow is the *parent* and the end with the arrow is the *child*, such that the arrangement is *parent*->*child*. 264 | + If a link says *P:gi0/1* , *C:gi1/4* then the parent node's connection is on port gi0/1 and the child node's connection is on port gi1/4. 265 | + If the link is part of an Etherchannel the etherchannel's interface name will also be shown. Since an etherchannel interface is locally significiant, a *P:* and *C:* will also be shown if available. 266 | 267 | # Examples 268 | 269 | #### Diagram Example 1 270 | ![natlas-Diagram Ex1][diag-ex1] 271 | 272 | #### Diagram Example 2 273 | ![natlas-Diagram Ex2][diag-ex2] 274 | 275 | #### Diagram Example 3 276 | ![natlas-Diagram Ex3][diag-ex3] 277 | 278 | #### get-hosts Example 1 279 | ![natlas-get-hosts Ex1][get-hosts-ex1] 280 | 281 | #### tracemac Example 1 282 | ![natlas-tracemac Ex1][tracemac-ex1] 283 | 284 | # FAQ 285 | 286 | #### Q1 - My diagram is too large. I only want to diagram part of my network. 287 | Try changing the config `discover` ACL to narrow down the scope of your discovery. You can explicitly deny CIDR's or host name regex patterns if you do not want them included in your diagram. 288 | 289 | #### Q2 - Where is the config file? 290 | Create a new one with 291 | `# natlas-cli.py newconfig > natlas.conf` 292 | 293 | #### Q3 - I need a diagram with less proprietary information. Can I get one without IPs or serial numbers? 294 | You can change the text inside each node by editing the config option `diagram\node_text`. Below is an example that would produce a minimal information diagram: 295 | 296 | ``` 297 | "diagram" : { 298 | node_text = '{node.name}
{node.ios}
{node.plat}' 299 | } 300 | ``` 301 | #### Q4 - How can I remove Cisco VoIP phones from my diagram? 302 | ``` 303 | "discover" : [ 304 | "deny host ^SEP.*$" 305 | ] 306 | ``` 307 | 308 | [diag-main]: https://raw.github.com/MJL85/natlas/master/docs/images/diagram_example.png "Diagram Main" 309 | [diag-ex1]: https://raw.github.com/MJL85/natlas/master/docs/images/diagram_example1.png "Diagram Example 1" 310 | [diag-ex2]: https://raw.github.com/MJL85/natlas/master/docs/images/diagram_example2.png "Diagram Example 2" 311 | [diag-ex3]: https://raw.github.com/MJL85/natlas/master/docs/images/diagram_example3.png "Diagram Example 3" 312 | [get-hosts-ex1]: https://raw.github.com/MJL85/natlas/master/docs/images/get-hosts_example1.png "get-hosts Example 1" 313 | [tracemac-ex1]: https://raw.github.com/MJL85/natlas/master/docs/images/tracemac_example1.png "tracemac Example 1" 314 | 315 | 316 | -------------------------------------------------------------------------------- /docs/donate/BCH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/donate/BCH.png -------------------------------------------------------------------------------- /docs/donate/BTC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/donate/BTC.png -------------------------------------------------------------------------------- /docs/images/diagram_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/images/diagram_example.png -------------------------------------------------------------------------------- /docs/images/diagram_example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/images/diagram_example1.png -------------------------------------------------------------------------------- /docs/images/diagram_example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/images/diagram_example2.png -------------------------------------------------------------------------------- /docs/images/diagram_example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/images/diagram_example3.png -------------------------------------------------------------------------------- /docs/images/get-hosts_example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/images/get-hosts_example1.png -------------------------------------------------------------------------------- /docs/images/tracemac_example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MJL85/natlas/f89c6cd14c5349b955241729f5276c89eddde2ca/docs/images/tracemac_example1.png -------------------------------------------------------------------------------- /docs/template_module.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas-cli.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | import getopt 29 | import os 30 | import natlas 31 | 32 | 33 | def mod_load(mod): 34 | ''' 35 | The function mod_load() is called by natlas to load information about 36 | the module. This information is available to the user from the cli by 37 | using: 38 | # natlas-cli list 39 | # natlas-cli help 40 | etc 41 | ''' 42 | 43 | 44 | ''' 45 | The name of your module. This is also the subcommand you will run in natlas. 46 | For example, if you set name to 'mymodule' then you would run it with: 47 | # natlas-cli mymodule 48 | 49 | The name can not include any whitespaces. 50 | ''' 51 | mod.name = 'TemplateModule' 52 | 53 | 54 | ''' 55 | What version number your module is. The version number is arbitrary 56 | and can be any string. 57 | ''' 58 | 59 | mod.version = '0.1' 60 | 61 | ''' 62 | Your name. 63 | ''' 64 | mod.author = 'Michael Laforest' 65 | 66 | 67 | ''' 68 | Your email address. 69 | ''' 70 | mod.authoremail = 'mjlaforest@gmail.com' 71 | 72 | 73 | ''' 74 | What will be displayed to the console when a user runs 75 | # natlas-cli list 76 | 77 | This should be brief, just a few words. A more descriptive 78 | explaination of your module should be set under the 'help' 79 | attribute below. 80 | ''' 81 | mod.about = 'Template module' 82 | 83 | ''' 84 | The command line options available for your module. 85 | If you have multiple combinations you can set this to an array 86 | of strings rather than a single string. 87 | 88 | Example: 89 | mod.syntax = '-n -g -x' 90 | 91 | The outout of help will be: 92 | name -n -g -x 93 | 94 | mod.syntax = ['-n ', '-g -x'] 95 | 96 | The outout of help will be: 97 | name -n 98 | name -g -x 99 | ''' 100 | mod.syntax = ''' 101 | -n -x 102 | ''' 103 | 104 | ''' 105 | Provide an example of typical output users will see when 106 | running this module. If left blank no example will be 107 | shown from the help. 108 | ''' 109 | mod.example = '' 110 | 111 | 112 | ''' 113 | A more descriptive definition of your module. Where the 'about' section 114 | is brief, this section should be more explicit. 115 | 116 | Indents will be removed. 117 | ''' 118 | mod.help = ''' 119 | This is a blank template showing how 120 | you can create your own module. Simply 121 | modify some attributes and add 122 | some code below. 123 | 124 | Your module will then show up with 125 | # natlas-cli list 126 | ''' 127 | 128 | ''' 129 | If set to 1, natlas will display the total run time after your 130 | module has compeleted. If is recommended to leave this as 1 131 | unless the stdout needs to be formatted specifically. 132 | ''' 133 | mod.notimer = 1 134 | 135 | ''' 136 | If set to 1, natlas will load the configuration file prior to calling 137 | your entry function. 138 | 139 | The configuration file can be specified by the user with -c, or the 140 | default will be used (./natlas.conf). The configuration file is needed 141 | for several API functions to work correctly, such as discover_network(). 142 | ''' 143 | mod.preload_conf = 1 144 | 145 | ''' 146 | What natlas version is required for your module. natlas will only allow 147 | your module to run if it meets the minimum version specified here. 148 | ''' 149 | mod.require_api = '999' 150 | 151 | return 1 152 | 153 | 154 | 155 | def mod_entry(natlas_obj, argv): 156 | ''' 157 | This function is called when a user runs your module. 158 | natlas_obj is an initialized natlas object needed by the API. 159 | argv is a standard argv list from the CLI, minus -c if natlas preloaded the config. 160 | 161 | This function must always return one of the below codes: 162 | 163 | natlas.RETURN_OK - your module finished with no errors 164 | natlas.RETURN_ERR - your module had an error 165 | natlas.RETURN_SYNTAXERR - your module had a syntax error from the cli 166 | ''' 167 | return natlas.RETURN_OK 168 | 169 | -------------------------------------------------------------------------------- /modules/checkconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas-cli.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | import getopt 29 | import os 30 | import natlas 31 | 32 | DEFAULT_OPT_CONF = './natlas.conf' 33 | 34 | def mod_load(mod): 35 | mod.name = 'checkconfig' 36 | mod.version = '0.1' 37 | mod.author = 'Michael Laforest' 38 | mod.authoremail = 'mjlaforest@gmail.com' 39 | mod.about = 'Validate the config' 40 | mod.syntax = '[-c ]' 41 | mod.help = 'Validate the configuration file.' 42 | mod.preload_conf = 0 43 | return 1 44 | 45 | def mod_entry(natlas_obj, argv): 46 | opt_conf = DEFAULT_OPT_CONF 47 | 48 | try: 49 | opts, args = getopt.getopt(argv, 'c:') 50 | except getopt.GetoptError: 51 | print_syntax() 52 | return 0 53 | for opt, arg in opts: 54 | if (opt == '-c'): 55 | opt_conf = arg 56 | natlas_obj.config_validate(opt_conf) 57 | 58 | return 1 59 | 60 | -------------------------------------------------------------------------------- /modules/diagram.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas-cli.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | import getopt 29 | import os 30 | import natlas 31 | 32 | DEFAULT_OPT_DEPTH = 100 33 | DEFAULT_OPT_TITLE = 'natlas Diagram' 34 | 35 | def mod_load(mod): 36 | mod.name = 'diagram' 37 | mod.version = '0.11' 38 | mod.author = 'Michael Laforest' 39 | mod.authoremail = 'mjlaforest@gmail.com' 40 | mod.about = 'Discover and diagram the network' 41 | mod.syntax = '-r \n' \ 42 | ' -o \n' \ 43 | ' [-d ]\n' \ 44 | ' [-c ]\n' \ 45 | ' [-t ]\n' \ 46 | ' [-C ]' 47 | mod.require_api = '0.12' 48 | mod_help = 'Discover and diagram the network beginning at the specified root node.' 49 | return 1 50 | 51 | def mod_entry(natlas_obj, argv): 52 | opt_root_ip = None 53 | opt_output = None 54 | opt_catalog = None 55 | opt_depth = DEFAULT_OPT_DEPTH 56 | opt_title = DEFAULT_OPT_TITLE 57 | 58 | try: 59 | opts, args = getopt.getopt(argv, 'o:d:r:t:F:c:C:') 60 | except getopt.GetoptError: 61 | print('Invalid arguments.') 62 | return 63 | for opt, arg in opts: 64 | if (opt == '-r'): opt_root_ip = arg 65 | if (opt == '-o'): opt_output = arg 66 | if (opt == '-d'): opt_depth = int(arg) 67 | if (opt == '-t'): opt_title = arg 68 | if (opt == '-C'): opt_catalog = arg 69 | 70 | if ((opt_root_ip == None) | (opt_output == None)): 71 | print('Invalid arguments.') 72 | return 73 | 74 | print(' Config file: %s' % natlas_obj.config_file) 75 | print(' Output file: %s' % opt_output) 76 | print('Out Catalog file: %s' % opt_catalog) 77 | print(' Root node: %s' % opt_root_ip) 78 | print(' Discover depth: %s' % opt_depth) 79 | print(' Diagram title: %s' % opt_title) 80 | print() 81 | 82 | # start discovery 83 | natlas_obj.set_discover_maxdepth(opt_depth) 84 | natlas_obj.set_verbose(1) 85 | natlas_obj.discover_network(opt_root_ip, 1) 86 | 87 | # outputs 88 | if (opt_output != None): natlas_obj.write_diagram(opt_output, opt_title) 89 | if (opt_catalog != None): natlas_obj.write_catalog(opt_catalog) 90 | 91 | return 92 | -------------------------------------------------------------------------------- /modules/get-arp-table.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | natlas 4 | 5 | Michael Laforest 6 | mjlaforest@gmail.com 7 | 8 | Copyright (C) 2015-2018 Michael Laforest 9 | 10 | This program is free software; you can redistribute it and/or 11 | modify it under the terms of the GNU General Public License 12 | as published by the Free Software Foundation; either version 2 13 | of the License, or (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 23 | ''' 24 | 25 | import sys 26 | import getopt 27 | import natlas 28 | 29 | def mod_load(mod): 30 | mod.name = 'get-arp-table' 31 | mod.version = '0.1' 32 | mod.author = 'Michael Laforest' 33 | mod.authoremail = 'mjlaforest@gmail.com' 34 | mod.preload_conf = 0 35 | mod.about = 'Display the ARP table' 36 | mod.syntax = '-n -c [-s | -d] [-i ] [-v ] [-m ]' 37 | mod.help = ''' 38 | Query a switch display the ARP table. 39 | 40 | The table can be filtered with: 41 | -s Include Static entries only 42 | -d Include Dynamic entries only 43 | -i Include entries with IP addresses that match regex pattern 44 | -v Include entries with VLANs that match regex pattern 45 | -m Include entries with MAC addreses that match regex pattern 46 | ''' 47 | mod.example = ''' 48 | Get all ARP entries where the MAC begins with 84b8.0262. and is in VLAN 800 or 801 49 | 50 | # get-arp-table -n 10.10.1.66 -c public -m '84b8\.0262\..*' -v "80[01]" 51 | 52 | IP MAC VLAN TYPE 53 | 10.10.19.93 84b8.0262.361c 800 dynamic 54 | 10.10.19.102 84b8.0262.3948 800 dynamic 55 | 10.10.29.104 84b8.0262.1890 801 dynamic 56 | 57 | Found 3 ARP entries 58 | ''' 59 | return 1 60 | 61 | 62 | def mod_entry(natlas_obj, argv): 63 | opt_devip = None 64 | opt_community = None 65 | opt_type = None 66 | opt_vlan = None 67 | opt_mac = None 68 | opt_ip = None 69 | try: 70 | opts, args = getopt.getopt(argv, 'n:c:sdv:m:i:') 71 | except getopt.GetoptError: 72 | return 73 | for opt, arg in opts: 74 | if (opt == '-n'): opt_devip = arg 75 | if (opt == '-c'): opt_community = arg 76 | if (opt == '-s'): opt_type = 'static' 77 | if (opt == '-d'): opt_type = 'dynamic' 78 | if (opt == '-v'): opt_vlan = arg 79 | if (opt == '-m'): opt_mac = arg 80 | if (opt == '-i'): opt_ip = arg 81 | 82 | if ((opt_devip == None) | (opt_community == None)): 83 | return 84 | 85 | # set some snmp credentials for us to use 86 | natlas_obj.snmp_add_credential(2, opt_community) 87 | 88 | # get the ARP table 89 | try: 90 | arp = natlas_obj.get_arp_table(opt_devip, ip=opt_ip, mac=opt_mac, interf=opt_vlan, arp_type=opt_type) 91 | except Exception as e: 92 | print(e) 93 | return 94 | 95 | # print ARP table 96 | print() 97 | print('IP MAC VLAN TYPE') 98 | print('-- --- ---- ----') 99 | for a in arp: 100 | print('{:<15} {:<14} {:<5} {:}'.format(a.ip, a.mac, str(a.interf).lstrip('Vl'), a.arp_type)) 101 | 102 | print('\nFound %i ARP entries' % len(arp)) 103 | 104 | -------------------------------------------------------------------------------- /modules/get-hosts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | natlas 4 | 5 | Michael Laforest 6 | mjlaforest@gmail.com 7 | 8 | Copyright (C) 2015-2018 Michael Laforest 9 | 10 | This program is free software; you can redistribute it and/or 11 | modify it under the terms of the GNU General Public License 12 | as published by the Free Software Foundation; either version 2 13 | of the License, or (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 23 | ''' 24 | 25 | import sys 26 | import getopt 27 | import socket 28 | import natlas 29 | 30 | def mod_load(mod): 31 | mod.name = 'get-hosts' 32 | mod.version = '0.1' 33 | mod.author = 'Michael Laforest' 34 | mod.authoremail = 'mjlaforest@gmail.com' 35 | mod.about = 'Display details about connected hosts' 36 | mod.syntax = [ 37 | '-r -c [-o ] [-d ]', 38 | '-n [-r ] -C [-v ] [-p ] [-o ]' 39 | ] 40 | mod.help = ''' 41 | Collect information about hosts connected to the network. 42 | 43 | To get information from just one node, use the -n option. 44 | To get information from discovered nodes, use the -r option. 45 | 46 | If -r is used, a network discovery is performed at the specified root node. The discovered nodes are then queried to determine hosts connected to each node. 47 | 48 | Details about hosts include MAC addresses, IP addresses, VLANs, switch ports, and DNS names if available. 49 | 50 | The resulting data is printed to stdout and can also be saved to a CSV file using the -o option. 51 | ''' 52 | 53 | mod.example = ''' 54 | Get devices connected to 10.10.10.3 on vlan 20 and on ports starting with "Gi". 55 | The gateway for vlan 44 is on 10.10.10.1. 56 | 57 | # get-hosts -n 10.10.10.3 -r 10.10.10.1 -C public -v 20 -p "Gi.*" 58 | 59 | Collecting MACs from 10.10.10.3... 60 | 20.................................................................................................... 61 | Collecting ARPs from 10.10.10.1... 62 | 63 | Found 25 MAC entries 64 | Found 104 ARP entries 65 | 66 | PORT IP MAC VLAN DNS 67 | Gi1/0/2 10.115.82.130 0023.246b.e7e8 20 super12 68 | Gi1/0/5 10.115.82.158 0023.246b.ea43 20 david 69 | Gi1/0/30 10.115.82.225 0023.246c.27a8 20 jerry 70 | Gi1/0/15 10.115.82.216 0023.246d.9956 20 john1 71 | Gi1/0/21 10.115.82.171 0023.2474.e2ef 20 kiosk3 72 | Gi1/0/22 10.115.82.202 0023.2474.e330 20 bobb 73 | Gi1/0/6 10.115.82.172 0023.2477.9bbd 20 brendam 74 | Gi1/0/23 10.115.82.102 0023.2477.9c18 20 debbie2 75 | Gi2/0/45 10.115.82.126 0023.2493.9dad 20 serverA 76 | Gi2/0/44 10.115.82.153 0023.2493.9df0 20 serverB 77 | Gi2/0/48 10.115.82.16 00c0.b788.aa3c 20 78 | Gi1/0/6 00f8.2c07.bea4 79 | Gi1/0/24 00f8.2c07.bf49 80 | Gi1/0/30 00f8.2c07.ef38 81 | Gi1/0/16 00f8.2c07.ef4c 82 | Gi1/0/17 00f8.2c07.ef57 83 | Gi1/0/21 00f8.2c07.ef5d 84 | Gi1/0/5 00f8.2c07.f08d 85 | Gi1/0/25 10.115.82.154 1c87.2c58.c83d 20 kiosk2 86 | Gi1/0/29 10.115.82.205 305a.3a46.9825 20 kiosk1 87 | Gi2/0/42 10.115.82.113 4ccc.6a16.bf92 20 president 88 | Gi1/0/9 10.115.82.45 5065.f357.b289 20 89 | Gi1/0/27 10.115.82.73 5820.b152.d24d 20 90 | Gi1/0/31 10.115.82.116 9457.a5cb.c16b 20 comptroller 91 | Gi1/0/8 f0b2.e576.cd7c 92 | ''' 93 | return 1 94 | 95 | def mod_entry(natlas_obj, argv): 96 | opt_root_ip = None 97 | opt_node_ip = None 98 | opt_router_ip = None 99 | opt_community = None 100 | opt_vlan = None 101 | opt_port = None 102 | opt_output = None 103 | opt_depth = 100 104 | try: 105 | opts, args = getopt.getopt(argv, 'r:n:o:d:C:v:p:') 106 | except getopt.GetoptError: 107 | return natlas.RETURN_SYNTAXERR 108 | for opt, arg in opts: 109 | if (opt == '-r'): opt_root_ip = arg 110 | if (opt == '-n'): opt_node_ip = arg 111 | if (opt == '-o'): opt_output = arg 112 | if (opt == '-d'): opt_depth = arg 113 | if (opt == '-C'): opt_community = arg 114 | if (opt == '-v'): opt_vlan = arg 115 | if (opt == '-p'): opt_depth = arg 116 | 117 | if ((opt_root_ip == None) & (opt_node_ip == None)): 118 | return natlas.RETURN_SYNTAXERR 119 | 120 | if (opt_node_ip != None): 121 | return single_node(natlas_obj, opt_node_ip, opt_root_ip, opt_community, opt_vlan, opt_port, opt_output) 122 | 123 | return all_nodes(natlas_obj, opt_root_ip, opt_output, opt_depth) 124 | 125 | 126 | def get_arp_entry_for_mac(arps, macaddr): 127 | for a in arps: 128 | if (a.mac == macaddr): 129 | return a 130 | return None 131 | 132 | 133 | def create_csv_file(filepath, colnames): 134 | f = None 135 | if (filepath != None): 136 | try: 137 | f = open(filepath, 'w') 138 | f.write('%s\n' % colnames) 139 | except: 140 | print('Unable to open CSV output file "%s"' % filepath) 141 | return f 142 | 143 | 144 | def all_nodes(natlas_obj, opt_root_ip, opt_output, opt_depth): 145 | # discover the network 146 | natlas_obj.set_discover_maxdepth(opt_depth) 147 | natlas_obj.set_verbose(1) 148 | natlas_obj.discover_network(opt_root_ip, 0) 149 | 150 | network_macs = [] 151 | network_arps = [] 152 | 153 | # iterate through each discovered node 154 | natlas_nodes = natlas_obj.get_discovered_nodes() 155 | for node in natlas_nodes: 156 | # get the switch MAC table 157 | nip = natlas_obj.get_node_ip(node) 158 | print('Collecting MACs from %s...' % nip) 159 | try: 160 | macs = natlas_obj.get_switch_macs(nip, verbose=1) 161 | network_macs.extend(macs) 162 | except Exception as e: 163 | print(e) 164 | pass 165 | 166 | # get the ARP table for the router 167 | print('Collecting ARPs from %s...' % nip) 168 | try: 169 | arps = natlas_obj.get_arp_table(nip) 170 | network_arps.extend(arps) 171 | except Exception as e: 172 | print(e) 173 | pass 174 | 175 | print() 176 | print('Found %i MAC entries' % len(network_macs)) 177 | print('Found %i ARP entries' % len(network_arps)) 178 | print() 179 | print('NODE_NAME NODE_IP PORT IP MAC VLAN DNS') 180 | print('--------- ------- ---- -- --- ---- ---') 181 | 182 | # create the output csv file 183 | f = create_csv_file(opt_output, 'NODE_NAME,NODE_IP,PORT,IP,MAC,VLAN,DNS') 184 | for m in network_macs: 185 | arp = get_arp_entry_for_mac(network_arps, m.mac) 186 | ip = '' 187 | interf = '' 188 | dns = '' 189 | if (arp != None): 190 | ip = arp.ip 191 | interf = str(arp.interf).lstrip('Vl') 192 | try: 193 | dns = socket.gethostbyaddr(ip)[0] 194 | except: 195 | pass 196 | print('{:<20} {:<15} {:<8} {:<14} {:<5} {:<8} {:}'.format(m.node_host, m.node_ip, m.port, ip, m.mac, interf, dns)) 197 | 198 | if (f != None): 199 | f.write('"%s","%s","%s","%s","%s","%s","%s"\n' % (m.node_host, m.node_ip, m.port, ip, m.mac, interf, dns)) 200 | 201 | if (f != None): 202 | f.close() 203 | 204 | return natlas.RETURN_OK 205 | 206 | 207 | def single_node(natlas_obj, opt_devip, opt_routerip, opt_community, opt_vlan, opt_port, opt_output): 208 | if ((opt_devip == None) | (opt_community == None)): 209 | return natlas.RETURN_SYNTAXERR 210 | if (opt_routerip == None): 211 | opt_routerip = opt_devip 212 | 213 | # set some snmp credentials for us to use 214 | natlas_obj.snmp_add_credential(2, opt_community) 215 | 216 | # get the switch MAC table 217 | print('\nCollecting MACs from %s...' % opt_devip) 218 | try: 219 | macs = natlas_obj.get_switch_macs(opt_devip, vlan=opt_vlan, port=opt_port, verbose=1) 220 | except Exception as e: 221 | print(e) 222 | return natlas.RETURN_ERR 223 | 224 | # get the ARP table for the router 225 | print('\nCollecting ARPs from %s...' % opt_routerip) 226 | try: 227 | arps = natlas_obj.get_arp_table(opt_routerip) 228 | except Exception as e: 229 | print(e) 230 | return natlas.RETURN_ERR 231 | 232 | print() 233 | print('Found %i MAC entries' % len(macs)) 234 | print('Found %i ARP entries' % len(arps)) 235 | print() 236 | print('PORT IP MAC VLAN DNS') 237 | print('---- -- --- ---- ---') 238 | 239 | f = create_csv_file(opt_output, 'NODE_NAME,NODE_IP,PORT,IP,MAC,VLAN,DNS') 240 | 241 | for m in macs: 242 | arp = get_arp_entry_for_mac(arps, m.mac) 243 | ip = '' 244 | interf = '' 245 | dns = '' 246 | if (arp != None): 247 | ip = arp.ip 248 | interf = str(arp.interf).lstrip('Vl') 249 | if ((opt_vlan != None) & (interf != opt_vlan)): 250 | continue 251 | try: 252 | dns = socket.gethostbyaddr(ip)[0] 253 | except: 254 | pass 255 | print('{:<8} {:<14} {:<5} {:<8} {:}'.format(m.port, ip, m.mac, interf, dns)) 256 | 257 | if (f != None): 258 | f.write('"","%s","%s","%s","%s","%s","%s"\n' % (opt_devip, m.port, ip, m.mac, interf, dns)) 259 | 260 | if (f != None): 261 | f.close() 262 | 263 | return natlas.RETURN_OK 264 | 265 | -------------------------------------------------------------------------------- /modules/get-mac-addresses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | natlas 4 | 5 | Michael Laforest 6 | mjlaforest@gmail.com 7 | 8 | Copyright (C) 2015-2018 Michael Laforest 9 | 10 | This program is free software; you can redistribute it and/or 11 | modify it under the terms of the GNU General Public License 12 | as published by the Free Software Foundation; either version 2 13 | of the License, or (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 23 | ''' 24 | 25 | import sys 26 | import getopt 27 | import natlas 28 | 29 | def mod_load(mod): 30 | mod.name = 'get-mac-table' 31 | mod.version = '0.1' 32 | mod.author = 'Michael Laforest' 33 | mod.authoremail = 'mjlaforest@gmail.com' 34 | mod.about = 'Get MAC table from device' 35 | mod.syntax = '-n -c [-m ] [-p ] [-v ]' 36 | mod.preload_conf = 0 37 | mod.help = ''' 38 | Query a switch and collect all VLANs and MAC addresses. 39 | The output can be filtered down using the -m, -p, and -v options. 40 | ''' 41 | mod.example = ''' 42 | Get all MAC addresses on ports Gi4/0/* 43 | 44 | # get-mac-table -n 10.10.10.10 -c public -p "Gi4/0/.*" 45 | 46 | VLAN Name 47 | 1 default 48 | 10 Data 49 | 50 | Collecting MACS... 51 | 52 | 1..10..... 53 | PORT MAC VLAN VLAN_Name 54 | Gi4/0/5 c472.95db.318a 1 default 55 | Gi4/0/39 0023.2477.2d4b 10 Data 56 | Gi4/0/24 00a3.d1e6.0b71 10 Data 57 | Gi4/0/12 00f8.2c07.bad7 10 Data 58 | Gi4/0/42 00f8.2c07.bb7f 10 Data 59 | Gi4/0/40 00f8.2c07.bbed 10 Data 60 | ''' 61 | return 1 62 | 63 | def mod_entry(natlas_obj, argv): 64 | opt_ip = None 65 | opt_community = None 66 | opt_mac = None 67 | opt_port = None 68 | opt_vlan = None 69 | try: 70 | opts, args = getopt.getopt(argv, 'n:c:m:p:v:') 71 | except getopt.GetoptError: 72 | return natlas.RETURN_ERR 73 | for opt, arg in opts: 74 | if (opt == '-n'): opt_ip = arg 75 | if (opt == '-c'): opt_community = arg 76 | if (opt == '-m'): opt_mac = arg 77 | if (opt == '-p'): opt_port = arg 78 | if (opt == '-v'): opt_vlan = arg 79 | 80 | if ((opt_ip == None) | (opt_community == None)): 81 | return natlas.RETURN_ERR 82 | 83 | # set some snmp credentials for us to use 84 | natlas_obj.snmp_add_credential(2, opt_community) 85 | 86 | # get the switch VLANs 87 | try: 88 | vlans = natlas_obj.get_switch_vlans(opt_ip) 89 | except Exception as e: 90 | print(e) 91 | return natlas.RETURN_ERR 92 | print('VLAN Name') 93 | for vlan in vlans: 94 | print('{:<8} {:}'.format(vlan.id, vlan.name)) 95 | 96 | if (opt_vlan != None): 97 | valid_vlan = 0 98 | for vlan in vlans: 99 | if (str(vlan.id) == opt_vlan): 100 | valid_vlan = 1 101 | break 102 | if (valid_vlan == 0): 103 | print('\n[ERROR] VLAN %s does not exist on this device.' % opt_vlan) 104 | return natlas.RETURN_OK 105 | 106 | # get the switch MAC table 107 | print('\nCollecting MACs...') 108 | macs = natlas_obj.get_switch_macs(opt_ip, mac=opt_mac, port=opt_port, vlan=opt_vlan, verbose=1) 109 | 110 | # print the MAC table 111 | print('\n\n') 112 | print('PORT MAC VLAN VLAN_Name') 113 | print('---- --- ---- ---------') 114 | for mac in macs: 115 | vlan_name = '' 116 | for vlan in vlans: 117 | if (vlan.id == mac.vlan): 118 | vlan_name = vlan.name 119 | print('{:<8} {:<14} {:<8} {:}'.format(mac.port, mac.mac, mac.vlan, vlan_name)) 120 | 121 | print() 122 | print('Found %i VLANs' % len(vlans)) 123 | print('Found %i MAC addresses' % len(macs)) 124 | return natlas.RETURN_OK 125 | 126 | -------------------------------------------------------------------------------- /modules/newconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas-cli.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | import getopt 29 | import os 30 | import natlas 31 | 32 | def mod_load(mod): 33 | mod.name = 'newconfig' 34 | mod.version = '0.1' 35 | mod.author = 'Michael Laforest' 36 | mod.authoremail = 'mjlaforest@gmail.com' 37 | mod.notimer = 1 38 | mod.about = 'Generate a new config' 39 | mod.syntax = '' 40 | mod.help = 'Generate a new configuration file.' 41 | mod.preload_conf = 0 42 | return 1 43 | 44 | def mod_entry(natlas_obj, argv): 45 | print(natlas_obj.config_generate()) 46 | return natlas.RETURN_OK 47 | -------------------------------------------------------------------------------- /modules/showconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas-cli.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | import getopt 29 | import os 30 | import natlas 31 | 32 | def mod_load(mod): 33 | mod.name = 'showconfig' 34 | mod.version = '0.1' 35 | mod.author = 'Michael Laforest' 36 | mod.authoremail = 'mjlaforest@gmail.com' 37 | mod.notimer = 1 38 | mod.about = 'Display the config' 39 | mod.syntax = '[-c ]' 40 | mod.help = 'Print the configuration file to the console.' 41 | mod.preload_conf = 0 42 | return 1 43 | 44 | def mod_entry(natlas_obj, argv): 45 | opt_conf = './natlas.conf' 46 | try: 47 | opts, args = getopt.getopt(argv, 'c:') 48 | except getopt.GetoptError: 49 | return natlas.RETURN_SYNTAXERR 50 | for opt, arg in opts: 51 | if (opt == '-c'): 52 | opt_conf = arg 53 | 54 | try: 55 | conf = open(opt_conf, 'r').read() 56 | except: 57 | print('Unable to open config file.') 58 | return 59 | print('%s' % conf) 60 | 61 | return natlas.RETURN_OK 62 | 63 | -------------------------------------------------------------------------------- /modules/tracemac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas-cli.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | import getopt 29 | import os 30 | import natlas 31 | 32 | HOP_LIMIT = 1000 33 | gnatlas = None 34 | visited_ips = [] 35 | 36 | def mod_load(mod): 37 | mod.name = 'tracemac' 38 | mod.version = '0.1' 39 | mod.author = 'Michael Laforest' 40 | mod.authoremail = 'mjlaforest@gmail.com' 41 | mod.about = 'Trace a MAC address through a layer 2 network.' 42 | mod.syntax = '-n -m ' 43 | mod.help = ''' 44 | Trace a MAC address through a layer 2 network. 45 | 46 | Define a switch on that network to begin the trace using -n. tracemac will use the MAC and CDP/LLDP tables to iteratively trace the MAC defined with -m until the host port is located. 47 | ''' 48 | mod.example = ''' 49 | # tracemac -n 10.10.20.1 -m d4be.d939.4fd2 50 | 51 | HOP NODE IP NODE NAME VLAN PORT REMOTE NODE IP REMOTE NODE NAME 52 | --- ------- --------- ---- ---- -------------- ---------------- 53 | 1 10.10.20.1 SwitchA 10 Gi1/0/10 10.10.20.2 SwitchB 54 | 2 10.10.20.2 SwitchB 10 Gi1/0/22 10.10.20.5 SwitchE 55 | 2 10.10.20.5 SwitchE 10 Gi0/8 56 | 57 | FOUND 58 | 59 | MAC Address: d4be.d939.4fd3 60 | Node IP: 10.10.20.5 61 | Node Name: SwitchE 62 | Port: Gi0/8 63 | ''' 64 | mod.notimer = 0 65 | mod.preload_conf = 1 66 | mod.require_api = '0.11' 67 | return 1 68 | 69 | def mod_entry(natlas_obj, argv): 70 | global gnatlas 71 | gnatlas = natlas_obj 72 | 73 | opt_ip = None 74 | opt_community = None 75 | try: 76 | opts, args = getopt.getopt(argv, 'n:m:') 77 | except getopt.GetoptError: 78 | return natlas.RETURN_ERR 79 | for opt, arg in opts: 80 | if (opt == '-n'): opt_ip = arg 81 | if (opt == '-m'): opt_mac = arg 82 | 83 | print('HOP NODE IP NODE NAME VLAN PORT REMOTE NODE IP REMOTE NODE NAME') 84 | print('--- ------- --------- ---- ---- -------------- ----------------') 85 | 86 | try: 87 | node, port = trace_node(opt_ip, opt_mac, 1) 88 | except Exception as e: 89 | print('[ERROR] %s' % e) 90 | return natlas.RETURN_OK 91 | 92 | print() 93 | if (node == None): 94 | print('NOT FOUND') 95 | else: 96 | print('FOUND\n') 97 | print('MAC Address: %s' % opt_mac) 98 | print(' Node IP: %s' % node.get_ipaddr()) 99 | print(' Node Name: %s' % node.name) 100 | print(' Port: %s' % port) 101 | 102 | return natlas.RETURN_OK 103 | 104 | def trace_node(node_ip, macaddr, depth): 105 | global visited_ips 106 | if (node_ip in visited_ips): 107 | raise Exception('Loop encountered.') 108 | visited_ips.append(node_ip) 109 | 110 | if (depth > HOP_LIMIT): 111 | # probably some weird problem 112 | raise Exception('Hop count too high. Terminating trace.') 113 | 114 | sys.stdout.write('{:<5} {:<15} '.format(depth, node_ip)) 115 | sys.stdout.flush() 116 | 117 | node = gnatlas.new_node(node_ip) 118 | macs = gnatlas.get_switch_macs(node=node) 119 | gnatlas.query_node(node, get_name=True) 120 | 121 | match = None 122 | for mac in macs: 123 | if (mac.mac == macaddr): 124 | node_name = '' 125 | if (node.name != None): 126 | node_name = node.name 127 | sys.stdout.write('{:<25} {:<7} {:<12} '.format(node_name, mac.vlan, mac.port)) 128 | sys.stdout.flush() 129 | match = mac 130 | break 131 | 132 | if (match == None): 133 | return (None, None) 134 | 135 | port = node.shorten_port_name(mac.port) 136 | neighbors = gnatlas.get_neighbors(node) 137 | 138 | match = None 139 | for n in neighbors: 140 | if (n.local_port == port): 141 | sys.stdout.write('{:<15} {:<25}'.format(n.remote_ip, n.remote_name)) 142 | sys.stdout.flush() 143 | match = n 144 | break 145 | 146 | print() 147 | if (match == None): 148 | # MAC is on this node 149 | return (node, mac.port) 150 | 151 | # found MAC on the same port as a neighbor - trace that node 152 | return trace_node(match.remote_ip, macaddr, depth+1) 153 | 154 | -------------------------------------------------------------------------------- /natlas-cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas-cli.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | import getopt 29 | import datetime 30 | import os 31 | import re 32 | from timeit import default_timer as timer 33 | from distutils.version import LooseVersion 34 | 35 | import natlas 36 | 37 | DEFAULT_OPT_DEPTH = 100 38 | DEFAULT_OPT_TITLE = 'natlas Diagram' 39 | DEFAULT_OPT_CONF = './natlas.conf' 40 | 41 | class natlas_mod: 42 | def __init__(self): 43 | self.filename = '' 44 | self.name = '' 45 | self.version = '' 46 | self.author = '' 47 | self.authoremail = '' 48 | self.syntax = None 49 | self.about = None 50 | self.help = None 51 | self.example = None 52 | self.entryfunc = None 53 | self.notimer = 0 54 | self.require_api = None 55 | self.preload_conf = 1 56 | def __str__(self): 57 | return ('' % (self.name, self.version, self.author)) 58 | def __repr__(self): 59 | return self.__str__() 60 | 61 | try: 62 | natlas_obj = natlas.natlas() 63 | except Exception as e: 64 | print(e) 65 | exit() 66 | 67 | def main(argv): 68 | if (len(argv) < 1): 69 | print_banner() 70 | print_syntax() 71 | return 72 | 73 | if (argv[0] != 'newconfig'): 74 | print_banner() 75 | 76 | modules = load_modules() 77 | 78 | if (argv[0] == 'list'): 79 | list_mods(modules) 80 | return 81 | if (argv[0] == 'info'): 82 | print_mod_info(modules, argv[1]) 83 | return 84 | if (argv[0] == 'help'): 85 | if (len(argv) < 2): 86 | print_syntax() 87 | return 88 | print_mod_help(modules, argv[1]) 89 | return 90 | if (argv[0] == 'syntax'): 91 | print_mod_syntax(modules, argv[1]) 92 | return 93 | 94 | mod = get_mod(modules, argv[0]) 95 | if (mod != None): 96 | exec_mod(mod, argv[1:]) 97 | return 98 | 99 | print_syntax() 100 | 101 | return 102 | 103 | def print_syntax(): 104 | print('Usage:\n' 105 | ' natlas-cli.py list - Display available modules\n' 106 | ' natlat-cli.py info - Display information about the module\n' 107 | ' natlat-cli.py help - Display help for module\n' 108 | ' natlat-cli.py syntax - Display syntax for module\n') 109 | 110 | def print_banner(): 111 | print('natlas v%s' % natlas.__version__) 112 | print('Michael Laforest ') 113 | print('Python %s\n' % sys.version.split(' ')[0]) 114 | 115 | def load_modules(): 116 | sys.path.insert(0, './modules') 117 | ret = [] 118 | for f in os.listdir('./modules'): 119 | if (f[-3:] == '.py'): 120 | mod = None 121 | try: 122 | mod = __import__(f[:-3], ['mod_load', 'mod_entry']) 123 | except Exception as e: 124 | print(e) 125 | continue 126 | 127 | if (hasattr(mod, 'mod_load') == 0): 128 | print('[ERROR] No mod_load() for %s' % f) 129 | continue 130 | 131 | m = natlas_mod() 132 | if (mod.mod_load(m) == 0): 133 | print('[ERROR] mod_load() returned an error for %s' % f) 134 | continue 135 | m.filename = f 136 | m.entryfunc = mod.mod_entry 137 | ret.append(m) 138 | 139 | return ret 140 | 141 | def exec_mod(module, argv): 142 | if (does_mod_accept_api(module) == 0): 143 | print('Module is disabled.') 144 | return 0 145 | 146 | start = timer() 147 | 148 | try: 149 | natlas_obj = natlas.natlas() 150 | except Exception as e: 151 | print('[ERROR] %s' % e) 152 | return 0 153 | 154 | if (module.preload_conf == 1): 155 | try: 156 | argv, opt_conf = argv_get_conf(argv) 157 | except Exception as e: 158 | print('[ERROR] %s' % e) 159 | return 0 160 | 161 | try: 162 | natlas_obj.config_load(opt_conf) 163 | except Exception as e: 164 | print(e) 165 | return 0 166 | 167 | modret = module.entryfunc(natlas_obj, argv) 168 | if (modret == natlas.RETURN_SYNTAXERR): 169 | print('Invalid syntax for module. See "syntax %s" for more info.' % module.name) 170 | return 0 171 | if (modret == natlas.RETURN_ERR): 172 | print('[ERROR] Error encountered in module.') 173 | return 0 174 | 175 | if (module.notimer == 0): 176 | s = timer() - start 177 | h=int(s/3600) 178 | m=int((s-(h*3600))/60) 179 | s=s-(int(s/3600)*3600)-(m*60) 180 | print('\nCompleted in %i:%i:%.2fs' % (h, m, s)) 181 | 182 | return 1 183 | 184 | def argv_get_conf(argv): 185 | opt_conf = DEFAULT_OPT_CONF 186 | for i in range(0, len(argv)): 187 | if (argv[i] == '-c'): 188 | if ((i+1) >= len(argv)): 189 | raise Exception('-c used but no file specified') 190 | opt_conf = argv[i+1] 191 | del argv[i+1] 192 | del argv[i] 193 | break 194 | return (argv, opt_conf) 195 | 196 | def list_mods(modules): 197 | print('Module Version Status Author About') 198 | print('------ ------- ------ ------ -----') 199 | for m in modules: 200 | status = 'Disabled' 201 | accept = does_mod_accept_api(m) 202 | if (accept == 1): 203 | status = 'OK' 204 | elif (accept == 2): 205 | status = 'OK*' 206 | print('{:<22} {:<8} {:<8} {:<24} {:}'.format(m.name, m.version, status, m.author, m.about)) 207 | print() 208 | if (re.match('^.*-dev.*$', natlas.__version__)): 209 | print('* Development version %s overrides disabled modules.' % natlas.__version__) 210 | print() 211 | return 212 | 213 | def get_mod(modules, mod_name): 214 | for m in modules: 215 | if (m.name == mod_name): 216 | return m 217 | return None 218 | 219 | def does_mod_accept_api(mod): 220 | if (mod.require_api == None): 221 | # no minimum version specified 222 | return 1 223 | accepted = (LooseVersion(mod.require_api) <= LooseVersion(natlas.__version__)) 224 | if (accepted == 0): 225 | if (re.match('^.*-dev.*$', natlas.__version__)): 226 | return 2 227 | return 0 228 | return 1 229 | 230 | def print_mod_info(modules, mod): 231 | m = get_mod(modules, mod) 232 | if (m == None): 233 | print('Invalid module') 234 | return 235 | require_api = 'Not specified' 236 | status = 'OK (default)' 237 | if (m.require_api != None): 238 | require_api = m.require_api 239 | accept = does_mod_accept_api(m) 240 | if (accept == 0): 241 | status = 'Disabled (requires newer version of natlas)' 242 | elif (accept == 2): 243 | status = 'OK (using development API)' 244 | print(' Module: %s' % m.name) 245 | print(' Version: %s' % m.version) 246 | print(' Author: %s <%s>' % (m.author, m.authoremail)) 247 | print(' File: modules/%s' % m.filename) 248 | print('Requires natlas: %s' % require_api) 249 | print(' Status: %s' % status) 250 | print() 251 | 252 | def print_mod_help(modules, mod): 253 | m = get_mod(modules, mod) 254 | if (m == None): 255 | print('Invalid module') 256 | return 257 | 258 | print('MODULE\n') 259 | print(' %s v%s' % (m.name, m.version)) 260 | print(' %s <%s>' % (m.author, m.authoremail)) 261 | if (m.about != None): 262 | print('\nABOUT\n') 263 | print_indented(m.about) 264 | if (m.syntax != None): 265 | print('\nSYNTAX\n') 266 | if (type(m.syntax) == type([])): 267 | for s in m.syntax: 268 | print(' %s %s' % (m.name, s)) 269 | else: 270 | print(' %s %s' % (m.name, m.syntax)) 271 | if (m.help != None): 272 | print('\nDETAILS\n') 273 | print_indented(m.help) 274 | if (m.example != None): 275 | print('\nEXAMPLE\n') 276 | print_indented(m.example, 0) 277 | print() 278 | 279 | def print_mod_syntax(modules, mod): 280 | m = get_mod(modules, mod) 281 | if (m == None): 282 | print('Invalid module') 283 | return 284 | if (type(m.syntax) == type([])): 285 | for s in m.syntax: 286 | print('%s %s' % (m.name, s)) 287 | else: 288 | print('%s %s' % (m.name, m.syntax)) 289 | 290 | def print_indented(str, wrap=1): 291 | lines = str.lstrip().splitlines() 292 | for line in lines: 293 | line = line.lstrip() 294 | if (line == ''): 295 | print() 296 | continue 297 | if (wrap == 1): 298 | wlines = [line[i:i+70] for i in range(0, len(line), 70)] 299 | for wline in wlines: 300 | print(' ', wline) 301 | else: 302 | print(' ', line) 303 | 304 | if __name__ == "__main__": 305 | main(sys.argv[1:]) 306 | 307 | -------------------------------------------------------------------------------- /natlas.conf: -------------------------------------------------------------------------------- 1 | { 2 | "snmp" : [ 3 | { "community":"private", "ver":2 }, 4 | { "community":"public", "ver":2 } 5 | ], 6 | "domains" : [ 7 | ".company.net", 8 | ".company.com" 9 | ], 10 | "discover" : [ 11 | "permit ip 10.0.0.0/8", 12 | "permit ip 192.168.1.0/24", 13 | "permit ip 0.0.0.0/32" 14 | ], 15 | "diagram" : { 16 | "node_text_size" : 10, 17 | "link_text_size" : 9, 18 | "title_text_size" : 15, 19 | "get_stack_members" : 0, 20 | "get_vss_members" : 0, 21 | "expand_stackwise" : 0, 22 | "expand_vss" : 1, 23 | "expand_lag" : 1, 24 | "group_vpc" : 0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /natlas/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .natlas import natlas 3 | from .network import natlas_network 4 | 5 | from .natlas import RETURN_SYNTAXERR, RETURN_ERR, RETURN_OK 6 | -------------------------------------------------------------------------------- /natlas/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.12.1" 2 | -------------------------------------------------------------------------------- /natlas/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | config.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import json 28 | import sys 29 | 30 | class natlas_config_diagram: 31 | node_text_size = 8 32 | link_text_size = 7 33 | title_text_size = 15 34 | get_stack_members = False 35 | get_vss_members = False 36 | expand_stackwise = False 37 | expand_vss = False 38 | expand_lag = True 39 | group_vpc = False 40 | node_text = '{node.name}
' \ 41 | '{node.ip}
' \ 42 | '<%if {node.ios}: {node.ios}
%>' \ 43 | '<%if {node.plat}: {node.plat}
%>' \ 44 | '<%if ("{node.serial}"!=None)&({node.vss.enabled}==0)&({node.stack.enabled}==0): {node.serial}
%>' \ 45 | '<%if ({node.stack.enabled}==1)&({config.diagram.expand_stackwise}==1): {stack.serial}
%>' \ 46 | '<%if {node.vss.enabled}&({config.diagram.expand_vss}==1): {vss.serial}
%>' \ 47 | '<%if ({node.vss.enabled}==1)&({config.diagram.expand_vss}==0): VSS {node.vss.domain}
%>' \ 48 | '<%if {node.vss.enabled}&({config.diagram.expand_vss}==0): VSS 0 - {node.vss.members[0].plat} - {node.vss.members[0].serial}
VSS 1 - {node.vss.members[1].plat} - {node.vss.members[1].serial}
%>' \ 49 | '<%if {node.bgp_las}: BGP {node.bgp_las}
%>' \ 50 | '<%if {node.ospf_id}: OSPF {node.ospf_id}
%>' \ 51 | '<%if {node.hsrp_pri}: HSRP VIP {node.hsrp_vip}
HSRP Pri {node.hsrp_pri}
%>' \ 52 | '<%if {node.stack.enabled}: Stackwise {node.stack.count}
%>' \ 53 | '<%stack SW {stack.num} - {stack.plat} {stack.serial} ({stack.role})
%>' \ 54 | '<%loopback {lo.name} - {lo.ip}
%>' \ 55 | '<%svi VLAN {svi.vlan} - {svi.ip}
%>' 56 | 57 | class natlas_discover_acl: 58 | ''' 59 | Define an ACL entry for the 'discover' config block. 60 | Defined in the form: 61 | 62 | Where 63 | = permit, deny, leaf, nop 64 | = ip, host 65 | = string 66 | ''' 67 | all_actions = [ ';', 'permit', 'deny', 'leaf', 'include' ] 68 | all_types = [ ';', 'ip', 'host', 'software', 'platform', 'serial' ] 69 | 70 | def __init__(self, str): 71 | self.action = "nop" 72 | self.type = "nop" 73 | self.str = "nop" 74 | 75 | t = list(filter(None, str.split())) 76 | if (len(t) < 3): 77 | raise Exception('Invalid ACL entry: "%s"' % str) 78 | 79 | self.action = t[0] 80 | self.type = t[1] 81 | self.str = t[2] 82 | 83 | if (self.action not in self.all_actions): 84 | raise Exception('Invalid ACL entry: "%s"; %s' % (str, self.action)) 85 | if (self.type not in self.all_types): 86 | raise Exception('Invalid ACL entry: "%s"; %s' % (str, self.type)) 87 | 88 | def __repr__(self): 89 | return '<%s %s %s>' % (self.action, self.type, self.str) 90 | 91 | class natlas_config: 92 | def __init__(self): 93 | self.host_domains = [] 94 | self.snmp_creds = [] 95 | self.discover_acl = [] 96 | self.diagram = natlas_config_diagram() 97 | 98 | def load(self, filename): 99 | # load config 100 | json_data = self.__load_json_conf(filename) 101 | if (json_data == None): 102 | return 0 103 | 104 | self.host_domains = json_data['domains'] 105 | self.snmp_creds = json_data['snmp'] 106 | 107 | # parse 'discover' block ACL entries 108 | for acl in json_data['discover']: 109 | try: 110 | entry = natlas_discover_acl(acl) 111 | except Exception as e: 112 | print(e) 113 | return 0 114 | 115 | self.discover_acl.append(entry) 116 | 117 | json_diagram = json_data.get('diagram', None) 118 | if (json_diagram != None): 119 | self.diagram.node_text_size = json_diagram.get('node_text_size', 8) 120 | self.diagram.link_text_size = json_diagram.get('link_text_size', 7) 121 | self.diagram.title_text_size = json_diagram.get('title_text_size', 15) 122 | self.diagram.get_stack_members = json_diagram.get('get_stack_members', False) 123 | self.diagram.get_vss_members = json_diagram.get('get_vss_members', False) 124 | self.diagram.expand_stackwise = json_diagram.get('expand_stackwise', False) 125 | self.diagram.expand_vss = json_diagram.get('expand_vss', False) 126 | self.diagram.expand_lag = json_diagram.get('expand_lag', True) 127 | self.diagram.group_vpc = json_diagram.get('group_vpc', False) 128 | self.diagram.node_text = json_diagram.get('node_text', self.diagram.node_text) 129 | 130 | return 1 131 | 132 | def __load_json_conf(self, json_file): 133 | json_data = None 134 | fd = open(json_file) 135 | json_data = fd.read() 136 | fd.close() 137 | json_data = json.loads(json_data) 138 | return json_data 139 | 140 | def generate_new(self): 141 | return '{\n' \ 142 | ' "snmp" : [\n' \ 143 | ' { "community":"private", "ver":2 },\n' \ 144 | ' { "community":"public", "ver":2 }\n' \ 145 | ' ],\n' \ 146 | ' "domains" : [\n' \ 147 | ' ".company.net",\n' \ 148 | ' ".company.com"\n' \ 149 | ' ],\n' \ 150 | ' "discover" : [\n' \ 151 | ' "permit ip 10.0.0.0/8",\n' \ 152 | ' "permit ip 192.168.1.0/24",\n' \ 153 | ' "permit ip 0.0.0.0/32"\n' \ 154 | ' ],\n' \ 155 | ' "diagram" : {\n' \ 156 | ' "node_text_size" : 10,\n' \ 157 | ' "link_text_size" : 9,\n' \ 158 | ' "title_text_size" : 15,\n' \ 159 | ' "get_stack_members" : 0,\n' \ 160 | ' "get_vss_members" : 0,\n' \ 161 | ' "expand_stackwise" : 0,\n' \ 162 | ' "expand_vss" : 0,\n' \ 163 | ' "expand_lag" : 1,\n' \ 164 | ' "group_vpc" : 0\n' \ 165 | ' }\n' \ 166 | '}' 167 | 168 | 169 | def validate_config(self, filename): 170 | print('Validating config...') 171 | json_data = self.__load_json_conf(filename) 172 | if (json_data == None): 173 | print('Could not load config.') 174 | return 0 175 | 176 | ret = 0 177 | 178 | ret += self.__validate_config_snmp(json_data) 179 | ret += self.__validate_config_domains(json_data) 180 | ret += self.__validate_config_discover(json_data) 181 | ret += self.__validate_config_diagram(json_data) 182 | 183 | if (ret < 4): 184 | print('FAILED') 185 | else: 186 | print('PASSED') 187 | 188 | def __validate_config_snmp(self, data): 189 | sys.stdout.write('Checking snmp...') 190 | obj = None 191 | try: 192 | obj = data['snmp'] 193 | except: 194 | print('does not exist') 195 | return 0 196 | 197 | if (type(obj) != list): 198 | print('not a list') 199 | return 0 200 | 201 | for cred in obj: 202 | if (type(cred) != dict): 203 | print('list contains a non-dict (%s)' % type(cred)) 204 | return 0 205 | try: 206 | c = cred['community'] 207 | if (type(c) != str): 208 | print('community is not a string') 209 | return 0 210 | except KeyError as e: 211 | print('one or more entries does not include %s' % e) 212 | return 0 213 | try: 214 | c = cred['ver'] 215 | if (type(c) != int): 216 | print('version is not an int') 217 | return 0 218 | else: 219 | if (c != 2): 220 | print('version for \'%s\' is not supported' % cred['community']) 221 | return 0 222 | except KeyError as e: 223 | print('one or more entries does not include %s' % e) 224 | return 0 225 | print('ok') 226 | return 1 227 | 228 | def __validate_config_domains(self, data): 229 | sys.stdout.write('Checking domains...') 230 | obj = None 231 | try: 232 | obj = data['domains'] 233 | except: 234 | print('does not exist') 235 | return 0 236 | if (type(obj) != list): 237 | print('not a list') 238 | return 0 239 | for d in obj: 240 | if (type(d) != str): 241 | print('domain is not a string') 242 | return 0 243 | print('ok') 244 | return 1 245 | 246 | def __validate_config_discover(self, data): 247 | sys.stdout.write('Checking discover...') 248 | obj = None 249 | try: 250 | obj = data['discover'] 251 | except: 252 | print('does not exist') 253 | return 0 254 | if (type(obj) != list): 255 | print('not a list') 256 | return 0 257 | for d in obj: 258 | if (type(d) != str): 259 | print('ACL is not a string') 260 | return 0 261 | 262 | ace = d.split(' ') 263 | if (len(ace) < 3): 264 | print('ACE not enough params \'%s\'' % d) 265 | return 0 266 | if (ace[0] not in natlas_discover_acl.all_actions): 267 | print('ACE op \'%s\' not valid' % ace[0]) 268 | return 0 269 | if (ace[1] not in natlas_discover_acl.all_types): 270 | print('ACE cond \'%s\' not valid' % ace[1]) 271 | return 0 272 | 273 | print('ok') 274 | return 1 275 | 276 | def __validate_config_diagram(self, data): 277 | sys.stdout.write('Checking diagram...') 278 | obj = None 279 | try: 280 | obj = data['diagram'] 281 | except: 282 | print('does not exist') 283 | return 0 284 | if (type(obj) != dict): 285 | print('not a dict') 286 | return 0 287 | 288 | for nv in obj: 289 | if (nv not in ['node_text_size', 290 | 'link_text_size', 291 | 'title_text_size', 292 | 'get_stack_members', 293 | 'get_vss_members', 294 | 'expand_stackwise', 295 | 'expand_vss', 296 | 'expand_lag', 297 | 'group_vpc']): 298 | print('invalid value \'%s\'' % nv) 299 | return 0 300 | 301 | print('ok') 302 | return 1 303 | 304 | -------------------------------------------------------------------------------- /natlas/mac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | mac.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import os 28 | import re 29 | import sys 30 | 31 | from timeit import default_timer as timer 32 | from .snmp import * 33 | from .config import natlas_config 34 | from .util import * 35 | from ._version import __version__ 36 | 37 | class natlas_mac: 38 | 39 | class mac_object: 40 | def __init__(self, _host, _ip, _vlan, _mac, _port): 41 | self.node_host = _host 42 | self.node_ip = _ip 43 | self.vlan = int(_vlan) 44 | self.mac = _mac 45 | self.port = _port 46 | 47 | def __str__(self): 48 | return ('' 49 | % (self.node_host, self.node_ip, self.vlan, self.mac, self.port)) 50 | def __repr__(self): 51 | return self.__str__() 52 | 53 | 54 | def __init__(self, conf): 55 | self.config = conf 56 | 57 | 58 | def __str__(self): 59 | return ('' % len(self.macs)) 60 | def __repr__(self): 61 | return self.__str__() 62 | 63 | 64 | def get_macs(self, ip, display_progress): 65 | ''' 66 | Return array of MAC addresses from single node at IP 67 | ''' 68 | if (ip == '0.0.0.0'): 69 | return None 70 | 71 | ret_macs = [] 72 | snmpobj = natlas_snmp(ip) 73 | 74 | # find valid credentials for this node 75 | if (snmpobj.get_cred(self.config.snmp_creds) == 0): 76 | return None 77 | 78 | system_name = util.shorten_host_name(snmpobj.get_val(OID_SYSNAME), self.config.host_domains) 79 | 80 | # cache some common MIB trees 81 | vlan_vbtbl = snmpobj.get_bulk(OID_VLANS) 82 | ifname_vbtbl = snmpobj.get_bulk(OID_IFNAME) 83 | 84 | for vlan_row in vlan_vbtbl: 85 | for vlan_n, vlan_v in vlan_row: 86 | # get VLAN ID from OID 87 | vlan = natlas_snmp.get_last_oid_token(vlan_n) 88 | if (vlan >= 1002): 89 | continue 90 | vmacs = self.get_macs_for_vlan(ip, vlan, display_progress, snmpobj, system_name, ifname_vbtbl) 91 | if (vmacs != None): 92 | ret_macs.extend(vmacs) 93 | 94 | if (display_progress == 1): 95 | print('') 96 | 97 | return ret_macs 98 | 99 | 100 | def get_macs_for_vlan(self, ip, vlan, display_progress=0, snmpobj=None, system_name=None, ifname_vbtbl=None): 101 | ''' 102 | Return array of MAC addresses for a single VLAN from a single node at an IP 103 | ''' 104 | ret_macs = [] 105 | 106 | if (snmpobj == None): 107 | snmpobj = natlas_snmp(ip) 108 | if (snmpobj.get_cred(self.config.snmp_creds) == 0): 109 | return None 110 | if (ifname_vbtbl == None): 111 | ifname_vbtbl = snmpobj.get_bulk(OID_IFNAME) 112 | if (system_name == None): 113 | system_name = util.shorten_host_name(snmpobj.get_val(OID_SYSNAME), self.config.host_domains) 114 | 115 | # change our SNMP credentials 116 | old_cred = snmpobj.v2_community 117 | snmpobj.v2_community = old_cred + '@' + str(vlan) 118 | 119 | if (display_progress == 1): 120 | sys.stdout.write(str(vlan)) # found VLAN 121 | sys.stdout.flush() 122 | 123 | # get CAM table for this VLAN 124 | cam_vbtbl = snmpobj.get_bulk(OID_VLAN_CAM) 125 | portnum_vbtbl = snmpobj.get_bulk(OID_BRIDGE_PORTNUMS) 126 | ifindex_vbtbl = snmpobj.get_bulk(OID_IFINDEX) 127 | cam_match = None 128 | 129 | if (cam_vbtbl == None): 130 | # error getting CAM for VLAN 131 | return None 132 | 133 | for cam_row in cam_vbtbl: 134 | for cam_n, cam_v in cam_row: 135 | cam_entry = natlas_mac.mac_format_ascii(cam_v, 0) 136 | 137 | # find the interface index 138 | p = cam_n.getOid() 139 | portnum_oid = '%s.%i.%i.%i.%i.%i.%i' % (OID_BRIDGE_PORTNUMS, p[11], p[12], p[13], p[14], p[15], p[16]) 140 | bridge_portnum = snmpobj.cache_lookup(portnum_vbtbl, portnum_oid) 141 | 142 | # get the interface index and description 143 | try: 144 | ifidx = snmpobj.cache_lookup(ifindex_vbtbl, OID_IFINDEX + '.' + bridge_portnum) 145 | port = snmpobj.cache_lookup(ifname_vbtbl, OID_IFNAME + '.' + ifidx) 146 | except TypeError: 147 | port = 'None' 148 | 149 | mac_addr = natlas_mac.mac_format_ascii(cam_v, 1) 150 | 151 | if (display_progress == 1): 152 | sys.stdout.write('.') # found CAM entry 153 | sys.stdout.flush() 154 | 155 | entry = natlas_mac.mac_object(system_name, ip, vlan, mac_addr, port) 156 | ret_macs.append(entry) 157 | 158 | # restore SNMP credentials 159 | snmpobj.v2_community = old_cred 160 | return ret_macs 161 | 162 | 163 | # 164 | # Parse an ASCII MAC address string to a hex string. 165 | # 166 | def mac_ascii_to_hex(mac_str): 167 | mac_str = re.sub('[\.:]', '', mac_str) 168 | if (len(mac_str) != 12): 169 | return None 170 | mac_hex = '' 171 | for i in range(0, len(mac_str), 2): 172 | mac_hex += chr(int(mac_str[i:i+2], 16)) 173 | return mac_hex 174 | 175 | def mac_format_ascii(mac_hex, inc_dots): 176 | v = mac_hex.prettyPrint() 177 | return natlas_mac.mac_hex_to_ascii(v, inc_dots) 178 | 179 | def mac_hex_to_ascii(mac_hex, inc_dots): 180 | ''' 181 | Format a hex MAC string to ASCII 182 | 183 | Args: 184 | mac_hex: Value from SNMP 185 | inc_dots: 1 to format as aabb.ccdd.eeff, 0 to format aabbccddeeff 186 | 187 | Returns: 188 | String representation of the mac_hex 189 | ''' 190 | v = mac_hex[2:] 191 | ret = '' 192 | for i in range(0, len(v), 4): 193 | ret += v[i:i+4] 194 | if ((inc_dots) & ((i+4) < len(v))): 195 | ret += '.' 196 | 197 | return ret 198 | 199 | -------------------------------------------------------------------------------- /natlas/natlas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | natlas.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | ''' 28 | This file defines the natlas API. 29 | ''' 30 | 31 | import sys 32 | import re 33 | 34 | from .config import natlas_config 35 | from .network import natlas_network 36 | from .node import natlas_node, natlas_vlan, natlas_arp 37 | from .mac import natlas_mac 38 | from .output import natlas_output 39 | from .output_diagram import natlas_output_diagram 40 | from .output_catalog import natlas_output_catalog 41 | 42 | REQUIRES_PYTHON = (3, 6) 43 | 44 | # module return codes 45 | RETURN_SYNTAXERR = -1 46 | RETURN_ERR = 0 47 | RETURN_OK = 1 48 | 49 | class natlas: 50 | def __init__(self): 51 | if (sys.version_info < REQUIRES_PYTHON): 52 | raise Exception('Requires Python %i.%i' % (REQUIRES_PYTHON[0], REQUIRES_PYTHON[1])) 53 | return 54 | self.config_file = None 55 | self.config = None 56 | self.network = None 57 | self.diagram = None 58 | self.catalog = None 59 | 60 | def __try_snmp(self, node): 61 | if (node == None): return 0 62 | if (node.snmpobj == None): return 0 63 | if (node.snmpobj.success == 1): return 1 64 | if (node.try_snmp_creds(self.config.snmp_creds) == 0): 65 | raise Exception('No valid SNMP credentials for %s' % node.ip) 66 | return 1 67 | 68 | def config_generate(self): 69 | return natlas_config().generate_new() 70 | 71 | def config_validate(self, conf_file): 72 | return natlas_config().validate_config(conf_file) 73 | 74 | def config_load(self, conf_file): 75 | self.config = None 76 | c = natlas_config() 77 | c.load(conf_file) 78 | self.config = c 79 | self.config_file = conf_file 80 | 81 | # initalize objects 82 | self.network = natlas_network(self.config) 83 | 84 | def snmp_add_credential(self, snmp_ver, snmp_community): 85 | if (self.config == None): 86 | self.config = natlas_config() 87 | if (snmp_ver != 2): 88 | raise ValueError('snmp_ver is not valid') 89 | return 90 | cred = {} 91 | cred['ver'] = snmp_ver 92 | cred['community'] = snmp_community 93 | self.config.snmp_creds.append(cred) 94 | 95 | def set_discover_maxdepth(self, depth): 96 | self.network.set_max_depth(int(depth)) 97 | 98 | def set_verbose(self, verbose): 99 | self.network.set_verbose(verbose) 100 | 101 | def discover_network(self, root_ip, details): 102 | self.network.discover(root_ip) 103 | if (details == 1): 104 | self.network.discover_details() 105 | 106 | # initalize the output objects 107 | self.diagram = natlas_output_diagram(self.network) 108 | self.catalog = natlas_output_catalog(self.network) 109 | 110 | def new_node(self, node_ip): 111 | node = natlas_node(ip=node_ip) 112 | self.__try_snmp(node) 113 | return node 114 | 115 | def query_node(self, node, **get_values): 116 | # see natlas_node._node_opts in node.py for what get_values are available 117 | self.__try_snmp(node) 118 | node.opts.reset(False) 119 | for getv in get_values: 120 | setattr(node.opts, getv, get_values[getv]) 121 | node.query_node() 122 | return 123 | 124 | def write_diagram(self, output_file, diagram_title): 125 | self.diagram.generate(output_file, diagram_title) 126 | 127 | def write_catalog(self, output_file): 128 | self.catalog.generate(output_file) 129 | 130 | def get_switch_vlans(self, switch_ip): 131 | node = natlas_node(switch_ip) 132 | if (node.try_snmp_creds(self.config.snmp_creds) == 0): 133 | return [] 134 | return node.get_vlans() 135 | 136 | def get_switch_macs(self, switch_ip=None, node=None, vlan=None, mac=None, port=None, verbose=0): 137 | ''' 138 | Get the CAM table from a switch. 139 | 140 | Args: 141 | switch_ip IP address of the device 142 | node natlas_node from new_node() 143 | vlan Filter results by VLAN 144 | MAC Filter results by MAC address (regex) 145 | port Filter results by port (regex) 146 | verbose Display progress to stdout 147 | 148 | switch_ip or node is required 149 | 150 | Return: 151 | Array of natlas_mac objects 152 | ''' 153 | if (switch_ip == None): 154 | if (node == None): 155 | raise Exception('get_switch_macs() requires switch_ip or node parameter') 156 | return None 157 | switch_ip = node.get_ipaddr() 158 | 159 | mac_obj = natlas_mac(self.config) 160 | 161 | if (vlan == None): 162 | # get all MACs 163 | macs = mac_obj.get_macs(switch_ip, verbose) 164 | else: 165 | # get MACs only for one VLAN 166 | macs = mac_obj.get_macs_for_vlan(switch_ip, vlan, verbose) 167 | 168 | if ((mac == None) & (port == None)): 169 | return macs if macs else [] 170 | 171 | # filter results 172 | ret = [] 173 | for m in macs: 174 | if (mac != None): 175 | if (re.match(mac, m.mac) == None): 176 | continue 177 | if (port != None): 178 | if (re.match(port, m.port) == None): 179 | continue 180 | ret.append(m) 181 | return ret 182 | 183 | def get_discovered_nodes(self): 184 | return self.network.nodes 185 | 186 | def get_node_ip(self, node): 187 | return node.get_ipaddr() 188 | 189 | def get_arp_table(self, switch_ip, ip=None, mac=None, interf=None, arp_type=None): 190 | ''' 191 | Get the ARP table from a switch. 192 | 193 | Args: 194 | switch_ip IP address of the device 195 | ip Filter results by IP (regex) 196 | mac Filter results by MAC (regex) 197 | interf Filter results by INTERFACE (regex) 198 | arp_type Filter results by ARP Type 199 | 200 | Return: 201 | Array of natlas_arp objects 202 | ''' 203 | node = natlas_node(switch_ip) 204 | if (node.try_snmp_creds(self.config.snmp_creds) == 0): 205 | return [] 206 | arp = node.get_arp_table() 207 | if (arp == None): 208 | return [] 209 | 210 | if ((ip == None) & (mac == None) & (interf == None) & (arp_type == None)): 211 | # no filtering 212 | return arp 213 | 214 | interf = str(interf) if vlan else None 215 | 216 | # filter the result table 217 | ret = [] 218 | for a in arp: 219 | if (ip != None): 220 | if (re.match(ip, a.ip) == None): 221 | continue 222 | if (mac != None): 223 | if (re.match(mac, a.mac) == None): 224 | continue 225 | if (interf != None): 226 | if (re.match(interf, str(a.interf)) == None): 227 | continue 228 | if (arp_type != None): 229 | if (re.match(arp_type, a.arp_type) == None): 230 | continue 231 | ret.append(a) 232 | return ret 233 | 234 | def get_neighbors(self, node): 235 | self.__try_snmp(node) 236 | cdp = node.get_cdp_neighbors() 237 | lldp = node.get_lldp_neighbors() 238 | return cdp+lldp 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /natlas/network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | network.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | from timeit import default_timer as timer 28 | from .config import natlas_config 29 | from .util import * 30 | from .node import * 31 | 32 | DCODE_ROOT = 0x01 33 | DCODE_ERR_SNMP = 0x02 34 | DCODE_DISCOVERED = 0x04 35 | DCODE_STEP_INTO = 0x08 36 | DCODE_CDP = 0x10 37 | DCODE_LLDP = 0x20 38 | DCODE_INCLUDE = 0x40 39 | DCODE_LEAF = 0x80 40 | 41 | DCODE_ROOT_STR = '[root]' 42 | DCODE_ERR_SNMP_STR = '!' 43 | DCODE_DISCOVERED_STR = '+' 44 | DCODE_STEP_INTO_STR = '>' 45 | DCODE_CDP_STR = '[ cdp]' 46 | DCODE_LLDP_STR = '[lldp]' 47 | DCODE_INCLUDE_STR = 'i' 48 | DCODE_LEAF_STR = 'L' 49 | 50 | NODE_KNOWN = 0 51 | NODE_NEW = 1 52 | NODE_NEWIP = 2 53 | 54 | class natlas_network: 55 | 56 | def __init__(self, conf): 57 | self.root_node = None 58 | self.nodes = [] 59 | self.max_depth = 0 60 | self.config = conf 61 | self.verbose = 1 62 | 63 | def __str__(self): 64 | return ('' % (self.root_node.name, len(self.nodes))) 65 | def __repr__(self): 66 | return self.__str__() 67 | 68 | def set_max_depth(self, depth): 69 | self.max_depth = depth 70 | 71 | def reset_discovered(self): 72 | for n in self.nodes: 73 | n.discovered = 0 74 | 75 | def set_verbose(self, level): 76 | ''' 77 | Set the verbose output level for discovery output. 78 | 79 | Args: 80 | Level 0 = no output 81 | 1 = normal output 82 | ''' 83 | self.verbose = level 84 | 85 | def discover(self, ip): 86 | ''' 87 | Discover the network starting at the defined root node IP. 88 | Recursively enumerate the network tree up to self.depth. 89 | Populates self.nodes[] as a list of discovered nodes in the 90 | network with self.root_node being the root. 91 | 92 | This function will discover the network with minimal information. 93 | It is enough to define the structure of the network but will not 94 | include much data on each node. Call discover_details() after this 95 | to update the self.nodes[] array with more info. 96 | ''' 97 | 98 | if (self.verbose > 0): 99 | print('Discovery codes:\n' \ 100 | ' . depth %s connection error\n' \ 101 | ' %s discovering node %s numerating adjacencies\n' \ 102 | ' %s include node %s leaf node\n' % 103 | (DCODE_ERR_SNMP_STR, 104 | DCODE_DISCOVERED_STR, DCODE_STEP_INTO_STR, 105 | DCODE_INCLUDE_STR, DCODE_LEAF_STR) 106 | ) 107 | 108 | print('Discovering network...') 109 | 110 | # Start the process of querying this node and recursing adjacencies. 111 | node, new_node = self.__query_node(ip, 'UNKNOWN') 112 | self.root_node = node 113 | 114 | if (node != None): 115 | self.nodes.append(node) 116 | self.__print_step(node.ip[0], node.name, 0, DCODE_ROOT|DCODE_DISCOVERED) 117 | self.__discover_node(node, 0) 118 | else: 119 | return 120 | 121 | # we may have missed chassis info 122 | for n in self.nodes: 123 | if ((n.serial == None) | (n.plat == None) | (n.ios == None)): 124 | n.opts.get_chassis_info = True 125 | if (n.serial == None): 126 | n.opts.get_serial = True 127 | if (n.ios == None): 128 | n.opts.get_ios = True 129 | if (n.plat == None): 130 | n.opts.get_plat = True 131 | n.query_node() 132 | 133 | 134 | def discover_details(self): 135 | ''' 136 | Enumerate the discovered nodes from discover() and update the 137 | nodes in the array with additional info. 138 | ''' 139 | if (self.root_node == None): 140 | return 141 | 142 | if (self.verbose > 0): 143 | print('\nCollecting node details...') 144 | 145 | ni = 0 146 | for n in self.nodes: 147 | ni = ni + 1 148 | 149 | indicator = '+' 150 | if (n.snmpobj.success == 0): 151 | indicator = '!' 152 | 153 | if (self.verbose > 0): 154 | sys.stdout.write('[%i/%i]%s %s (%s)' % (ni, len(self.nodes), indicator, n.name, n.snmpobj._ip)) 155 | sys.stdout.flush() 156 | 157 | # set what details to discover for this node 158 | n.opts.get_router = True 159 | n.opts.get_ospf_id = True 160 | n.opts.get_bgp_las = True 161 | n.opts.get_hsrp_pri = True 162 | n.opts.get_hsrp_vip = True 163 | n.opts.get_serial = True 164 | n.opts.get_stack = True 165 | n.opts.get_stack_details = self.config.diagram.get_stack_members 166 | n.opts.get_vss = True 167 | n.opts.get_vss_details = self.config.diagram.get_vss_members 168 | n.opts.get_svi = True 169 | n.opts.get_lo = True 170 | n.opts.get_vpc = True 171 | n.opts.get_ios = True 172 | n.opts.get_plat = True 173 | 174 | start = timer() 175 | n.query_node() 176 | end = timer() 177 | if (self.verbose > 0): 178 | print(' %.2f sec' % (end - start)) 179 | 180 | # There is some back fill information we can populate now that 181 | # we know all there is to know. 182 | if (self.verbose > 0): 183 | print('\nBack filling node details...') 184 | 185 | for n in self.nodes: 186 | # Find and link VPC nodes together for easy reference later 187 | if ((n.vpc_domain != None) & (n.vpc_peerlink_node == None)): 188 | for link in n.links: 189 | if ((link.local_port == n.vpc_peerlink_if) | (link.local_lag == n.vpc_peerlink_if)): 190 | n.vpc_peerlink_node = link.node 191 | link.node.vpc_peerlink_node = n 192 | break 193 | 194 | 195 | def __print_step(self, ip, name, depth, dcodes): 196 | if (self.verbose == 0): 197 | return 198 | 199 | if (dcodes & DCODE_DISCOVERED): 200 | sys.stdout.write('%-3i' % len(self.nodes)) 201 | else: 202 | sys.stdout.write(' ') 203 | 204 | if (dcodes & DCODE_INCLUDE): 205 | # flip this off cause we didn't even try 206 | dcodes = dcodes & ~DCODE_ERR_SNMP 207 | 208 | if (dcodes & DCODE_ROOT): sys.stdout.write( DCODE_ROOT_STR ) 209 | elif (dcodes & DCODE_CDP): sys.stdout.write( DCODE_CDP_STR ) 210 | elif (dcodes & DCODE_LLDP): sys.stdout.write( DCODE_LLDP_STR ) 211 | else: sys.stdout.write(' ') 212 | 213 | status = '' 214 | if (dcodes & DCODE_ERR_SNMP): status += DCODE_ERR_SNMP_STR 215 | if (dcodes & DCODE_LEAF): status += DCODE_LEAF_STR 216 | elif (dcodes & DCODE_INCLUDE): status += DCODE_INCLUDE_STR 217 | if (dcodes & DCODE_DISCOVERED): status += DCODE_DISCOVERED_STR 218 | elif (dcodes & DCODE_STEP_INTO): status += DCODE_STEP_INTO_STR 219 | sys.stdout.write('%3s' % status) 220 | 221 | for i in range(0, depth): 222 | sys.stdout.write('.') 223 | 224 | name = util.shorten_host_name(name, self.config.host_domains) 225 | if (self.verbose > 0): 226 | print('%s (%s)' % (name, ip)) 227 | 228 | 229 | def __query_node(self, ip, host): 230 | ''' 231 | Query this node. 232 | Return node details and if we already knew about it or if this is a new node. 233 | Don't save the node to the known list, just return info about it. 234 | 235 | Args: 236 | ip: IP Address of the node. 237 | host: Hostname of this known (if known from CDP/LLDP) 238 | 239 | Returns: 240 | natlas_node: Node of this object 241 | int: NODE_NEW = Newly discovered node 242 | NODE_NEWIP = Already knew about this node but not by this IP 243 | NODE_KNOWN = Already knew about this node 244 | ''' 245 | host = util.shorten_host_name(host, self.config.host_domains) 246 | node, node_updated = self.__get_known_node(ip, host) 247 | 248 | if (node == None): 249 | # new node 250 | node = natlas_node() 251 | node.name = host 252 | node.ip = [ip] 253 | state = NODE_NEW 254 | else: 255 | # existing node 256 | if (node.snmpobj.success == 1): 257 | # we already queried this node successfully - return it 258 | return (node, NODE_KNOWN) 259 | # existing node but we couldn't connect before 260 | if (node_updated == 1): 261 | state = NODE_NEWIP 262 | else: 263 | state = NODE_KNOWN 264 | node.name = host 265 | 266 | if (ip == 'UNKNOWN'): 267 | return (node, state) 268 | 269 | # vmware ESX reports the IP as 0.0.0.0 270 | # LLDP can return an empty string for IPs. 271 | if ((ip == '0.0.0.0') | (ip == '')): 272 | return (node, state) 273 | 274 | # find valid credentials for this node 275 | if (node.try_snmp_creds(self.config.snmp_creds) == 0): 276 | return (node, state) 277 | 278 | node.name = node.get_system_name(self.config.host_domains) 279 | if (node.name != host): 280 | # the hostname changed (cdp/lldp vs snmp)! 281 | # double check we don't already know about this node 282 | if (state == NODE_NEW): 283 | node2, node_updated2 = self.__get_known_node(ip, host) 284 | if ((node2 != None) & (node_updated2 == 0)): 285 | return (node, NODE_KNOWN) 286 | if (node_updated2 == 1): 287 | state = NODE_NEWIP 288 | 289 | # Finally, if we still don't have a name, use the IP. 290 | # e.g. Maybe CDP/LLDP was empty and we dont have good credentials 291 | # for this device. A blank name can break Dot. 292 | if ((node.name == None) | (node.name == '')): 293 | node.name = node.get_ipaddr() 294 | 295 | node.opts.get_serial = True # CDP/LLDP does not report, need for extended ACL 296 | node.query_node() 297 | return (node, state) 298 | 299 | 300 | def __get_known_node(self, ip, host): 301 | ''' 302 | Look for known nodes by IP and HOST. 303 | If found by HOST, add the IP if not already known. 304 | 305 | Return: 306 | node: Node, if found. Otherwise None. 307 | updated: 1=updated, 0=not updated 308 | ''' 309 | # already known by IP ? 310 | for ex in self.nodes: 311 | for exip in ex.ip: 312 | if (exip == '0.0.0.0'): 313 | continue 314 | if (exip == ip): 315 | return (ex, 0) 316 | 317 | # already known by HOST ? 318 | node = self.__get_known_node_by_host(host) 319 | if (node != None): 320 | # node already known 321 | if (ip not in node.ip): 322 | node.ip.append(ip) 323 | return (node, 1) 324 | return (node, 0) 325 | 326 | return (None, 0) 327 | 328 | 329 | def __discover_node(self, node, depth): 330 | ''' 331 | Given a node, recursively enumerate its adjacencies 332 | until we reach the specified depth (>0). 333 | 334 | Args: 335 | node: natlas_node object to enumerate. 336 | depth: The depth left that we can go further away from the root. 337 | ''' 338 | if (node == None): 339 | return 340 | 341 | if (depth >= self.max_depth): 342 | return 343 | 344 | if (node.discovered > 0): 345 | return 346 | node.discovered = 1 347 | 348 | # vmware ESX can report IP as 0.0.0.0 349 | # If we are allowing 0.0.0.0/32 in the config, 350 | # then we added it as a leaf, but don't discover it 351 | if (node.ip[0] == '0.0.0.0'): 352 | return 353 | 354 | # may be a leaf we couldn't connect to previously 355 | if (node.snmpobj.success == 0): 356 | return 357 | 358 | # print some info to stdout 359 | dcodes = DCODE_STEP_INTO 360 | if (depth == 0): 361 | dcodes |= DCODE_ROOT 362 | self.__print_step(node.ip[0], node.name, depth, dcodes) 363 | 364 | # get the cached snmp credentials 365 | snmpobj = node.snmpobj 366 | 367 | # list of valid neighbors to discover next 368 | valid_neighbors = [] 369 | 370 | # get list of neighbors 371 | cdp_neighbors = node.get_cdp_neighbors() 372 | lldp_neighbors = node.get_lldp_neighbors() 373 | neighbors = cdp_neighbors + lldp_neighbors 374 | if (len(neighbors) == 0): 375 | return 376 | 377 | for n in neighbors: 378 | # some neighbors may not advertise IP addresses - default them to 0.0.0.0 379 | if (n.remote_ip == None): 380 | n.remote_ip = '0.0.0.0' 381 | 382 | # check the ACL 383 | acl_action = self.__match_node_acl(n.remote_ip, n.remote_name) 384 | if (acl_action == 'deny'): 385 | # deny inclusion of this node 386 | continue 387 | 388 | dcodes = DCODE_DISCOVERED 389 | child = None 390 | if (acl_action == 'include'): 391 | # include this node but do not discover it 392 | child = natlas_node() 393 | child.ip = [n.remote_ip] 394 | dcodes |= DCODE_INCLUDE 395 | else: 396 | # discover this node 397 | child, query_result = self.__query_node(n.remote_ip, n.remote_name) 398 | 399 | # if we couldn't pull info from SNMP fill in what we know 400 | if (child.snmpobj.success == 0): 401 | child.name = util.shorten_host_name(n.remote_name, self.config.host_domains) 402 | dcodes |= DCODE_ERR_SNMP 403 | 404 | # need to check the ACL again for extended ops (we have more info) 405 | acl_action = self.__match_node_acl(n.remote_ip, n.remote_name, n.remote_plat, n.remote_ios, child.serial) 406 | if (acl_action == 'deny'): 407 | continue 408 | 409 | if (query_result == NODE_NEW): 410 | self.nodes.append(child) 411 | if (acl_action == 'leaf'): dcodes |= DCODE_LEAF 412 | if (n.discovered_proto == 'cdp'): dcodes |= DCODE_CDP 413 | if (n.discovered_proto == 'lldp'): dcodes |= DCODE_LLDP 414 | self.__print_step(n.remote_ip, n.remote_name, depth+1, dcodes) 415 | 416 | # CDP/LLDP advertises the platform 417 | child.plat = n.remote_plat 418 | child.ios = n.remote_ios 419 | 420 | # add the discovered node to the link object and link to the parent 421 | n.node = child 422 | self.__add_link(node, n) 423 | 424 | # if we need to discover this node then add it to the list 425 | if ((query_result == NODE_NEW) & (acl_action != 'leaf') & (acl_action != 'include')): 426 | valid_neighbors.append(child) 427 | 428 | # discover the valid neighbors 429 | for n in valid_neighbors: 430 | self.__discover_node(n, depth+1) 431 | 432 | 433 | def __match_node_acl(self, ip, host, platform=None, software=None, serial=None): 434 | for acl in self.config.discover_acl: 435 | if (acl.type == 'ip'): 436 | if (self.__match_ip(ip, acl.str)): 437 | return acl.action 438 | elif (acl.type == 'host'): 439 | if (self.__match_strpattern(host, acl.str)): 440 | return acl.action 441 | elif (acl.type == 'platform'): 442 | if ((platform != None) and self.__match_strpattern(platform, acl.str)): 443 | return acl.action 444 | elif (acl.type == 'software'): 445 | if ((software != None) and self.__match_strpattern(software, acl.str)): 446 | return acl.action 447 | elif (acl.type == 'serial'): 448 | if ((serial != None) and self.__match_strpattern(serial, acl.str)): 449 | return acl.action 450 | return 'deny' 451 | 452 | 453 | def __match_ip(self, ip, cidr): 454 | if (cidr == 'any'): 455 | return 1 456 | 457 | validate = re.match('^([0-2]?[0-9]?[0-9]\.){3}[0-2]?[0-9]?[0-9]$', ip) 458 | if (validate == None): 459 | return 0 460 | 461 | if (USE_NETADDR): 462 | if (ip in IPNetwork(cidr)): 463 | return 1 464 | else: 465 | if (util.is_ipv4_in_cidr(ip, cidr)): 466 | return 1 467 | return 0 468 | 469 | 470 | def __match_strpattern(self, str, pattern): 471 | if (str == '*'): 472 | return 1 473 | if (re.search(pattern, str)): 474 | return 1 475 | return 0 476 | 477 | # 478 | # Add or update a link. 479 | # Return 480 | # 0 - Found an existing link and updated it 481 | # 1 - Added as a new link 482 | # 483 | def __add_link(self, node, link): 484 | if (link.node.discovered == 1): 485 | # both nodes have been discovered, 486 | # so try to update existing reverse link info 487 | # instead of adding a new link 488 | for n in self.nodes: 489 | # find the child, which was the original parent 490 | if (n.name == link.node.name): 491 | # find the existing link 492 | for ex_link in n.links: 493 | if ((ex_link.node.name == node.name) & (ex_link.local_port == link.remote_port)): 494 | if ((link.local_if_ip != 'UNKNOWN') & (ex_link.remote_if_ip == None)): 495 | ex_link.remote_if_ip = link.local_if_ip 496 | 497 | if ((link.local_lag != 'UNKNOWN') & (ex_link.remote_lag == None)): 498 | ex_link.remote_lag = link.local_lag 499 | 500 | if ((len(link.local_lag_ips) == 0) & len(ex_link.remote_lag_ips)): 501 | ex_link.remote_lag_ips = link.local_lag_ips 502 | 503 | if ((link.local_native_vlan != None) & (ex_link.remote_native_vlan == None)): 504 | ex_link.remote_native_vlan = link.local_native_vlan 505 | 506 | if ((link.local_allowed_vlans != None) & (ex_link.remote_allowed_vlans == None)): 507 | ex_link.remote_allowed_vlans = link.local_allowed_vlans 508 | 509 | return 0 510 | else: 511 | for ex_link in node.links: 512 | if ((ex_link.node.name == link.node.name) & (ex_link.local_port == link.local_port)): 513 | # haven't discovered yet but somehow we have this link twice. 514 | # maybe from different discovery processes? 515 | return 0 516 | 517 | node.add_link(link) 518 | return 1 519 | 520 | 521 | def __get_known_node_by_host(self, hostname): 522 | ''' 523 | Determine if the node is already known by hostname. 524 | If it is, return it. 525 | ''' 526 | for n in self.nodes: 527 | if (n.name == hostname): 528 | return n 529 | return None 530 | 531 | -------------------------------------------------------------------------------- /natlas/node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | node.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | 29 | from .snmp import * 30 | from .util import * 31 | from .node_stack import natlas_node_stack, natlas_node_stack_member 32 | from .node_vss import natlas_node_vss, natlas_node_vss_member 33 | from .mac import natlas_mac 34 | 35 | class natlas_node_link: 36 | ''' 37 | Generic link to another node. 38 | CDP and LLDP neighbors are discovered 39 | and returned as natlas_node_link objects. 40 | ''' 41 | 42 | def __init__(self): 43 | # the linked node 44 | self.node = None 45 | 46 | # details about the link 47 | self.link_type = None 48 | self.remote_ip = None 49 | self.remote_name = None 50 | self.vlan = None 51 | self.local_native_vlan = None 52 | self.local_allowed_vlans = None 53 | self.remote_native_vlan = None 54 | self.remote_allowed_vlans = None 55 | self.local_port = None 56 | self.remote_port = None 57 | self.local_lag = None 58 | self.remote_lag = None 59 | self.local_lag_ips = None 60 | self.remote_lag_ips = None 61 | self.local_if_ip = None 62 | self.remote_if_ip = None 63 | self.remote_platform = None 64 | self.remote_ios = None 65 | self.remote_mac = None 66 | self.discovered_proto = None 67 | 68 | def __str__(self): 69 | return ( 70 | 'link_type = %s\n' \ 71 | 'remote_ip = %s\n' \ 72 | 'remote_name = %s\n' \ 73 | 'vlan = %s\n' \ 74 | 'local_native_vlan = %s\n' \ 75 | 'local_allowed_vlans = %s\n' \ 76 | 'remote_native_vlan = %s\n' \ 77 | 'remote_allowed_vlans = %s\n' \ 78 | 'local_port = %s\n' \ 79 | 'remote_port = %s\n' \ 80 | 'local_lag = %s\n' \ 81 | 'remote_lag = %s\n' \ 82 | 'local_lag_ips = %s\n' \ 83 | 'remote_lag_ips = %s\n' \ 84 | 'local_if_ip = %s\n' \ 85 | 'remote_if_ip = %s\n' \ 86 | 'remote_platform = %s\n' \ 87 | 'remote_ios = %s\n' \ 88 | 'remote_mac = %s\n' \ 89 | 'discovered_proto = %s\n' \ 90 | % (self.link_type, self.remote_ip, self.remote_name, self.vlan, self.local_native_vlan, 91 | self.local_allowed_vlans, self.remote_native_vlan, self.remote_allowed_vlans, 92 | self.local_port, self.remote_port, self.local_lag, self.remote_lag, self.local_lag_ips, 93 | self.remote_lag_ips, self.local_if_ip, self.remote_if_ip, self.remote_platform, self.remote_ios, 94 | self.remote_mac, self.discovered_proto)) 95 | def __repr__(self): 96 | return ('' % (self.local_port, self.remote_name, self.remote_port)) 97 | 98 | 99 | class natlas_node_svi: 100 | def __init__(self, vlan): 101 | self.vlan = vlan 102 | self.ip = [] 103 | def __str__(self): 104 | return ('VLAN = %s\nIP = %s' % (self.vlan, self.ip)) 105 | def __repr__(self): 106 | return ('' % (self.vlan, self.ip)) 107 | 108 | 109 | class natlas_node_lo: 110 | def __init__(self, name, ips): 111 | self.name = name.replace('Loopback', 'lo') 112 | self.ips = ips 113 | def __str__(self): 114 | return ('Name = %s\nIPs = %s' % (self.name, self.ips)) 115 | def __repr__(self): 116 | return ('' % (self.name, self.ips)) 117 | 118 | 119 | class natlas_vlan: 120 | def __init__(self, vid, name): 121 | self.id = vid 122 | self.name = name 123 | def __str__(self): 124 | return ('' % (self.id, self.name)) 125 | def __repr__(self): 126 | return self.__str__() 127 | 128 | class natlas_arp: 129 | def __init__(self, ip, mac, interf, arp_type): 130 | self.ip = ip 131 | self.mac = mac 132 | self.interf = interf 133 | self.arp_type = arp_type 134 | def __str__(self): 135 | return ('' % (self.ip, self.mac, self.interf, self.arp_type)) 136 | def __repr__(self): 137 | return self.__str__() 138 | 139 | class natlas_node: 140 | class _node_opts: 141 | def __init__(self): 142 | self.reset() 143 | 144 | def reset(self, setting=False): 145 | self.get_name = setting 146 | self.get_ip = setting 147 | self.get_plat = setting 148 | self.get_ios = setting 149 | self.get_router = setting 150 | self.get_ospf_id = setting 151 | self.get_bgp_las = setting 152 | self.get_hsrp_pri = setting 153 | self.get_hsrp_vip = setting 154 | self.get_serial = setting 155 | self.get_stack = setting 156 | self.get_stack_details = setting 157 | self.get_vss = setting 158 | self.get_vss_details = setting 159 | self.get_svi = setting 160 | self.get_lo = setting 161 | self.get_bootf = setting 162 | self.get_chassis_info = setting 163 | self.get_vpc = setting 164 | 165 | def __init__(self, ip=None): 166 | self.opts = natlas_node._node_opts() 167 | self.snmpobj = natlas_snmp() 168 | self.links = [] 169 | self.discovered = 0 170 | self.name = None 171 | self.ip = [ip] 172 | self.plat = None 173 | self.ios = None 174 | self.router = None 175 | self.ospf_id = None 176 | self.bgp_las = None 177 | self.hsrp_pri = None 178 | self.hsrp_vip = None 179 | self.serial = None 180 | self.bootfile = None 181 | self.svis = [] 182 | self.loopbacks = [] 183 | self.vpc_peerlink_if = None 184 | self.vpc_peerlink_node = None 185 | self.vpc_domain = None 186 | self.stack = natlas_node_stack() 187 | self.vss = natlas_node_vss() 188 | 189 | self.cdp_vbtbl = None 190 | self.ldp_vbtbl = None 191 | self.link_type_vbtbl = None 192 | self.lag_vbtbl = None 193 | self.vlan_vbtbl = None 194 | self.ifname_vbtbl = None 195 | self.ifip_vbtbl = None 196 | self.svi_vbtbl = None 197 | self.ethif_vbtbl = None 198 | self.trk_allowed_vbtbl = None 199 | self.trk_native_vbtbl = None 200 | self.vpc_vbtbl = None 201 | self.vlan_vbtbl = None 202 | self.vlandesc_vbtbl = None 203 | self.arp_vbtbl = None 204 | 205 | def __str__(self): 206 | return ( 207 | 'Name = %s\n' \ 208 | 'IP = %s\n' \ 209 | 'Platform = %s\n' \ 210 | 'IOS = %s\n' \ 211 | 'Router = %s\n' \ 212 | 'OSPF_ID = %s\n' \ 213 | 'BGP_LAS = %s\n' \ 214 | 'HSRP_PRI = %s\n' \ 215 | 'HSRP_VIP = %s\n' \ 216 | 'Serials = %s\n' \ 217 | 'Bootfile = %s\n' \ 218 | 'SVIs = %s\n' \ 219 | 'Loopbacks = %s\n' \ 220 | 'VPC_Peerlink_If = %s\n' \ 221 | 'VPC_Peerlink_Node = %s\n' \ 222 | 'VPC_Domain = %s\n' \ 223 | 'Stack = %s\n' \ 224 | 'VSS = %s\n' 225 | 'Links = %s\n' 226 | % (self.name, self.ip, self.plat, self.ios, self.router, 227 | self.ospf_id, self.bgp_las, self.hsrp_pri, self.hsrp_vip, 228 | self.serial, self.bootfile, self.svis, self.loopbacks, 229 | self.vpc_peerlink_if, self.vpc_peerlink_node, self.vpc_domain, 230 | self.stack, self.vss, self.links) 231 | ) 232 | def __repr__(self): 233 | return ('' % 234 | (self.name, self.ip, self.plat, self.ios, self.serial, self.router, self.vss, self.stack)) 235 | 236 | 237 | def add_link(self, link): 238 | self.links.append(link) 239 | 240 | 241 | # find valid credentials for this node. 242 | # try each known IP until one works 243 | def try_snmp_creds(self, snmp_creds): 244 | if (self.snmpobj.success == 0): 245 | for ipaddr in self.ip: 246 | if ((ipaddr == '0.0.0.0') | (ipaddr == 'UNKNOWN') | (ipaddr == '')): 247 | continue 248 | self.snmpobj._ip = ipaddr 249 | if (self.snmpobj.get_cred(snmp_creds) == 1): 250 | return 1 251 | return 0 252 | 253 | 254 | # Query this node. 255 | # Set .opts and .snmp_creds before calling. 256 | def query_node(self): 257 | if (self.snmpobj.ver == 0): 258 | # call try_snmp_creds() first or it failed to find good creds 259 | return 0 260 | 261 | snmpobj = self.snmpobj 262 | 263 | if (self.opts.get_name == True): 264 | self.name = self.get_system_name([]) 265 | 266 | # router 267 | if (self.opts.get_router == True): 268 | if (self.router == None): 269 | self.router = 1 if (snmpobj.get_val(OID_IP_ROUTING) == '1') else 0 270 | 271 | if (self.router == 1): 272 | # OSPF 273 | if (self.opts.get_ospf_id == True): 274 | self.ospf_id = snmpobj.get_val(OID_OSPF) 275 | if (self.ospf_id != None): 276 | self.ospf_id = snmpobj.get_val(OID_OSPF_ID) 277 | 278 | # BGP 279 | if (self.opts.get_bgp_las == True): 280 | self.bgp_las = snmpobj.get_val(OID_BGP_LAS) 281 | if (self.bgp_las == '0'): # 4500x is reporting 0 with disabled 282 | self.bgp_las = None 283 | 284 | # HSRP 285 | if (self.opts.get_hsrp_pri == True): 286 | self.hsrp_pri = snmpobj.get_val(OID_HSRP_PRI) 287 | if (self.hsrp_pri != None): 288 | self.hsrp_vip = snmpobj.get_val(OID_HSRP_VIP) 289 | 290 | # stack 291 | if (self.opts.get_stack): 292 | self.stack = natlas_node_stack(snmpobj, self.opts) 293 | 294 | # vss 295 | if (self.opts.get_vss): 296 | self.vss = natlas_node_vss(snmpobj, self.opts) 297 | 298 | # serial 299 | if ((self.opts.get_serial == 1) & (self.stack.count == 0) & (self.vss.enabled == 0)): 300 | self.serial = snmpobj.get_val(OID_SYS_SERIAL) 301 | 302 | # SVI 303 | if (self.opts.get_svi == True): 304 | if (self.svi_vbtbl == None): 305 | self.svi_vbtbl = snmpobj.get_bulk(OID_SVI_VLANIF) 306 | 307 | if (self.ifip_vbtbl == None): 308 | self.ifip_vbtbl = snmpobj.get_bulk(OID_IF_IP) 309 | 310 | for row in self.svi_vbtbl: 311 | for n, v in row: 312 | n = str(n) 313 | vlan = n.split('.')[14] 314 | svi = natlas_node_svi(vlan) 315 | svi_ips = self.__get_cidrs_from_ifidx(v) 316 | svi.ip.extend(svi_ips) 317 | self.svis.append(svi) 318 | 319 | # loopback 320 | if (self.opts.get_lo == True): 321 | self.ethif_vbtbl = snmpobj.get_bulk(OID_ETH_IF) 322 | 323 | if (self.ifip_vbtbl == None): 324 | self.ifip_vbtbl = snmpobj.get_bulk(OID_IF_IP) 325 | 326 | for row in self.ethif_vbtbl: 327 | for n, v in row: 328 | n = str(n) 329 | if (n.startswith(OID_ETH_IF_TYPE) & (v == 24)): 330 | ifidx = n.split('.')[10] 331 | lo_name = snmpobj.cache_lookup(self.ethif_vbtbl, OID_ETH_IF_DESC + '.' + ifidx) 332 | lo_ips = self.__get_cidrs_from_ifidx(ifidx) 333 | lo = natlas_node_lo(lo_name, lo_ips) 334 | self.loopbacks.append(lo) 335 | 336 | # bootfile 337 | if (self.opts.get_bootf): 338 | self.bootfile = snmpobj.get_val(OID_SYS_BOOT) 339 | 340 | # chassis info (serial, IOS, platform) 341 | if (self.opts.get_chassis_info): 342 | self.__get_chassis_info() 343 | 344 | # VPC peerlink 345 | if (self.opts.get_vpc): 346 | self.vpc_domain, self.vpc_peerlink_if = self.__get_vpc_info(self.ethif_vbtbl) 347 | 348 | # reset the get options 349 | self.opts.reset() 350 | return 1 351 | 352 | 353 | def __get_cidrs_from_ifidx(self, ifidx): 354 | ips = [] 355 | 356 | for ifrow in self.ifip_vbtbl: 357 | for ifn, ifv in ifrow: 358 | ifn = str(ifn) 359 | if (ifn.startswith(OID_IF_IP_ADDR)): 360 | if (str(ifv) == str(ifidx)): 361 | t = ifn.split('.') 362 | ip = ".".join(t[10:]) 363 | mask = self.snmpobj.cache_lookup(self.ifip_vbtbl, OID_IF_IP_NETM + ip) 364 | nbits = util.get_net_bits_from_mask(mask) 365 | cidr = '%s/%i' % (ip, nbits) 366 | ips.append(cidr) 367 | return ips 368 | 369 | 370 | def __cache_common_mibs(self): 371 | if (self.link_type_vbtbl == None): 372 | self.link_type_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_VTP) 373 | 374 | if (self.lag_vbtbl == None): 375 | self.lag_vbtbl = self.snmpobj.get_bulk(OID_LAG_LACP) 376 | 377 | if (self.vlan_vbtbl == None): 378 | self.vlan_vbtbl = self.snmpobj.get_bulk(OID_IF_VLAN) 379 | 380 | if (self.ifname_vbtbl == None): 381 | self.ifname_vbtbl = self.snmpobj.get_bulk(OID_IFNAME) 382 | 383 | if (self.trk_allowed_vbtbl == None): 384 | self.trk_allowed_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_ALLOW) 385 | 386 | if (self.trk_native_vbtbl == None): 387 | self.trk_native_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_NATIVE) 388 | 389 | if (self.ifip_vbtbl == None): 390 | self.ifip_vbtbl = self.snmpobj.get_bulk(OID_IF_IP) 391 | 392 | 393 | # 394 | # Get a list of CDP neighbors. 395 | # Returns a list of natlas_node_link's. 396 | # Will always return an array. 397 | # 398 | def get_cdp_neighbors(self): 399 | neighbors = [] 400 | snmpobj = self.snmpobj 401 | 402 | # get list of CDP neighbors 403 | self.cdp_vbtbl = snmpobj.get_bulk(OID_CDP) 404 | if (self.cdp_vbtbl == None): 405 | print('No CDP Neighbors Found.') 406 | return [] 407 | 408 | # cache some common MIB trees 409 | self.__cache_common_mibs() 410 | 411 | for row in self.cdp_vbtbl: 412 | for name, val in row: 413 | name = str(name) 414 | # process only if this row is a CDP_DEVID 415 | if (name.startswith(OID_CDP_DEVID) == 0): 416 | continue 417 | 418 | t = name.split('.') 419 | ifidx = t[14] 420 | ifidx2 = t[15] 421 | 422 | # get remote IP 423 | rip = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_IPADDR + '.' + ifidx + '.' + ifidx2) 424 | rip = util.convert_ip_int_str(rip) 425 | 426 | # get local port 427 | lport = self.__get_ifname(ifidx) 428 | 429 | # get remote port 430 | rport = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_DEVPORT + '.' + ifidx + '.' + ifidx2) 431 | rport = self.shorten_port_name(rport) 432 | 433 | # get remote platform 434 | rplat = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_DEVPLAT + '.' + ifidx + '.' + ifidx2) 435 | 436 | # get IOS version 437 | rios = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_IOS + '.' + ifidx + '.' + ifidx2) 438 | if (rios != None): 439 | try: 440 | rios = binascii.unhexlify(rios[2:]) 441 | except: 442 | pass 443 | rios = self.__format_ios_ver(rios) 444 | 445 | link = self.__get_node_link_info(ifidx, ifidx2) 446 | link.remote_name = val.prettyPrint() 447 | link.remote_ip = rip 448 | link.discovered_proto = 'cdp' 449 | link.local_port = lport 450 | link.remote_port = rport 451 | link.remote_plat = rplat 452 | link.remote_ios = rios 453 | 454 | neighbors.append(link) 455 | 456 | return neighbors 457 | 458 | 459 | # 460 | # Get a list of LLDP neighbors. 461 | # Returns a list of natlas_node_link's 462 | # Will always return an array. 463 | # 464 | def get_lldp_neighbors(self): 465 | neighbors = [] 466 | snmpobj = self.snmpobj 467 | 468 | self.lldp_vbtbl = snmpobj.get_bulk(OID_LLDP) 469 | if (self.lldp_vbtbl == None): 470 | print('No LLDP Neighbors Found.') 471 | return [] 472 | 473 | self.__cache_common_mibs() 474 | 475 | for row in self.lldp_vbtbl: 476 | for name, val in row: 477 | name = str(name) 478 | if (name.startswith(OID_LLDP_TYPE) == 0): 479 | continue 480 | 481 | t = name.split('.') 482 | ifidx = t[12] 483 | ifidx2 = t[13] 484 | 485 | rip = '' 486 | for r in self.lldp_vbtbl: 487 | for n, v in r: 488 | n = str(n) 489 | if (n.startswith(OID_LLDP_DEVADDR + '.' + ifidx + '.' + ifidx2)): 490 | t2 = n.split('.') 491 | rip = '.'.join(t2[16:]) 492 | 493 | 494 | lport = self.__get_ifname(ifidx) 495 | 496 | rport = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVPORT + '.' + ifidx + '.' + ifidx2) 497 | rport = self.shorten_port_name(rport) 498 | 499 | devid = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVID + '.' + ifidx + '.' + ifidx2) 500 | try: 501 | mac_seg = [devid[x:x+4] for x in xrange(2, len(devid), 4)] 502 | devid = '.'.join(mac_seg) 503 | except: 504 | pass 505 | 506 | rimg = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVDESC + '.' + ifidx + '.' + ifidx2) 507 | if (rimg != None): 508 | try: 509 | rimg = binascii.unhexlify(rimg[2:]) 510 | except: 511 | pass 512 | rimg = self.__format_ios_ver(rimg) 513 | 514 | name = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVNAME + '.' + ifidx + '.' + ifidx2) 515 | if ((name == None) | (name == '')): 516 | name = devid 517 | 518 | link = self.__get_node_link_info(ifidx, ifidx2) 519 | link.remote_ip = rip 520 | link.remote_name = name 521 | link.discovered_proto = 'lldp' 522 | link.local_port = lport 523 | link.remote_port = rport 524 | link.remote_plat = None 525 | link.remote_ios = rimg 526 | link.remote_mac = devid 527 | 528 | neighbors.append(link) 529 | 530 | return neighbors 531 | 532 | 533 | def __get_node_link_info(self, ifidx, ifidx2): 534 | snmpobj = self.snmpobj 535 | 536 | # get link type (trunk ?) 537 | link_type = snmpobj.cache_lookup(self.link_type_vbtbl, OID_TRUNK_VTP + '.' + ifidx) 538 | 539 | native_vlan = None 540 | allowed_vlans = 'All' 541 | if (link_type == '1'): 542 | native_vlan = snmpobj.cache_lookup(self.trk_native_vbtbl, OID_TRUNK_NATIVE + '.' + ifidx) 543 | 544 | allowed_vlans = snmpobj.cache_lookup(self.trk_allowed_vbtbl, OID_TRUNK_ALLOW + '.' + ifidx) 545 | allowed_vlans = self.__parse_allowed_vlans(allowed_vlans) 546 | 547 | # get LAG membership 548 | lag = snmpobj.cache_lookup(self.lag_vbtbl, OID_LAG_LACP + '.' + ifidx) 549 | lag_ifname = self.__get_ifname(lag) 550 | lag_ips = self.__get_cidrs_from_ifidx(lag) 551 | 552 | # get VLAN info 553 | vlan = snmpobj.cache_lookup(self.vlan_vbtbl, OID_IF_VLAN + '.' + ifidx) 554 | 555 | # get IP address 556 | lifips = self.__get_cidrs_from_ifidx(ifidx) 557 | 558 | link = natlas_node_link() 559 | link.link_type = link_type 560 | link.vlan = vlan 561 | link.local_native_vlan = native_vlan 562 | link.local_allowed_vlans = allowed_vlans 563 | link.local_lag = lag_ifname 564 | link.local_lag_ips = lag_ips 565 | link.remote_lag_ips = [] 566 | link.local_if_ip = lifips[0] if len(lifips) else None 567 | 568 | return link 569 | 570 | 571 | def __parse_allowed_vlans(self, allowed_vlans): 572 | if (allowed_vlans.startswith('0x') == False): 573 | return 'All' 574 | 575 | ret = '' 576 | group = 0 577 | op = 0 578 | 579 | for i in range(2, len(allowed_vlans)): 580 | v = int(allowed_vlans[i], 16) 581 | for b in range(0, 4): 582 | a = v & (0x1 << (3 - b)) 583 | vlan = ((i-2)*4)+b 584 | 585 | if (a): 586 | if (op == 1): 587 | group += 1 588 | else: 589 | if (len(ret)): 590 | if (group > 1): 591 | ret += '-' 592 | ret += str(vlan - 1) if vlan else '1' 593 | else: 594 | ret += ',%i' % vlan 595 | else: 596 | ret += str(vlan) 597 | group = 0 598 | op = 1 599 | else: 600 | if (op == 1): 601 | if (len(ret)): 602 | if (group > 1): 603 | ret += '-%i' % (vlan - 1) 604 | op = 0 605 | group = 0 606 | 607 | if (op): 608 | if (ret == '1'): 609 | return 'All' 610 | if (group): 611 | ret += '-1001' 612 | else: 613 | ret += ',1001' 614 | 615 | return ret if len(ret) else 'All' 616 | 617 | 618 | def __get_chassis_info(self): 619 | # Get: 620 | # Serial number 621 | # Platform 622 | # IOS 623 | # Slow but reliable method by using SNMP directly. 624 | # Usually we will get this via CDP. 625 | snmpobj = self.snmpobj 626 | 627 | if ((self.stack.count > 0) | (self.vss.enabled == 1)): 628 | # Use opts.get_stack_details 629 | # or opts.get_vss_details 630 | # for this. 631 | return 632 | 633 | class_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_CLASS) 634 | 635 | if (self.opts.get_serial): serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) 636 | if (self.opts.get_plat): platf_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) 637 | if (self.opts.get_ios): ios_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SOFTWARE) 638 | 639 | if (class_vbtbl == None): 640 | return 641 | 642 | for row in class_vbtbl: 643 | for n, v in row: 644 | n = str(n) 645 | if (v != ENTPHYCLASS_CHASSIS): 646 | continue 647 | 648 | t = n.split('.') 649 | idx = t[12] 650 | 651 | if (self.opts.get_serial): self.serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + idx) 652 | if (self.opts.get_plat): self.plat = snmpobj.cache_lookup(platf_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + idx) 653 | if (self.opts.get_ios): self.ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + idx) 654 | 655 | if (self.opts.get_ios): 656 | # modular switches might have IOS on a module rather than chassis 657 | if (self.ios == ''): 658 | for row in class_vbtbl: 659 | for n, v in row: 660 | n = str(n) 661 | if (v != ENTPHYCLASS_MODULE): 662 | continue 663 | t = n.split('.') 664 | idx = t[12] 665 | self.ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + idx) 666 | if (self.ios != ''): 667 | break 668 | if (self.ios != ''): 669 | break 670 | self.ios = self.__format_ios_ver(self.ios) 671 | 672 | return 673 | 674 | # 675 | # Lookup and format an interface name from a cache table of indexes. 676 | # 677 | def __get_ifname(self, ifidx): 678 | if ((ifidx == None) | (ifidx == OID_ERR)): 679 | return 'UNKNOWN' 680 | if (self.ifname_vbtbl == None): 681 | self.ifname_vbtbl = self.snmpobj.get_bulk(OID_IFNAME) 682 | 683 | str = self.snmpobj.cache_lookup(self.ifname_vbtbl, OID_IFNAME + '.' + ifidx) 684 | str = self.shorten_port_name(str) 685 | 686 | return str or 'UNKNOWN' 687 | 688 | 689 | def get_system_name(self, domains): 690 | return util.shorten_host_name(self.snmpobj.get_val(OID_SYSNAME), domains) 691 | 692 | 693 | # 694 | # Normalize a reporeted software vesion string. 695 | # 696 | def __format_ios_ver(self, img): 697 | x = img 698 | if (type(img) == bytes): 699 | x = img.decode("utf-8") 700 | 701 | try: 702 | img_s = re.search('(Version:? |CCM:)([^ ,$]*)', x) 703 | except: 704 | return img 705 | 706 | if (img_s): 707 | if (img_s.group(1) == 'CCM:'): 708 | return 'CCM %s' % img_s.group(2) 709 | return img_s.group(2) 710 | 711 | return img 712 | 713 | def get_ipaddr(self): 714 | ''' 715 | Return the best IP address for this device. 716 | Returns the first matching IP: 717 | - Lowest Loopback interface 718 | - Lowest SVI address/known IP 719 | ''' 720 | # Loopbacks - first interface 721 | if (len(self.loopbacks)): 722 | ips = self.loopbacks[0].ips 723 | if (len(ips)): 724 | ips.sort() 725 | return util.strip_slash_masklen(ips[0]) 726 | 727 | # SVIs + all known - lowest address 728 | ips = [] 729 | for svi in self.svis: 730 | ips.extend(svi.ip) 731 | ips.extend(self.ip) 732 | if (len(ips)): 733 | ips.sort() 734 | return util.strip_slash_masklen(ips[0]) 735 | 736 | return '' 737 | 738 | 739 | def __get_vpc_info(self, ifarr): 740 | ''' 741 | If VPC is enabled, 742 | Return the VPC domain and interface name of the VPC peerlink. 743 | ''' 744 | if (self.vpc_vbtbl == None): 745 | self.vpc_vbtbl = self.snmpobj.get_bulk(OID_VPC_PEERLINK_IF) 746 | if ((self.vpc_vbtbl == None) | (len(self.vpc_vbtbl) == 0)): 747 | return (None, None) 748 | domain = natlas_snmp.get_last_oid_token(self.vpc_vbtbl[0][0][0]) 749 | ifidx = str(self.vpc_vbtbl[0][0][1]) 750 | ifname = self.snmpobj.cache_lookup(ifarr, OID_ETH_IF_DESC + '.' + ifidx) 751 | ifname = self.shorten_port_name(ifname) 752 | return (domain, ifname) 753 | 754 | def get_vlans(self): 755 | # use cache if possible 756 | if (self.vlan_vbtbl == None): 757 | self.vlan_vbtbl = self.snmpobj.get_bulk(OID_VLANS) 758 | if (self.vlandesc_vbtbl == None): 759 | self.vlandesc_vbtbl = self.snmpobj.get_bulk(OID_VLAN_DESC) 760 | arr = [] 761 | i = 0 762 | for vlan_row in self.vlan_vbtbl: 763 | for vlan_n, vlan_v in vlan_row: 764 | # get VLAN ID from OID 765 | vlan = natlas_snmp.get_last_oid_token(vlan_n) 766 | if (vlan >= 1002): 767 | continue 768 | arr.append(natlas_vlan(vlan, str(self.vlandesc_vbtbl[i][0][1]))) 769 | i = i + 1 770 | return arr 771 | 772 | def get_arp_table(self): 773 | # use cache if possible 774 | if (self.arp_vbtbl == None): 775 | self.arp_vbtbl = self.snmpobj.get_bulk(OID_ARP) 776 | arr = [] 777 | for r in self.arp_vbtbl: 778 | for n, v in r: 779 | n = str(n) 780 | if (n.startswith(OID_ARP_VLAN)): 781 | tok = n.split('.') 782 | ip = '.'.join(tok[11:]) 783 | interf = self.__get_ifname(str(v)) 784 | mach = self.snmpobj.cache_lookup(self.arp_vbtbl, OID_ARP_MAC+'.'+str(v)+'.'+ip) 785 | mac = natlas_mac.mac_hex_to_ascii(mach, 1) 786 | atype = self.snmpobj.cache_lookup(self.arp_vbtbl, OID_ARP_TYPE+'.'+str(v)+'.'+ip) 787 | 788 | atype = int(atype) 789 | type_str = 'unknown' 790 | if (atype == ARP_TYPE_OTHER): type_str = 'other' 791 | elif (atype == ARP_TYPE_INVALID): type_str = 'invalid' 792 | elif (atype == ARP_TYPE_DYNAMIC): type_str = 'dynamic' 793 | elif (atype == ARP_TYPE_STATIC): type_str = 'static' 794 | 795 | arr.append(natlas_arp(ip, mac, interf, type_str)) 796 | return arr if arr else [] 797 | 798 | 799 | def shorten_port_name(self, port): 800 | if (port == OID_ERR): 801 | return 'UNKNOWN' 802 | 803 | if (port != None): 804 | port = port.replace('TenGigabitEthernet', 'te') 805 | port = port.replace('GigabitEthernet', 'gi') 806 | port = port.replace('FastEthernet', 'fa') 807 | port = port.replace('port-channel', 'po') 808 | port = port.replace('Te', 'te') 809 | port = port.replace('Gi', 'gi') 810 | port = port.replace('Fa', 'fa') 811 | port = port.replace('Po', 'po') 812 | 813 | return port 814 | 815 | -------------------------------------------------------------------------------- /natlas/node_stack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | node_stack.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | from .snmp import * 28 | from .util import * 29 | import sys 30 | 31 | 32 | class natlas_node_stack_member: 33 | 34 | def __init__(self): 35 | self.opts = None 36 | self.num = 0 37 | self.role = 0 38 | self.pri = 0 39 | self.mac = None 40 | self.img = None 41 | self.serial = None 42 | self.plat = None 43 | 44 | def __str__(self): 45 | return ('' % (self.num, self.role, self.serial)) 46 | def __repr__(self): 47 | return self.__str__() 48 | 49 | 50 | class natlas_node_stack: 51 | 52 | def __init__(self, snmpobj = None, opts = None): 53 | self.members = [] 54 | self.count = 0 55 | self.enabled = 0 56 | self.opts = opts 57 | 58 | if (snmpobj != None): 59 | self.get_members(snmpobj) 60 | 61 | 62 | def __str__(self): 63 | return ('' % (self.enabled, self.count, self.members)) 64 | def __repr__(self): 65 | return self.__str__() 66 | 67 | 68 | def get_members(self, snmpobj): 69 | if (self.opts == None): 70 | return 71 | 72 | vbtbl = snmpobj.get_bulk(OID_STACK) 73 | if (vbtbl == None): 74 | return None 75 | 76 | if (self.opts.get_stack_details == 0): 77 | self.count = 0 78 | for row in vbtbl: 79 | for n, v in row: 80 | n = str(n) 81 | if (n.startswith(OID_STACK_NUM + '.')): 82 | self.count += 1 83 | 84 | if (self.count == 1): 85 | self.count = 0 86 | return 87 | 88 | if (self.opts.get_serial): serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) 89 | if (self.opts.get_plat): platf_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) 90 | 91 | for row in vbtbl: 92 | for n, v in row: 93 | n = str(n) 94 | if (n.startswith(OID_STACK_NUM + '.')): 95 | # Get info on this stack member and add to the list 96 | m = natlas_node_stack_member() 97 | t = n.split('.') 98 | idx = t[14] 99 | 100 | m.num = v 101 | m.role = snmpobj.cache_lookup(vbtbl, OID_STACK_ROLE + '.' + idx) 102 | m.pri = snmpobj.cache_lookup(vbtbl, OID_STACK_PRI + '.' + idx) 103 | m.mac = snmpobj.cache_lookup(vbtbl, OID_STACK_MAC + '.' + idx) 104 | m.img = snmpobj.cache_lookup(vbtbl, OID_STACK_IMG + '.' + idx) 105 | 106 | if (self.opts.get_serial): m.serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + idx) 107 | if (self.opts.get_plat): m.plat = snmpobj.cache_lookup(platf_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + idx) 108 | 109 | if (m.role == '1'): 110 | m.role = 'master' 111 | elif (m.role == '2'): 112 | m.role = 'member' 113 | elif (m.role == '3'): 114 | m.role = 'notMember' 115 | elif (m.role == '4'): 116 | m.role = 'standby' 117 | 118 | mac_seg = [m.mac[x:x+4] for x in range(2, len(m.mac), 4)] 119 | m.mac = '.'.join(mac_seg) 120 | self.members.append(m) 121 | 122 | self.count = len(self.members) 123 | if (self.count == 1): 124 | self.count = 0 125 | if (self.count > 0): 126 | self.enabled = 1 127 | 128 | -------------------------------------------------------------------------------- /natlas/node_vss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | node_vss.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import sys 28 | from .snmp import * 29 | from .util import * 30 | 31 | 32 | class natlas_node_vss_member: 33 | def __init__(self): 34 | self.opts = None 35 | self.ios = None 36 | self.serial = None 37 | self.plat = None 38 | 39 | def __str__(self): 40 | return ('' % (self.serial, self.plat)) 41 | def __repr__(self): 42 | return self.__str__() 43 | 44 | 45 | class natlas_node_vss: 46 | def __init__(self, snmpobj = None, opts = None): 47 | self.members = [ natlas_node_vss_member(), natlas_node_vss_member() ] 48 | self.enabled = 0 49 | self.domain = None 50 | self.opts = opts 51 | 52 | if (snmpobj != None): 53 | self.get_members(snmpobj) 54 | 55 | def __str__(self): 56 | return ('' % (self.enabled, self.domain, self.members)) 57 | def __repr__(self): 58 | return self.__str__() 59 | 60 | def get_members(self, snmpobj): 61 | # check if VSS is enabled 62 | self.enabled = 1 if (snmpobj.get_val(OID_VSS_MODE) == '2') else 0 63 | if (self.enabled == 0): 64 | return 65 | 66 | if (self.opts == None): 67 | return 68 | 69 | self.domain = snmpobj.get_val(OID_VSS_DOMAIN) 70 | 71 | if (self.opts.get_vss_details == 0): 72 | return 73 | 74 | # pull some VSS-related info 75 | module_vbtbl = snmpobj.get_bulk(OID_VSS_MODULES) 76 | 77 | if (self.opts.get_ios): ios_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SOFTWARE) 78 | if (self.opts.get_serial): serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) 79 | if (self.opts.get_plat): plat_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) 80 | 81 | chassis = 0 82 | 83 | # enumerate VSS modules and find chassis info 84 | for row in module_vbtbl: 85 | for n,v in row: 86 | if (v == 1): 87 | modidx = str(n).split('.')[14] 88 | # we want only chassis - line card module have no software 89 | ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + modidx) 90 | 91 | if (ios != ''): 92 | if (self.opts.get_ios): self.members[chassis].ios = ios 93 | if (self.opts.get_plat): self.members[chassis].plat = snmpobj.cache_lookup(plat_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + modidx) 94 | if (self.opts.get_serial): self.members[chassis].serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + modidx) 95 | chassis += 1 96 | 97 | if (chassis > 1): 98 | return 99 | 100 | -------------------------------------------------------------------------------- /natlas/output.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | output.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | from .config import natlas_config 28 | from ._version import __version__ 29 | 30 | class natlas_output: 31 | 32 | def __init__(self): 33 | self.type = 'base' 34 | 35 | def generate(self): 36 | raise Exception('natlas_output.generate() called direct') 37 | 38 | -------------------------------------------------------------------------------- /natlas/output_catalog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | output_catalog.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | from .config import natlas_config 28 | from .network import natlas_network 29 | from .output import natlas_output 30 | from ._version import __version__ 31 | 32 | 33 | class natlas_output_catalog: 34 | 35 | def __init__(self, network): 36 | natlas_output.__init__(self) 37 | self.network = network 38 | self.config = network.config 39 | 40 | def generate(self, filename): 41 | try: 42 | f = open(filename, 'w') 43 | except: 44 | print('Unable to open catalog file "%s"' % filename) 45 | return 46 | 47 | for n in self.network.nodes: 48 | # get info that we may not have yet 49 | n.opts.get_serial = True 50 | n.opts.get_plat = True 51 | n.opts.get_bootf = True 52 | n.query_node() 53 | 54 | if (n.stack.count > 0): 55 | # stackwise 56 | for smem in n.stack.members: 57 | serial = smem.serial or 'NOT CONFIGURED TO POLL' 58 | plat = smem.plat or 'NOT CONFIGURED TO POLL' 59 | f.write('"%s","%s","%s","%s","%s","STACK","%s"\n' % (n.name, n.ip[0], plat, n.ios, serial, n.bootfile)) 60 | elif (n.vss.enabled != 0): 61 | #vss 62 | for i in range(0, 2): 63 | serial = n.vss.members[i].serial 64 | plat = n.vss.members[i].plat 65 | ios = n.vss.members[i].ios 66 | f.write('"%s","%s","%s","%s","%s","VSS","%s"\n' % (n.name, n.ip[0], plat, ios, serial, n.bootfile)) 67 | else: 68 | # stand alone 69 | f.write('"%s","%s","%s","%s","%s","","%s"\n' % (n.name, n.ip[0], n.plat, n.ios, n.serial, n.bootfile)) 70 | 71 | f.close() 72 | 73 | -------------------------------------------------------------------------------- /natlas/output_diagram.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | output_diagram.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | import pydot 28 | import datetime 29 | import os 30 | 31 | from .config import natlas_config 32 | from .network import natlas_network 33 | from .output import natlas_output 34 | from .util import * 35 | from ._version import __version__ 36 | 37 | 38 | class natlas_diagram_dot_node: 39 | def __init__(self): 40 | self.ntype = 'single' 41 | self.shape = 'ellipse' 42 | self.style = 'solid' 43 | self.peripheries = 1 44 | self.label = '' 45 | self.vss_label = '' 46 | 47 | 48 | class natlas_output_diagram: 49 | 50 | def __init__(self, network): 51 | natlas_output.__init__(self) 52 | self.network = network 53 | self.config = network.config 54 | 55 | def generate(self, dot_file, title): 56 | self.network.reset_discovered() 57 | 58 | title_text_size = self.config.diagram.title_text_size 59 | credits = '' \ 60 | '' \ 61 | '' \ 68 | '' \ 69 | '
' \ 62 | '$title$
' \ 63 | '$date$
' \ 64 | '' \ 65 | 'Generated by natlas $ver$
' \ 66 | 'Michael Laforest

' \ 67 | '
' % (title_text_size, title_text_size-2) 70 | 71 | today = datetime.datetime.now() 72 | today = today.strftime('%Y-%m-%d %H:%M') 73 | credits = credits.replace('$ver$', __version__) 74 | credits = credits.replace('$date$', today) 75 | credits = credits.replace('$title$', title) 76 | 77 | node_text_size = self.config.diagram.node_text_size 78 | link_text_size = self.config.diagram.link_text_size 79 | 80 | diagram = pydot.Dot( 81 | graph_type = 'graph', 82 | labelloc = 'b', 83 | labeljust = 'r', 84 | fontsize = node_text_size, 85 | label = '<%s>' % credits 86 | ) 87 | diagram.set_node_defaults( 88 | fontsize = link_text_size 89 | ) 90 | diagram.set_edge_defaults( 91 | fontsize = link_text_size, 92 | labeljust = 'l' 93 | ) 94 | 95 | # add all of the nodes and links 96 | self.__generate(diagram, self.network.root_node) 97 | 98 | 99 | # expand output string 100 | files = util.expand_path_pattern(dot_file) 101 | for f in files: 102 | # get file extension 103 | file_name, file_ext = os.path.splitext(f) 104 | output_func = getattr(diagram, 'write_' + file_ext.lstrip('.'), None) 105 | if (output_func == None): 106 | print('Error: Output type "%s" does not exist.' % file_ext) 107 | else: 108 | output_func(f) 109 | print('Created diagram: %s' % f) 110 | 111 | 112 | def __generate(self, diagram, node): 113 | if (node == None): 114 | return (0, 0) 115 | if (node.discovered > 0): 116 | return (0, 0) 117 | node.discovered = 1 118 | 119 | dot_node = self.__get_node(diagram, node) 120 | 121 | if (dot_node.ntype == 'single'): 122 | diagram.add_node( 123 | pydot.Node( 124 | name = node.name, 125 | label = '<%s>' % dot_node.label, 126 | style = dot_node.style, 127 | shape = dot_node.shape, 128 | peripheries = dot_node.peripheries 129 | ) 130 | ) 131 | elif (dot_node.ntype == 'vss'): 132 | cluster = pydot.Cluster( 133 | graph_name = node.name, 134 | suppress_disconnected = False, 135 | labelloc = 't', 136 | labeljust = 'c', 137 | fontsize = self.config.diagram.node_text_size, 138 | label = '<
VSS %s>' % node.vss.domain 139 | ) 140 | for i in range(0, 2): 141 | # {vss.} vars 142 | nlabel = dot_node.label.format(vss=node.vss.members[i]) 143 | cluster.add_node( 144 | pydot.Node( 145 | name = '%s[natlasVSS%i]' % (node.name, i+1), 146 | label = '<%s>' % nlabel, 147 | style = dot_node.style, 148 | shape = dot_node.shape, 149 | peripheries = dot_node.peripheries 150 | ) 151 | ) 152 | diagram.add_subgraph(cluster) 153 | elif (dot_node.ntype == 'vpc'): 154 | cluster = pydot.Cluster( 155 | graph_name = node.name, 156 | suppress_disconnected = False, 157 | labelloc = 't', 158 | labeljust = 'c', 159 | fontsize = self.config.diagram.node_text_size, 160 | label = '<
VPC %s>' % node.vpc_domain 161 | ) 162 | cluster.add_node( 163 | pydot.Node( 164 | name = node.name, 165 | label = '<%s>' % dot_node.label, 166 | style = dot_node.style, 167 | shape = dot_node.shape, 168 | peripheries = dot_node.peripheries 169 | ) 170 | ) 171 | if (node.vpc_peerlink_node != None): 172 | node2 = node.vpc_peerlink_node 173 | node2.discovered = 1 174 | dot_node2 = self.__get_node(diagram, node2) 175 | cluster.add_node( 176 | pydot.Node( 177 | name = node2.name, 178 | label = '<%s>' % dot_node2.label, 179 | style = dot_node2.style, 180 | shape = dot_node2.shape, 181 | peripheries = dot_node2.peripheries 182 | ) 183 | ) 184 | diagram.add_subgraph(cluster) 185 | elif (dot_node.ntype == 'stackwise'): 186 | cluster = pydot.Cluster( 187 | graph_name = node.name, 188 | suppress_disconnected = False, 189 | labelloc = 't', 190 | labeljust = 'c', 191 | fontsize = self.config.diagram.node_text_size, 192 | label = '<
Stackwise>' 193 | ) 194 | for i in range(0, node.stack.count): 195 | # {stack.} vars 196 | if (len(node.stack.members) == 0): 197 | nlabel = dot_node.label 198 | else: 199 | nlabel = dot_node.label.format(stack=node.stack.members[i]) 200 | cluster.add_node( 201 | pydot.Node( 202 | name = '%s[natlasSW%i]' % (node.name, i+1), 203 | label = '<%s>' % nlabel, 204 | style = dot_node.style, 205 | shape = dot_node.shape, 206 | peripheries = dot_node.peripheries 207 | ) 208 | ) 209 | diagram.add_subgraph(cluster) 210 | 211 | lags = [] 212 | for link in node.links: 213 | self.__generate(diagram, link.node) 214 | 215 | # determine if this link should be broken out or not 216 | expand_lag = 0 217 | if (self.config.diagram.expand_lag == 1): 218 | expand_lag = 1 219 | elif (link.local_lag == 'UNKNOWN'): 220 | expand_lag = 1 221 | elif (self.__does_lag_span_devs(link.local_lag, node.links) > 1): 222 | # a LAG could span different devices, eg Nexus. 223 | # in this case we should always break it out, otherwise we could 224 | # get an unlinked node in the diagram. 225 | expand_lag = 1 226 | 227 | if (expand_lag == 1): 228 | self.__create_link(diagram, node, link, 0) 229 | else: 230 | found = 0 231 | for lag in lags: 232 | if (link.local_lag == lag): 233 | found = 1 234 | break 235 | if (found == 0): 236 | lags.append(link.local_lag) 237 | self.__create_link(diagram, node, link, 1) 238 | 239 | 240 | def __get_node(self, diagram, node): 241 | dot_node = natlas_diagram_dot_node() 242 | dot_node.ntype = 'single' 243 | dot_node.shape = 'ellipse' 244 | dot_node.style = 'solid' 245 | dot_node.peripheries = 1 246 | dot_node.label = '' 247 | 248 | # get the node text 249 | dot_node.label = self.__get_node_text(diagram, node, self.config.diagram.node_text) 250 | 251 | # set the node properties 252 | if (node.vss.enabled == 1): 253 | if (self.config.diagram.expand_vss == 1): 254 | dot_node.ntype = 'vss' 255 | else: 256 | # group VSS into one diagram node 257 | dot_node.peripheries = 2 258 | 259 | if (node.stack.count > 0): 260 | if (self.config.diagram.expand_stackwise == 1): 261 | dot_node.ntype = 'stackwise' 262 | else: 263 | # group Stackwise into one diagram node 264 | dot_node.peripheries = node.stack.count 265 | 266 | if (node.vpc_domain != None): 267 | if (self.config.diagram.group_vpc == 1): 268 | dot_node.ntype = 'vpc' 269 | 270 | if (node.router == 1): 271 | dot_node.shape = 'diamond' 272 | 273 | return dot_node 274 | 275 | 276 | def __create_link(self, diagram, node, link, draw_as_lag): 277 | link_color = 'black' 278 | link_style = 'solid' 279 | link_label = '' 280 | 281 | if ((link.local_port == node.vpc_peerlink_if) | (link.local_lag == node.vpc_peerlink_if)): 282 | link_label += 'VPC ' 283 | 284 | if (draw_as_lag): 285 | link_label += 'LAG' 286 | members = 0 287 | for l in node.links: 288 | if (l.local_lag == link.local_lag): 289 | members += 1 290 | link_label += '\n%i Members' % members 291 | else: 292 | link_label += 'P:%s\nC:%s' % (link.local_port, link.remote_port) 293 | 294 | is_lag = 1 if (link.local_lag != 'UNKNOWN') else 0 295 | 296 | if (draw_as_lag == 0): 297 | # LAG as member 298 | if (is_lag): 299 | local_lag_ip = '' 300 | remote_lag_ip = '' 301 | if (len(link.local_lag_ips)): 302 | local_lag_ip = ' - %s' % link.local_lag_ips[0] 303 | if (len(link.remote_lag_ips)): 304 | remote_lag_ip = ' - %s' % link.remote_lag_ips[0] 305 | 306 | link_label += '\nLAG Member' 307 | 308 | if ((local_lag_ip == '') & (remote_lag_ip == '')): 309 | link_label += '\nP:%s | C:%s' % (link.local_lag, link.remote_lag) 310 | else: 311 | link_label += '\nP:%s%s' % (link.local_lag, local_lag_ip) 312 | link_label += '\nC:%s%s' % (link.remote_lag, remote_lag_ip) 313 | 314 | # IP Addresses 315 | if ((link.local_if_ip != 'UNKNOWN') & (link.local_if_ip != None)): 316 | link_label += '\nP:%s' % link.local_if_ip 317 | if ((link.remote_if_ip != 'UNKNOWN') & (link.remote_if_ip != None)): 318 | link_label += '\nC:%s' % link.remote_if_ip 319 | else: 320 | # LAG as grouping 321 | for l in node.links: 322 | if (l.local_lag == link.local_lag): 323 | link_label += '\nP:%s | C:%s' % (l.local_port, l.remote_port) 324 | 325 | local_lag_ip = '' 326 | remote_lag_ip = '' 327 | 328 | if (len(link.local_lag_ips)): 329 | local_lag_ip = ' - %s' % link.local_lag_ips[0] 330 | if (len(link.remote_lag_ips)): 331 | remote_lag_ip = ' - %s' % link.remote_lag_ips[0] 332 | 333 | if ((local_lag_ip == '') & (remote_lag_ip == '')): 334 | link_label += '\nP:%s | C:%s' % (link.local_lag, link.remote_lag) 335 | else: 336 | link_label += '\nP:%s%s' % (link.local_lag, local_lag_ip) 337 | link_label += '\nC:%s%s' % (link.remote_lag, remote_lag_ip) 338 | 339 | 340 | if (link.link_type == '1'): 341 | # Trunk = Bold/Blue 342 | link_color = 'blue' 343 | link_style = 'bold' 344 | 345 | if ((link.local_native_vlan == link.remote_native_vlan) | (link.remote_native_vlan == None)): 346 | link_label += '\nNative %s' % link.local_native_vlan 347 | else: 348 | link_label += '\nNative P:%s C:%s' % (link.local_native_vlan, link.remote_native_vlan) 349 | 350 | if (link.local_allowed_vlans == link.remote_allowed_vlans): 351 | link_label += '\nAllowed %s' % link.local_allowed_vlans 352 | else: 353 | link_label += '\nAllowed P:%s' % link.local_allowed_vlans 354 | if (link.remote_allowed_vlans != None): 355 | link_label += '\nAllowed C:%s' % link.remote_allowed_vlans 356 | elif (link.link_type is None): 357 | # Routed = Bold/Red 358 | link_color = 'red' 359 | link_style = 'bold' 360 | else: 361 | # Switched access, include VLAN ID in label 362 | if (link.vlan != None): 363 | link_label += '\nVLAN %s' % link.vlan 364 | 365 | edge_src = node.name 366 | edge_dst = link.node.name 367 | lmod = util.get_module_from_interf(link.local_port) 368 | rmod = util.get_module_from_interf(link.remote_port) 369 | 370 | if (self.config.diagram.expand_vss == 1): 371 | if (node.vss.enabled == 1): 372 | edge_src = '%s[natlasVSS%s]' % (node.name, lmod) 373 | if (link.node.vss.enabled == 1): 374 | edge_dst = '%s[natlasVSS%s]' % (link.node.name, rmod) 375 | 376 | if (self.config.diagram.expand_stackwise == 1): 377 | if (node.stack.count > 0): 378 | edge_src = '%s[natlasSW%s]' % (node.name, lmod) 379 | if (link.node.stack.count > 0): 380 | edge_dst = '%s[natlasSW%s]' % (link.node.name, rmod) 381 | 382 | edge = pydot.Edge( 383 | edge_src, edge_dst, 384 | dir = 'forward', 385 | label = link_label, 386 | color = link_color, 387 | style = link_style 388 | ) 389 | 390 | diagram.add_edge(edge) 391 | 392 | 393 | def __does_lag_span_devs(self, lag_name, links): 394 | if (lag_name == None): 395 | return 0 396 | 397 | devs = [] 398 | for link in links: 399 | if (link.local_lag == lag_name): 400 | if (link.node.name not in devs): 401 | devs.append(link.node.name) 402 | 403 | return len(devs) 404 | 405 | 406 | def __eval_if_block(self, if_cond, node): 407 | # evaluate condition 408 | if_cond_eval = if_cond.format(node=node, config=self.config).strip() 409 | try: 410 | if eval(if_cond_eval): 411 | return 1 412 | except: 413 | if ((if_cond_eval != '0') & (if_cond_eval != 'None') & (if_cond_eval != '')): 414 | return 1 415 | else: 416 | return 0 417 | 418 | return 0 419 | 420 | 421 | def __get_node_text(self, diagram, node, fmt): 422 | ''' 423 | Generate the node text given the format string 'fmt' 424 | ''' 425 | fmt_proc = fmt 426 | 427 | # IF blocks 428 | while (1): 429 | if_block = re.search('<%if ([^%]*): ([^%]*)%>', fmt_proc) 430 | if (if_block == None): 431 | break 432 | 433 | # evaluate condition 434 | if_cond = if_block[1] 435 | if_val = if_block[2] 436 | if (self.__eval_if_block(if_cond, node) == 0): 437 | if_val = '' 438 | fmt_proc = fmt_proc[:if_block.span()[0]] + if_val + fmt_proc[if_block.span()[1]:] 439 | 440 | # {node.ip} = best IP 441 | ip = node.get_ipaddr() 442 | fmt_proc = fmt_proc.replace('{node.ip}', ip) 443 | 444 | # stackwise 445 | stack_block = re.search('<%stack ([^%]*)%>', fmt_proc) 446 | if (stack_block != None): 447 | if (node.stack.count == 0): 448 | # no stackwise, remove this 449 | fmt_proc = fmt_proc[:stack_block.span()[0]] + fmt_proc[stack_block.span()[1]:] 450 | else: 451 | val = '' 452 | if (self.config.diagram.expand_stackwise == 0): 453 | if (self.config.diagram.get_stack_members): 454 | for smem in node.stack.members: 455 | nval = stack_block[1] 456 | nval = nval.replace('{stack.num}', str(smem.num)) 457 | nval = nval.replace('{stack.plat}', smem.plat) 458 | nval = nval.replace('{stack.serial}', smem.serial) 459 | nval = nval.replace('{stack.role}', smem.role) 460 | val += nval 461 | fmt_proc = fmt_proc[:stack_block.span()[0]] + val + fmt_proc[stack_block.span()[1]:] 462 | 463 | # loopbacks 464 | loopback_block = re.search('<%loopback ([^%]*)%>', fmt_proc) 465 | if (loopback_block != None): 466 | val = '' 467 | for lo in node.loopbacks: 468 | for lo_ip in lo.ips: 469 | nval = loopback_block[1] 470 | nval = nval.replace('{lo.name}', lo.name) 471 | nval = nval.replace('{lo.ip}', lo_ip) 472 | val += nval 473 | fmt_proc = fmt_proc[:loopback_block.span()[0]] + val + fmt_proc[loopback_block.span()[1]:] 474 | 475 | # SVIs 476 | svi_block = re.search('<%svi ([^%]*)%>', fmt_proc) 477 | if (svi_block != None): 478 | val = '' 479 | for svi in node.svis: 480 | for svi_ip in svi.ip: 481 | nval = svi_block[1] 482 | nval = nval.replace('{svi.vlan}', svi.vlan) 483 | nval = nval.replace('{svi.ip}', svi_ip) 484 | val += nval 485 | fmt_proc = fmt_proc[:svi_block.span()[0]] + val + fmt_proc[svi_block.span()[1]:] 486 | 487 | # replace {stack.} with magic 488 | fmt_proc = re.sub('{stack\.(([a-zA-Z])*)}', '$stack2354$\g<1>$stack2354$', fmt_proc) 489 | fmt_proc = re.sub('{vss\.(([a-zA-Z])*)}', '$vss2354$\g<1>$vss2354$', fmt_proc) 490 | 491 | # {node.} variables 492 | fmt_proc = fmt_proc.format(node=node) 493 | 494 | # replace magics 495 | fmt_proc = re.sub('\$stack2354\$(([a-zA-Z])*)\$stack2354\$', '{stack.\g<1>}', fmt_proc) 496 | fmt_proc = re.sub('\$vss2354\$(([a-zA-Z])*)\$vss2354\$', '{vss.\g<1>}', fmt_proc) 497 | 498 | return fmt_proc 499 | 500 | -------------------------------------------------------------------------------- /natlas/snmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | snmp.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | from pysnmp.entity.rfc3413.oneliner import cmdgen 28 | 29 | SNMP_PORT = 161 30 | 31 | OID_SYSNAME = '1.3.6.1.2.1.1.5.0' 32 | 33 | OID_SYS_SERIAL = '1.3.6.1.4.1.9.3.6.3.0' 34 | OID_SYS_BOOT = '1.3.6.1.4.1.9.2.1.73.0' 35 | 36 | OID_IFNAME = '1.3.6.1.2.1.31.1.1.1.1' # + ifidx (BULK) 37 | 38 | OID_CDP = '1.3.6.1.4.1.9.9.23.1.2.1.1' # (BULK) 39 | OID_CDP_IPADDR = '1.3.6.1.4.1.9.9.23.1.2.1.1.4' 40 | OID_CDP_IOS = '1.3.6.1.4.1.9.9.23.1.2.1.1.5' 41 | OID_CDP_DEVID = '1.3.6.1.4.1.9.9.23.1.2.1.1.6' # + .ifidx.53 42 | OID_CDP_DEVPORT = '1.3.6.1.4.1.9.9.23.1.2.1.1.7' 43 | OID_CDP_DEVPLAT = '1.3.6.1.4.1.9.9.23.1.2.1.1.8' 44 | OID_CDP_INT = '1.3.6.1.4.1.9.9.23.1.1.1.1.' # 6.ifidx 45 | 46 | OID_LLDP = '1.0.8802.1.1.2.1.4' 47 | OID_LLDP_TYPE = '1.0.8802.1.1.2.1.4.1.1.4.0' 48 | OID_LLDP_DEVID = '1.0.8802.1.1.2.1.4.1.1.5.0' 49 | OID_LLDP_DEVPORT = '1.0.8802.1.1.2.1.4.1.1.7.0' 50 | OID_LLDP_DEVNAME = '1.0.8802.1.1.2.1.4.1.1.9.0' 51 | OID_LLDP_DEVDESC = '1.0.8802.1.1.2.1.4.1.1.10.0' 52 | OID_LLDP_DEVADDR = '1.0.8802.1.1.2.1.4.2.1.5.0' 53 | 54 | OID_TRUNK_ALLOW = '1.3.6.1.4.1.9.9.46.1.6.1.1.4' # + ifidx (Allowed VLANs) 55 | OID_TRUNK_NATIVE = '1.3.6.1.4.1.9.9.46.1.6.1.1.5' # + ifidx (Native VLAN) 56 | OID_TRUNK_VTP = '1.3.6.1.4.1.9.9.46.1.6.1.1.14' # + ifidx (VTP Status) 57 | OID_LAG_LACP = '1.2.840.10006.300.43.1.2.1.1.12' # + ifidx (BULK) 58 | 59 | OID_IP_ROUTING = '1.3.6.1.2.1.4.1.0' 60 | OID_IF_VLAN = '1.3.6.1.4.1.9.9.68.1.2.2.1.2' # + ifidx (BULK) 61 | 62 | OID_IF_IP = '1.3.6.1.2.1.4.20.1' # (BULK) 63 | OID_IF_IP_ADDR = '1.3.6.1.2.1.4.20.1.2' # + a.b.c.d = ifid 64 | OID_IF_IP_NETM = '1.3.6.1.2.1.4.20.1.3.' # + a.b.c.d 65 | 66 | OID_SVI_VLANIF = '1.3.6.1.4.1.9.9.128.1.1.1.1.3' # cviRoutedVlanIfIndex 67 | 68 | OID_ETH_IF = '1.3.6.1.2.1.2.2.1' # ifEntry 69 | OID_ETH_IF_TYPE = '1.3.6.1.2.1.2.2.1.3' # ifEntry.ifType 24=loopback 70 | OID_ETH_IF_DESC = '1.3.6.1.2.1.2.2.1.2' # ifEntry.ifDescr 71 | 72 | OID_OSPF = '1.3.6.1.2.1.14.1.2.0' 73 | OID_OSPF_ID = '1.3.6.1.2.1.14.1.1.0' 74 | 75 | OID_BGP_LAS = '1.3.6.1.2.1.15.2.0' 76 | 77 | OID_HSRP_PRI = '1.3.6.1.4.1.9.9.106.1.2.1.1.3.1.10' 78 | OID_HSRP_VIP = '1.3.6.1.4.1.9.9.106.1.2.1.1.11.1.10' 79 | 80 | OID_STACK = '1.3.6.1.4.1.9.9.500' 81 | OID_STACK_NUM = '1.3.6.1.4.1.9.9.500.1.2.1.1.1' 82 | OID_STACK_ROLE = '1.3.6.1.4.1.9.9.500.1.2.1.1.3' 83 | OID_STACK_PRI = '1.3.6.1.4.1.9.9.500.1.2.1.1.4' 84 | OID_STACK_MAC = '1.3.6.1.4.1.9.9.500.1.2.1.1.7' 85 | OID_STACK_IMG = '1.3.6.1.4.1.9.9.500.1.2.1.1.8' 86 | 87 | OID_VSS_MODULES = '1.3.6.1.4.1.9.9.388.1.4.1.1.1' # .modidx = 1 88 | OID_VSS_MODE = '1.3.6.1.4.1.9.9.388.1.1.4.0' 89 | OID_VSS_DOMAIN = '1.3.6.1.4.1.9.9.388.1.1.1.0' 90 | 91 | OID_ENTPHYENTRY_CLASS = '1.3.6.1.2.1.47.1.1.1.1.5' # + .modifx (3=chassis) (9=module) 92 | OID_ENTPHYENTRY_SOFTWARE = '1.3.6.1.2.1.47.1.1.1.1.9' # + .modidx 93 | OID_ENTPHYENTRY_SERIAL = '1.3.6.1.2.1.47.1.1.1.1.11' # + .modidx 94 | OID_ENTPHYENTRY_PLAT = '1.3.6.1.2.1.47.1.1.1.1.13' # + .modidx 95 | 96 | OID_VPC_PEERLINK_IF = '1.3.6.1.4.1.9.9.807.1.4.1.1.2' 97 | 98 | OID_VLANS = '1.3.6.1.4.1.9.9.46.1.3.1.1.2' 99 | OID_VLAN_DESC = '1.3.6.1.4.1.9.9.46.1.3.1.1.4' 100 | OID_VLAN_CAM = '1.3.6.1.2.1.17.4.3.1.1' 101 | 102 | OID_BRIDGE_PORTNUMS = '1.3.6.1.2.1.17.4.3.1.2' 103 | OID_IFINDEX = '1.3.6.1.2.1.17.1.4.1.2' 104 | 105 | OID_ARP = '1.3.6.1.2.1.4.22.1' 106 | OID_ARP_VLAN = '1.3.6.1.2.1.4.22.1.1' 107 | OID_ARP_MAC = '1.3.6.1.2.1.4.22.1.2' 108 | OID_ARP_IP = '1.3.6.1.2.1.4.22.1.3' 109 | OID_ARP_TYPE = '1.3.6.1.2.1.4.22.1.4' 110 | 111 | OID_ERR = 'No Such Object currently exists at this OID' 112 | OID_ERR_INST = 'No Such Instance currently exists at this OID' 113 | 114 | # OID_ENTPHYENTRY_CLASS values 115 | ENTPHYCLASS_OTHER = 1 116 | ENTPHYCLASS_UNKNOWN = 2 117 | ENTPHYCLASS_CHASSIS = 3 118 | ENTPHYCLASS_BACKPLANE = 4 119 | ENTPHYCLASS_CONTAINER = 5 120 | ENTPHYCLASS_POWERSUPPLY = 6 121 | ENTPHYCLASS_FAN = 7 122 | ENTPHYCLASS_SENSOR = 8 123 | ENTPHYCLASS_MODULE = 9 124 | ENTPHYCLASS_PORT = 10 125 | ENTPHYCLASS_STACK = 11 126 | ENTPHYCLASS_PDU = 12 127 | 128 | # ARP TYPES 129 | ARP_TYPE_OTHER = 1 130 | ARP_TYPE_INVALID = 2 131 | ARP_TYPE_DYNAMIC = 3 132 | ARP_TYPE_STATIC = 4 133 | 134 | class natlas_snmp: 135 | def __init__(self, ip='0.0.0.0'): 136 | self.success = 0 137 | self.ver = 0 138 | self.v2_community = None 139 | self._ip = ip 140 | 141 | # 142 | # Try to find valid SNMP credentials in the provided list. 143 | # Returns 1 if success, 0 if failed. 144 | # 145 | def get_cred(self, snmp_creds): 146 | for cred in snmp_creds: 147 | # we don't currently support anything other than SNMPv2 148 | if (cred['ver'] != 2): 149 | continue 150 | 151 | community = cred['community'] 152 | 153 | cmdGen = cmdgen.CommandGenerator() 154 | errIndication, errStatus, errIndex, varBinds = cmdGen.getCmd( 155 | cmdgen.CommunityData(community), 156 | cmdgen.UdpTransportTarget((self._ip, SNMP_PORT)), 157 | '1.3.6.1.2.1.1.5.0', 158 | lookupNames = False, lookupValues = False 159 | ) 160 | if errIndication: 161 | continue 162 | else: 163 | self.ver = 2 164 | self.success = 1 165 | self.v2_community = community 166 | 167 | return 1 168 | 169 | return 0 170 | 171 | # 172 | # Get single SNMP value at OID. 173 | # 174 | def get_val(self, oid): 175 | cmdGen = cmdgen.CommandGenerator() 176 | errIndication, errStatus, errIndex, varBinds = cmdGen.getCmd( 177 | cmdgen.CommunityData(self.v2_community), 178 | cmdgen.UdpTransportTarget((self._ip, SNMP_PORT), retries=2), 179 | oid, lookupNames = False, lookupValues = False 180 | ) 181 | 182 | if errIndication: 183 | print('[E] get_snmp_val(%s): %s' % (self.v2_community, errIndication)) 184 | else: 185 | r = varBinds[0][1].prettyPrint() 186 | if ((r == OID_ERR) | (r == OID_ERR_INST)): 187 | return None 188 | return r 189 | 190 | return None 191 | 192 | 193 | # 194 | # Get bulk SNMP value at OID. 195 | # 196 | # Returns 1 on success, 0 on failure. 197 | # 198 | def get_bulk(self, oid): 199 | cmdGen = cmdgen.CommandGenerator() 200 | errIndication, errStatus, errIndex, varBindTable = cmdGen.bulkCmd( 201 | cmdgen.CommunityData(self.v2_community), 202 | cmdgen.UdpTransportTarget((self._ip, SNMP_PORT), timeout=30, retries=2), 203 | 0, 50, 204 | oid, 205 | lookupNames = False, lookupValues = False 206 | ) 207 | 208 | if errIndication: 209 | print('[E] get_snmp_bulk(%s): %s' % (self.v2_community, errIndication)) 210 | else: 211 | ret = [] 212 | for r in varBindTable: 213 | for n, v in r: 214 | n = str(n) 215 | if (n.startswith(oid) == 0): 216 | return ret 217 | ret.append(r) 218 | return ret 219 | 220 | return None 221 | 222 | 223 | # 224 | # Lookup a value from the return table of get_bulk() 225 | # 226 | def cache_lookup(self, varBindTable, name): 227 | if (varBindTable == None): 228 | return None 229 | 230 | for r in varBindTable: 231 | for n, v in r: 232 | n = str(n) 233 | if (n == name): 234 | return v.prettyPrint() 235 | return None 236 | 237 | 238 | # 239 | # Given an OID 1.2.3.4...x.y.z return z 240 | # 241 | def get_last_oid_token(oid): 242 | _oid = oid.getOid() 243 | ts = len(_oid) 244 | return _oid[ts-1] 245 | 246 | -------------------------------------------------------------------------------- /natlas/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | natlas 5 | util.py 6 | 7 | Michael Laforest 8 | mjlaforest@gmail.com 9 | 10 | Copyright (C) 2015-2018 Michael Laforest 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms of the GNU General Public License 14 | as published by the Free Software Foundation; either version 2 15 | of the License, or (at your option) any later version. 16 | 17 | This program is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU General Public License for more details. 21 | 22 | You should have received a copy of the GNU General Public License 23 | along with this program; if not, write to the Free Software 24 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 25 | ''' 26 | 27 | # Set the below line =0 if you do not want to use PyNetAddr 28 | USE_NETADDR = 1 29 | 30 | import re 31 | import struct 32 | import binascii 33 | 34 | from .snmp import * 35 | from .config import natlas_config 36 | 37 | if (USE_NETADDR == 1): 38 | from netaddr import IPAddress, IPNetwork 39 | 40 | class util: 41 | 42 | def get_net_bits_from_mask(netm): 43 | cidr = 0 44 | mt = netm.split('.') 45 | for b in range(0, 4): 46 | v = int(mt[b]) 47 | while (v > 0): 48 | if (v & 0x01): 49 | cidr += 1 50 | v = v >> 1 51 | 52 | return cidr 53 | 54 | 55 | # 56 | # Return 1 if IP is in the CIDR range. 57 | # 58 | def is_ipv4_in_cidr(ip, cidr): 59 | t = cidr.split('/') 60 | cidr_ip = t[0] 61 | cidr_m = t[1] 62 | 63 | o = cidr_ip.split('.') 64 | cidr_ip = ((int(o[0])<<24) + (int(o[1]) << 16) + (int(o[2]) << 8) + (int(o[3]))) 65 | 66 | cidr_mb = 0 67 | zeros = 32 - int(cidr_m) 68 | for b in range(0, zeros): 69 | cidr_mb = (cidr_mb << 1) | 0x01 70 | cidr_mb = 0xFFFFFFFF & ~cidr_mb 71 | 72 | o = ip.split('.') 73 | ip = ((int(o[0])<<24) + (int(o[1]) << 16) + (int(o[2]) << 8) + (int(o[3]))) 74 | 75 | return ((cidr_ip & cidr_mb) == (ip & cidr_mb)) 76 | 77 | 78 | # 79 | # Shorten the hostname by removing any defined domain suffixes. 80 | # 81 | def shorten_host_name(_host, domains): 82 | host = _host 83 | if (_host == None): 84 | return 'UNKNOWN' 85 | 86 | # some devices (eg Motorola) report as hex strings 87 | if (_host.startswith('0x')): 88 | try: 89 | host = binascii.unhexlify(_host[2:]).decode('utf-8') 90 | except: 91 | # this can fail if the node gives us bad data - revert to original 92 | # ex, lldp can advertise MAC as hostname, and it might not convert 93 | # to ascii 94 | host = _host 95 | 96 | # Nexus appends (SERIAL) to hosts 97 | host = re.sub('\([^\(]*\)$', '', host) 98 | for domain in domains: 99 | host = host.replace(domain, '') 100 | 101 | # fix some stuff that can break Dot 102 | host = re.sub('-', '_', host) 103 | host = host.rstrip(' \r\n\0') 104 | 105 | return host 106 | 107 | 108 | # 109 | # Return a string representation of an IPv4 address 110 | # 111 | def convert_ip_int_str(iip): 112 | if ((iip != None) & (iip != '')): 113 | ip = int(iip, 0) 114 | ip = '%i.%i.%i.%i' % (((ip >> 24) & 0xFF), ((ip >> 16) & 0xFF), ((ip >> 8) & 0xFF), (ip & 0xFF)) 115 | return ip 116 | 117 | return 'UNKNOWN' 118 | 119 | 120 | def get_module_from_interf(port): 121 | try: 122 | s = re.search('[^\d]*(\d*)/\d*/\d*', port) 123 | if (s): 124 | return s.group(1) 125 | except: 126 | pass 127 | return '1' 128 | 129 | 130 | def strip_slash_masklen(cidr): 131 | try: 132 | s = re.search('^(.*)/[0-9]{1,2}$', cidr) 133 | if (s): 134 | return s.group(1) 135 | except: 136 | pass 137 | return cidr 138 | 139 | 140 | def expand_path_pattern(str): 141 | try: 142 | match = re.search('{([^\}]*)}', str) 143 | tokens = match[1].split('|') 144 | except: 145 | return [str] 146 | 147 | ret = [] 148 | for token in tokens: 149 | s = str.replace(match[0], token) 150 | ret.append(s) 151 | 152 | return ret 153 | 154 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __license__ = ''' 2 | This program is free software; you can redistribute it and/or 3 | modify it under the terms of the GNU General Public License 4 | as published by the Free Software Foundation; either version 2 5 | of the License, or (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program; if not, write to the Free Software 14 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 15 | ''' 16 | 17 | from setuptools import setup 18 | import os 19 | 20 | import imp 21 | _version = imp.load_source('', 'natlas/_version.py') 22 | 23 | long_description = open('README.md').read() 24 | 25 | setup( 26 | name = 'natlas', 27 | version = _version.__version__, 28 | author = 'Michael Laforest', 29 | author_email = 'mjlaforest@gmail.com', 30 | license = 'LICENSE', 31 | url = 'http://github.com/MJL85/natlas/', 32 | 33 | description = 'natlas is a collection of Python tools for network professionals.', 34 | long_description = long_description, 35 | keywords = 'python network cisco diagram snmp cdp', 36 | 37 | classifiers = [ 38 | 'Development Status :: 4 - Beta', 39 | 'Intended Audience :: Information Technology', 40 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Topic :: Utilities' 44 | ], 45 | 46 | packages = ['natlas'], 47 | include_package_data = True, 48 | 49 | scripts = [ 'natlas/natlas.py' ], 50 | 51 | install_requires = [ 52 | 'graphviz', 53 | 'pydot', 54 | 'pysnmp>=4.2.5', 55 | 'pyparsing', 56 | 'netaddr>=0.7.14' 57 | ] 58 | ) 59 | --------------------------------------------------------------------------------