From a1baec8e6a35ea066a18827209732d49c6db4fea Mon Sep 17 00:00:00 2001 From: Michael Laforest Date: Sat, 5 May 2018 20:35:34 -0400 Subject: [PATCH] v0.10 --- .gitignore | 4 +- CHANGELOG | 31 +- README.md | 296 +++++++ docs/donate/BCH.png | Bin 0 -> 2444 bytes mnet.conf | 7 +- mnet.py | 399 ++++++---- mnetsuite/__init__.py | 13 +- mnetsuite/_version.py | 2 +- mnetsuite/config.py | 269 ++++--- mnetsuite/graph.py | 799 ------------------- mnetsuite/network.py | 510 ++++++++++++ mnetsuite/node.py | 1474 +++++++++++++++-------------------- mnetsuite/node_stack.py | 127 +++ mnetsuite/node_vss.py | 99 +++ mnetsuite/output.py | 37 + mnetsuite/output_catalog.py | 72 ++ mnetsuite/output_diagram.py | 499 ++++++++++++ mnetsuite/output_stdout.py | 132 ++++ mnetsuite/snmp.py | 389 ++++----- mnetsuite/tracemac.py | 323 ++++---- mnetsuite/util.py | 275 ++++--- setup.py | 65 +- 22 files changed, 3415 insertions(+), 2407 deletions(-) create mode 100644 README.md create mode 100644 docs/donate/BCH.png delete mode 100755 mnetsuite/graph.py create mode 100644 mnetsuite/network.py create mode 100644 mnetsuite/node_stack.py create mode 100644 mnetsuite/node_vss.py create mode 100644 mnetsuite/output.py create mode 100644 mnetsuite/output_catalog.py create mode 100644 mnetsuite/output_diagram.py create mode 100644 mnetsuite/output_stdout.py diff --git a/.gitignore b/.gitignore index f9a99d1..af33353 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ dist *~ *.log - +*.swp +IDEAS +mnetsuite/mac.py diff --git a/CHANGELOG b/CHANGELOG index 3f7ab1d..32ac8d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,40 @@ --------------------- -MNet Suite +mnet Michael Laforest mjlaforest@gmail.com CHANGE LOG -------------------- +v0.10 - 5/5/2018 + - Ported to Python 3 + - Cleaned up and refactored code + - Improved discovery + - Improved discovery console output + - Fixed VSS chassis detection; now finds correct serial# and platform + - All output referencing node IP now uses best IP instead of first found + - Try all known IPs for a node until one works; no longer fails on unreacable IPs (eg, VRFs/ACL blocks/etc) + - Default depth is now 100 + - Added runtime to stdout + - Renamed 'graph' to 'diagram' + + - Cisco ACL-style node matching (replaced config subnets/exclude with discover) + - Added 'leaf' option to stop discovery beyond a matching node + - Added 'include' option to stop discovery at a matching node + + - Config option diagram/node_text replaces the below: + - include_svi + - include_lo + - include_serials + + diagram + - Changed -f option to -o + - If a LAG spans multiple devices (eg, Nexus) override expand_lag for that device + - Output can create multiple files. Ex: -o "file.{svg|png}" will create file.svg and file.png + + getmacs + - Added as new module + v0.9 - graph - Fixed bug with allowed VLAN lists not showing correct ranges. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4f8c93 --- /dev/null +++ b/README.md @@ -0,0 +1,296 @@ +# mnet +mnet Suite - Tools for network professionals. +Michael Laforest `` + +Automated discovery and diagram tools using SNMP, CDP, and LLDP. + +# Support + +If you use any of these tools or find them useful please consider donating. + +Donation Method | Address | QR Code +--- | --- | --- +Bitcoin (BTC) | 1HY3jPYVfE6YZbuYTYfMpazvSKRXjZDMbS | ![1HY3jPYVfE6YZbuYTYfMpazvSKRXjZDMbS](https://github.com/MJL85/mnet/blob/master/docs/donate/BTC.png "Bitcoin (BTC)") +Bitcoin Cash (BCH) | 1HSycjR3LAZxuLG34aEBbQdUSayPkh8XsH | ![1HSycjR3LAZxuLG34aEBbQdUSayPkh8XsH](https://github.com/MJL85/mnet/blob/master/docs/donate/BCH.png "Bitcoin Cash (BCH)") + +# Suite Tools +| Module | Description | +| --- | --- | +| Diagram | Discovers a network and generates a diagram based on CDP and LLDP neighbor information. | +| TraceMAC | Attempts to locate a specific MAC address by recursively looking it up in switch CAM tables. | +| GetMACS | Collect a list of all MAC addresses on the discovered network and generate a report. | + +# Installing mnet + +mnet can be installed through Python's pip. + +`# pip install mnet` + +# Running mnet + +### Network Discovery + +A network discovery will be performed For the `diagram` and `getmacs` modules. The discovery process will use SNMP, CDP, and LLDP to discover the network topology and details about each node. + +The discovery will begin at the specified root node and perform the following actions: + +1. Collect a list of adjacencies through CDP and LLDP. +2. Evaluate each adjacent node against the `discover` ACL. +3. If the ACL permits discovery then collect information from that node. +4. If the current discovered depth is less than the user deviced maximum depth, repeat step 1 with this node. + +The `discover` ACL is defined in the configuration file as: + +``` +"discover" : [ + ACE, + ACE, + ACE +] +``` + +An ACE is defined as: + +``` < [host REGEX] | [ip CIDR] >``` + +| Option | Include Node | Collect Node Information | Allow Discovery of Adjacencies | +| --- |:---:|:---:|:---:| +| permit | X | X | X | +| leaf | X | X | | +| include | X | | | +| deny | | | | + +| Parameter | Description | Example | +| --- | --- | --- | +| host REGEX | The host can be matched against any regular expression string. The host string is what is reported from CDP or LLDP. | `host Router-.*` | +| ip CIDR | The ip can be matched against and CIDR. | `ip 10.50.31.0/24` | + +### Diagram Module + +``` +mnet.py diagram -r + -o + [-d ] + [-c ] + [-t ] + [-C ] +``` +The above command will discover the network and generate a network diagram. + +| Option | Description | +| --- | --- | +| `-r ` | IP address of the network node to start on. | +| `-o ` | The file that the output will be written to.
Common file extensions: `.png`, `.pdf`, `.svg` | +| `-c ` | The JSON configuration file to use. | +| `-d ` | The maximum hop depth to discover, starting at the root node specified by `-r` | +| `-t ` | The title to give your generated network diagram. | +| `-C ` | If specified, mnet will generate a comma separated (CSV) catalog file with a list of all devices discovered. | + +### TraceMAC Module + +``` +mnet.py tracemac -r + -m + [-c ] +``` +The above command will run the `TraceMAC` module and trace a MAC address through CAM tables. + +| Option | Description | +| --- | --- | +| `-r ` | IP address of the network node to start on. | +| `-m ` | The MAC address to locate. Can be in any form. Ex: `11:22:33:44:55:66` or `112233445566` or `1122.3344.5566` | +| `-c ` | The JSON configuration file to use. | + +### GetMACS Module + +``` +mnet.py getmacs -r + -o + [-d ] + [-c ] +``` +Discover the network per the `discover` rules in the configuration file, then generate a CSV output file of all MAC addresses. + +| Option | Description | +| --- | --- | +| `-r ` | IP address of the network node to start on. | +| `-o ` | The comma separated value (.csv) file that the output will be written to. | +| `-d ` | The maximum hop depth to discover, starting at the root node specified by `-r` | +| `-c ` | The JSON configuration file to use. | + + +### Config Module + +``` +mnet.py config +``` +The above command will run the `config` module and output a standard config to stdout. + +Use this module and redirect stdout to a file in order to create a new blank config file. +`# mnet.py config > mnet.conf` + +# Configuration File + +The toolset uses a JSON configuration file for common parameters. + +``` +{ + "snmp" : [ + { "community":"private", "ver":2 }, + { "community":"public", "ver":2 } + ], + "domains" : [ + ".company.net", + ".company.com" + ], + "discover" : [ + "permit ip 10.0.0.0/8", + "permit host Router[1,2]", + "deny ip any", + ], + "diagram" : { + "node_text_size" : 10, + "link_text_size" : 9, + "title_text_size" : 15, + "get_stack_members" : 0, + "get_vss_members" : 0, + "expand_stackwise" : 0, + "expand_vss" : 0, + "expand_lag" : 1, + "group_vpc" : 0 + } + +} +``` + +| Block / Variable | Description | +| --- | --- | +| `snmp` | Defines a list of SNMP credentials. When connecting to a node, each of these credentials is tried in order until one is successful. This allows crawling a large network with devices that potentially use different SNMP credentials. | +| `discover` | Defines a Cisco-style ACL. See the `Network Discovery` section. | +| `diagram` | Defines specific values used to change diagram attributes. Detailed below in the *Diagram block* table. | + +**Diagram block** + +| Variable | Type | Default Value | Description | +| --- | --- | --- | --- | +| `node_text_size` | integer | `10` | Node text size. | +| `link_text_size` | integer | `9` | Link text size. | +| `title_text_size` | integer | `15` | Diagram title text size. | +| `get_stack_members` | bool | `0` | If set to `1`, nodes will include details about stackwise members. | +| `get_vss_members` | bool | `0` | If set to `1`, nodes will include details about VSS members. | +| `expand_stackwise` | bool | `0` | If set to `1`, nodes belonging to stackwise groups will be expanded to show each member as a node. | +| `expand_vss` | bool | `0` | If set to `1`, nodes belonging to VSS groups will be expanded to show each member as a node. | +| `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. | +| `group_vpc` | bool | `0` | If set to `1`, VPC peers will be grouped together on the diagram, otherwise they will not be clustered. | + +# mnet's Diagram Module + +### Details + +A network discovery will be performed and a network diagram will be generated. + +mnet will attempt to collect the following information and include it in the generated diagram: ++ All devices (via CDP and LLDP) ++ Interface names ++ IP addresses ++ VLAN memberships ++ Etherchannel memberships (LACP only) ++ Identify trunk links ++ Identify switched links ++ Identify routed links ++ BGP Local AS ++ OSPF Router ID ++ HSRP Virtual IP ++ HSRP Priority ++ VSS Domain ++ Stackwise membership ++ VPC peerlink information + +mnet's Diagram module attempts to include all of the above information in the diagram in an intuitive way. The keep the diagram clean, the following are used: ++ Nodes + + Circle nodes represent layer 2 switches. + + Diamond nodes represent layer 3 switches or routers. + + If a node has multiple borders then either VSS or StackWise is enabled. + + VSS - Will always have a double border. + + StackWise - The number of borders denotes the number of switches in the stack. + + If the configuration specifies, VSS/VPC/Stackwise nodes will be grouped in larger squares. ++ Links + + 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*. + + 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. + + 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. + +### Examples + +Example 1 +![MNet-Diagram Ex1](http://i.imgur.com/Mny7PLl.png "MNet-Graph Ex1") + +Example 2 +![MNet-Diagram Ex2](http://i.imgur.com/BuXnzWG.png "MNet-Graph Ex2") + +Example 3 +![MNet-Diagram Ex3](http://i.imgur.com/i1dqM09.png "MNet-Graph Ex3") + +# mnet's TraceMAC Module + +### Examples + +The below example shows a trace for MAC address `00:23:68:63:75:70` starting +at node `10.10.0.3`. The MAC address is found on switch `IDF3_D` on port +`Gi0/11`. + +``` +# mnet.py tracemac -r 10.10.0.3 -m 0023.6863.7570 +MNet Suite v0.7 +Written by Michael Laforest + + Config file: ./mnet.conf + Root node: 10.10.0.3 + MAC address: 0023.6863.7570 + + + +Start trace. +------------ +IDF1_A (10.10.0.3) + VLAN: 1 + Port: Gi1/3 + Next Node: IDF1_B + Next Node IP: 10.10.0.2 +------------ +IDF1_B (10.10.0.2) + VLAN: 1 + Port: Gi0/24 + Next Node: IDF3_D + Next Node IP: 10.10.0.6 +------------ +IDF3_D (10.10.0.6) + VLAN: 1 + Port: Gi0/11 +------------ +Trace complete. +``` + +# FAQ + +**Q.** `My diagram is too large. I only want to diagram part of my network.` + +**A.** 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. + +**Q.** `My diagram is still too wide. What can I do?` + +**A.** Try to use the Graphviz `unflatted` command line program to reformat the generated dot file. + +**Q.** `Where is the config file?` + +*A.* If you need a config file you can generate a new one with `# mnet.py config > mnet.conf` . + +**Q.** `I need a diagram with less proprietary information. Can I get one without IPs or serial numbers?` + +*A.* Yes, 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: + +``` +"diagram" : { + node_text = '{node.name}
{node.ios}
{node.plat}' +} +``` + diff --git a/docs/donate/BCH.png b/docs/donate/BCH.png new file mode 100644 index 0000000000000000000000000000000000000000..e39c84798d78cd858516a6dbafc858526d143f8d GIT binary patch literal 2444 zcmY*bc{tSDAO2p-nr$eg$P%h)7$jSgF`_Fe8AO&Dj9sz}Cekfr4>!vtk}ZZY?v%k` z>|2(yjTu{+&^GprZTR`!ez)Jff1J;Gp7VL$^S+<+oacGpSPS#Z0(?jL000m$H8Hxe zzd!o#;^Ek@Q;K3s0KjW$YINylNDec{$J>%B*_ns+>5NSqQ;ZcgL~<*C6fhJn(tzEf zKJ~moz9ceT0`vndbhnIFVn%!2iN-GPeovyIUs`fB^`u4^veB2m6-1Bmz$m1a+ z-op27vL8pPL@5eM;RE`awOk9e`WaZi6&GwoUO9H)O#qt}kc-U?NIKaeA*}U;Iu6XJ zn;bHFBJubHRQM9|#BXf|2Vx)a=q^Spm==;1^vW4kaK(7D;vVlQi*VS{7abZz+X!C+3HXow~{Dfo#FD;pDVoD z{~_WLGWsw!hEK#c&8G5elnwjB6ZGE``1Uqw=Bd7sZtzE*rEvcwk^OvIv>LfsZi{_U z?sS&Lm-Y{UaGCe}dpWktutG;^#G0!D3;uK3zybAg<<>KP2U7f}<3cZVR}xay#&^+DLegGk zyuZIuBW1oIY{s_nU)v!REc|-1S}jqtDzu|R7?%2+y&B}W>0lT15a-SxI?+#8%69O? z>np#}9O001ICSQ&5#0yxmN5`D#cR$|wFnUamcS`Go_P_Tw{3!J0B+4cuHag?^` z-kUC|{4pze=Jw?Af!8}Vz1L4oRYH0}oZoRw&*0jZAV`e(^Dcgy5dZhRT?2!(-%Cfc zgdCs}mi zdd9u*yw+p^vdRVPOqCiS?Y2R5ML&ne*wbxl$pl#LNwy`Lr9bIDN?f9UxKLo?XfiKZ z`@G_4)#3DKrW4*3p)J;p;d)07T>SR(OAq1I(A*zaql+2m#nP>OEYpirD1&vWraDX= zhwf2OA@We~&e>|UcCwViF?ZZ%%kEYIpC#agwxafz;U_9$i<()LdwRLK))h4#0k(JQ z=BuJnh%xVDuv3Bgf@?AYZf4q{OmfO_TR3U|ZH-yJ6zsJoVke`#&(YB;-6s{6zF@9s zEDmWrzoWD0N@Yy!tTd8Vqk^1AL4Z*Hp7JZCj{yUmOX*wf8RwSCLCJV%SJmF@M4tu7 zs>{C0baB(D+k%VvKGPm6fgA>DCB(M5D`?TO^)^JYH0>+@L2(W5_cTYIMcag(8ffFy z*MXZiiO1zvWdwS2*sN#jweL(hJix(0cRk#-iDITvdxv)|@ivHVRSO}h&Gib^aV-f{ zG-VPO#$I6#0l*2LQt!zd?>K-6OeeFwd1CS?b#x*ozcv6zo5kX$)Zs#R=#CiGuxJ$v zd}v-2s(GR$vP2Z9FS3*N+t?B(HRh0Q7GCm&=q!eJHlRN(o%0}3e0hpsYe<*LZ0LIY z6iU(+UcgpdY^;3Uzy;W*$Ck)PHR}z`b}y`=l&o4q+^n>hU6P+aLvS++4^^xXS-mu_43C?mTm4pv=@T6)~Ofg&sK0pvh9+l5*+wCxunY>t7$_)1D%%#`&y0k+-Aupf zA@0=F5|5$GMwSrofUls_HV^#XqEp-{?db-J#;f*iKi z7vH+CI7L@#!=7PK6q5KX1wn_{ZL>f`qS}i@SbVRGw8;~T4wY+ z*Yztg-^|J(2@rcTyfG2HO7`khxwDtc4^;g`hicPx`U+DtK2OytaX~QAtw6oBzYYy1P>(%{ zOEGud+rl}+uVZRTH@X6m8U=+XpWIdFrC?e1a)Gn`S&cj2yLAf9tChjWw7yX%VmVy}uce{=LZF!@D(-fJ)PU_t>UJnc{BulJB=;hA zs3EuctC<2tC3XH({X!rOCuN`f4L2vr3zUwGElq?ba%lU0zg#zcY$YMGuTZcd(Eh;aMH!g3ku1>#}~`7bIlkfVuV@LC!d&3V~~7|cNzz| z{_O2W12UC*)1j*S)VRw@T$?yzu0xaot4fI{qB!|!30rfQKn&+Z;>NTDA@;$R&zC&` zSZ=e=@JtX&p5ohJ!(>PO-`a+bWEkB==p`u7d*fu}*k8iQLtdPrTyj5roUlVUL}CPO zRh|~Xj^gxwo(tdNJkLKxuQ{ywDe;V)rcFcW!=HZ4Gh3X{3tw9x&V2V$V7VS>80aTv z%}Gif`r-BazfOWS=2fVYQqzLNk?K=wd4&Ezd#nB+Yzk>oOHK*mXjt4vc+V7vc5p@i z2oYk?)~=(Gw%8)0nHUpYx!{!(|FZPl`06d;dK3MgIL<7+F7^MVk4F~01mrVXoVtJ5 zstIE8;fo=>l*A7iqs)6;%O^fc{yVDt=\n' - ' -f \n' - ' [-d ]\n' - ' [-c ]\n' - ' [-t ]\n' - ' [-C ]\n' - '\n' - ' mnet.py tracemac -r \n' - ' -m \n' - ' [-c ]\n' - '\n' - ' mnet.py config\n' - ) + print('Usage:\n' + ' mnet.py diagram -r \n' + ' -o \n' + ' [-d ]\n' + ' [-c ]\n' + ' [-t ]\n' + ' [-C ]\n' + '\n' + ' mnet.py tracemac -r \n' + ' -m \n' + ' [-c ]\n' + '\n' + ' mnet.py getmacs -r \n' + ' -o \n' + ' [-d ]\n' + ' [-c ]\n' + '\n' + ' mnet.py config\n' + ) def print_banner(): - print('MNet Suite v%s' % mnetsuite.__version__) - print('Written by Michael Laforest ') - print('') + print('mnet suite v%s' % mnetsuite.__version__) + print('Michael Laforest ') + print('') def main(argv): - opt_root_ip = None - if (len(argv) < 1): - print_banner() - print_syntax() - return - - mod = argv[0] - if (mod == 'graph'): - print_banner() - graph(argv[1:]) - elif (mod == 'tracemac'): - print_banner() - tracemac(argv[1:]) - elif (mod == 'config'): - generate_config() - else: - print_banner() - print_syntax() - - -def graph(argv): - max_depth = 0 - - graph = mnetsuite.mnet_graph() - - opt_dot = None - opt_depth = 0 - opt_title = 'MNet Network Diagram' - opt_conf = './mnet.conf' - opt_catalog = None - - try: - opts, args = getopt.getopt(argv, 'f:d:r:t:F:c:C:') - except getopt.GetoptError: - print_syntax() - sys.exit(1) - for opt, arg in opts: - if (opt == '-r'): - opt_root_ip = arg - if (opt == '-f'): - opt_dot = arg - if (opt == '-d'): - opt_depth = int(arg) - max_depth = int(arg) - if (opt == '-t'): - opt_title = arg - if (opt == '-c'): - opt_conf = arg - if (opt == '-C'): - opt_catalog = arg - - if ((opt_root_ip == None) | (opt_dot == None)): - print_syntax() - print('Invalid arguments.') - return - - print(' Config file: %s' % opt_conf) - print(' Root node: %s' % opt_root_ip) - print(' Output file: %s' % opt_dot) - print(' Crawl depth: %s' % opt_depth) - print(' Diagram title: %s' % opt_title) - print('Out Catalog file: %s' % opt_catalog) - - print('\n\n') - - # load the config - if (graph.load_config(opt_conf) == 0): - return - graph.set_max_depth(opt_depth) - - # start - graph.crawl(opt_root_ip) - - # outputs - graph.output_stdout() - - if (opt_dot != None): - graph.output_dot(opt_dot, opt_title) - - if (opt_catalog != None): - graph.output_catalog(opt_catalog) + opt_root_ip = None + if (len(argv) < 1): + print_banner() + print_syntax() + return + + start = timer() + mod = argv[0] + if (mod == 'diagram'): + print_banner() + diagram(argv[1:]) + elif (mod == 'tracemac'): + print_banner() + tracemac(argv[1:]) + elif (mod == 'getmacs'): + print_banner() + getmacs(argv[1:]) + elif (mod == 'config'): + generate_config() + else: + print_banner() + print_syntax() + + s = timer() - start + h=int(s/3600) + m=int((s-(h*3600))/60) + s=s-(int(s/3600)*3600)-(m*60) + print('Completed in %i:%i:%.2fs' % (h, m, s)) + +def diagram(argv): + opt_root_ip = None + opt_output = None + opt_catalog = None + opt_depth = DEFAULT_OPT_DEPTH + opt_title = DEFAULT_OPT_TITLE + opt_conf = DEFAULT_OPT_CONF + + try: + opts, args = getopt.getopt(argv, 'o:d:r:t:F:c:C:') + except getopt.GetoptError: + print_syntax() + sys.exit(1) + for opt, arg in opts: + if (opt == '-r'): + opt_root_ip = arg + if (opt == '-o'): + opt_output = arg + if (opt == '-d'): + opt_depth = int(arg) + if (opt == '-t'): + opt_title = arg + if (opt == '-c'): + opt_conf = arg + if (opt == '-C'): + opt_catalog = arg + + if ((opt_root_ip == None) | (opt_output == None)): + print_syntax() + print('Invalid arguments.') + return + + print(' Config file: %s' % opt_conf) + print(' Output file: %s' % opt_output) + print('Out Catalog file: %s' % opt_catalog) + print(' Root node: %s' % opt_root_ip) + print(' Discover depth: %s' % opt_depth) + print(' Diagram title: %s' % opt_title) + print() + + # load the config + config = mnetsuite.mnet_config() + if (config.load(opt_conf) == 0): + return 0 + + # start discovery + network = mnetsuite.mnet_network(config) + network.set_max_depth(opt_depth) + network.discover(opt_root_ip) + network.discover_details() + + # outputs + #stdout = mnetsuite.mnet_output_stdout(network) + #stdout.generate() + + if (opt_output != None): + diagram = mnetsuite.mnet_output_diagram(network) + diagram.generate(opt_output, opt_title) + + if (opt_catalog != None): + catalog = mnetsuite.mnet_output_catalog(network) + catalog.generate(opt_catalog) def tracemac(argv): - trace = mnetsuite.mnet_tracemac() - - opt_root_ip = None - opt_conf = './mnet.conf' - opt_mac = None - - try: - opts, args = getopt.getopt(argv, 'r:c:m:') - except getopt.GetoptError: - print_syntax() - return - for opt, arg in opts: - if (opt == '-r'): - opt_root_ip = arg - if (opt == '-c'): - opt_conf = arg - if (opt == '-m'): - opt_mac = arg - - if ((opt_root_ip == None) | (opt_mac == None)): - print_syntax() - print('Invalid arguments.') - return - - print(' Config file: %s' % opt_conf) - print(' Root node: %s' % opt_root_ip) - print(' MAC address: %s' % opt_mac) - - print('\n\n') - - mac = trace.parse_mac(opt_mac) - if (mac == None): - print('MAC address is invalid.') - return - - # load config - trace.load_config(opt_conf) - - # start - print('Start trace.') - print('------------') - - ip = opt_root_ip - while (ip != None): - ip = trace.trace(ip, mac) - print('------------') - - print('Trace complete.\n') + opt_root_ip = None + opt_mac = None + opt_conf = DEFAULT_OPT_CONF + + try: + opts, args = getopt.getopt(argv, 'r:c:m:') + except getopt.GetoptError: + print_syntax() + return + for opt, arg in opts: + if (opt == '-r'): + opt_root_ip = arg + if (opt == '-c'): + opt_conf = arg + if (opt == '-m'): + opt_mac = arg + + if ((opt_root_ip == None) | (opt_mac == None)): + print_syntax() + print('Invalid arguments.') + return + + print(' Config file: %s' % opt_conf) + print(' Root node: %s' % opt_root_ip) + print(' MAC address: %s' % opt_mac) + print('\n') + + # load the config + config = mnetsuite.mnet_config() + if (config.load(opt_conf) == 0): + return 0 + + trace = mnetsuite.mnet_tracemac(config) + + # start + print('Start trace.') + print('------------') + + ip = opt_root_ip + while (ip != None): + ip = trace.trace(ip, opt_mac) + print('------------') + + print('Trace complete.\n') + + +def getmacs(argv): + opt_root_ip = None + opt_output = None + opt_conf = DEFAULT_OPT_CONF + opt_depth = DEFAULT_OPT_DEPTH + + try: + opts, args = getopt.getopt(argv, 'o:r:c:d:') + except getopt.GetoptError: + print_syntax() + return + for opt, arg in opts: + if (opt == '-r'): + opt_root_ip = arg + if (opt == '-d'): + opt_depth = int(arg) + if (opt == '-c'): + opt_conf = arg + if (opt == '-o'): + opt_output = arg + + if ((opt_root_ip == None) | (opt_output == None)): + print_syntax() + print('Invalid arguments.') + return + + print(' Config file: %s' % opt_conf) + print(' Output file: %s' % opt_output) + print(' Root node: %s' % opt_root_ip) + print(' Discover depth: %s' % opt_depth) + print('\n') + + # load the config + config = mnetsuite.mnet_config() + if (config.load(opt_conf) == 0): + return 0 + + # start discovery + network = mnetsuite.mnet_network(config) + network.set_max_depth(opt_depth) + network.discover(opt_root_ip) + + # get macs + mac = mnetsuite.mnet_mac(config) + macs = mac.get_macs_from_network_discovery(network, 1) + + # generate output csv + if (opt_output): + mac.output_csv(opt_output) def generate_config(): - conf = mnetsuite.config.mnet_config() - print('%s' % conf.generate_new()) + conf = mnetsuite.config.mnet_config() + print('%s' % conf.generate_new()) if __name__ == "__main__": - main(sys.argv[1:]) - + main(sys.argv[1:]) + diff --git a/mnetsuite/__init__.py b/mnetsuite/__init__.py index 238e21b..5af0b17 100644 --- a/mnetsuite/__init__.py +++ b/mnetsuite/__init__.py @@ -1,3 +1,10 @@ -from .graph import mnet_graph -from .tracemac import mnet_tracemac -from ._version import __version__ +from .config import mnet_config +from .network import mnet_network +from .output import mnet_output +from .output_stdout import mnet_output_stdout +from .output_diagram import mnet_output_diagram +from .output_catalog import mnet_output_catalog +#from .mac import mnet_mac +from .tracemac import mnet_tracemac +from ._version import __version__ + diff --git a/mnetsuite/_version.py b/mnetsuite/_version.py index 732155f..91bf823 100644 --- a/mnetsuite/_version.py +++ b/mnetsuite/_version.py @@ -1 +1 @@ -__version__ = "0.8.3" +__version__ = "0.10" diff --git a/mnetsuite/config.py b/mnetsuite/config.py index 6f943dc..2ba1725 100755 --- a/mnetsuite/config.py +++ b/mnetsuite/config.py @@ -1,130 +1,169 @@ #!/usr/bin/python ''' - MNet Suite - config.py + MNet Suite + config.py - Michael Laforest - mjlaforest@gmail.com + Michael Laforest + mjlaforest@gmail.com - Copyright (C) 2015 Michael Laforest + Copyright (C) 2015-2018 Michael Laforest - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ''' import json -class mnet_config_graph: - node_text_size = 8 - link_text_size = 7 - title_text_size = 15 - include_svi = False - include_lo = False - include_serials = False - get_stack_members = False - get_vss_members = False - expand_stackwise = False - expand_vss = False - expand_lag = True +class mnet_config_diagram: + node_text_size = 8 + link_text_size = 7 + title_text_size = 15 + get_stack_members = False + get_vss_members = False + expand_stackwise = False + expand_vss = False + expand_lag = True + group_vpc = False + node_text = '{node.name}
' \ + '{node.ip}
' \ + '<%if {node.ios}: {node.ios}
%>' \ + '<%if {node.plat}: {node.plat}
%>' \ + '<%if ("{node.serial}"!=None)&({node.vss.enabled}==0)&({node.stack.enabled}==0): {node.serial}
%>' \ + '<%if ({node.stack.enabled}==1)&({config.diagram.expand_stackwise}==1): {stack.serial}
%>' \ + '<%if {node.vss.enabled}&({config.diagram.expand_vss}==1): {vss.serial}
%>' \ + '<%if ({node.vss.enabled}==1)&({config.diagram.expand_vss}==0): VSS {node.vss.domain}
%>' \ + '<%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}
%>' \ + '<%if {node.bgp_las}: BGP {node.bgp_las}
%>' \ + '<%if {node.ospf_id}: OSPF {node.ospf_id}
%>' \ + '<%if {node.hsrp_pri}: HSRP VIP {node.hsrp_vip}
HSRP Pri {node.hsrp_pri}
%>' \ + '<%if {node.stack.enabled}: Stackwise {node.stack.count}
%>' \ + '<%stack SW {stack.num} - {stack.plat} {stack.serial} ({stack.role})
%>' \ + '<%loopback {lo.name} - {lo.ip}
%>' \ + '<%svi VLAN {svi.vlan} - {svi.ip}
%>' + +class mnet_discover_acl: + ''' + Define an ACL entry for the 'discover' config block. + Defined in the form: + + Where + = permit, deny, leaf, nop + = ip, host + = string + ''' + all_actions = [ ";", "permit", "deny", "leaf", "include" ] + all_types = [ ";", "ip", "host" ] + + def __init__(self, str): + self.action = "nop" + self.type = "nop" + self.str = "nop" + + t = list(filter(None, str.split())) + if (len(t) < 3): + raise Exception('Invalid ACL entry: "%s"' % str) + + self.action = t[0] + self.type = t[1] + self.str = t[2] + + if (self.action not in self.all_actions): + raise Exception('Invalid ACL entry: "%s"; %s' % (str, self.action)) + if (self.type not in self.all_types): + raise Exception('Invalid ACL entry: "%s"; %s' % (str, self.type)) + + def __repr__(self): + return '<%s %s %s>' % (self.action, self.type, self.str) class mnet_config: - host_domains = [] - snmp_cred = [] - exclude_subnets = [] - allowed_subnets = [] - exclude_hosts = [] - graph = None - - def __init__(self): - self.host_domains = [] - self.snmp_creds = [] - self.exclude_subnets = [] - self.allowed_subnets = [] - self.graph = mnet_config_graph() - - def load(self, filename): - # load config - json_data = self._load_json_conf(filename) - if (json_data == None): - return 0 - - self.host_domains = json_data['domains'] - self.snmp_creds = json_data['snmp'] - self.exclude_subnets = json_data['exclude'] - self.allowed_subnets = json_data['subnets'] - self.exclude_hosts = json_data['exclude_hosts'] - - json_graph = json_data.get('graph', None) - if (json_graph != None): - self.graph.node_text_size = json_graph.get('node_text_size', 8) - self.graph.link_text_size = json_graph.get('link_text_size', 7) - self.graph.title_text_size = json_graph.get('title_text_size', 15) - self.graph.include_svi = json_graph.get('include_svi', False) - self.graph.include_lo = json_graph.get('include_lo', False) - self.graph.include_serials = json_graph.get('include_serials', False) - self.graph.get_stack_members = json_graph.get('get_stack_members', False) - self.graph.get_vss_members = json_graph.get('get_vss_members', False) - self.graph.expand_stackwise = json_graph.get('expand_stackwise', False) - self.graph.expand_vss = json_graph.get('expand_vss', False) - self.graph.expand_lag = json_graph.get('expand_lag', True) - - return 1 - - def _load_json_conf(self, json_file): - json_data = None - - try: - json_data = json.loads(open(json_file).read()) - - except: - print('Invalid JSON file or file not found.') - return None - - return json_data - - def generate_new(self): - return '{\n' \ - ' "snmp" : [\n' \ - ' { "community":"private", "ver":2 },\n' \ - ' { "community":"public", "ver":2 }\n' \ - ' ],\n' \ - ' "domains" : [\n' \ - ' ".company.net",\n' \ - ' ".company.com"\n' \ - ' ],\n' \ - ' "exclude" : [\n' \ - ' "192.168.0.0/16"\n' \ - ' ],\n' \ - ' "subnets" : [\n' \ - ' "10.0.0.0/8",\n' \ - ' "0.0.0.0/32"\n' \ - ' ],\n' \ - ' "exclude_hosts": [\n' \ - ' ],\n' \ - ' "graph" : {\n' \ - ' "node_text_size" : 10,\n' \ - ' "link_text_size" : 9,\n' \ - ' "title_text_size" : 15,\n' \ - ' "include_svi" : 0,\n' \ - ' "include_lo" : 0,\n' \ - ' "include_serials" : 0,\n' \ - ' "get_stack_members" : 0,\n' \ - ' "get_vss_members" : 0,\n' \ - ' "expand_stackwise" : 0,\n' \ - ' "expand_vss" : 0,\n' \ - ' "expand_lag" : 1\n' \ - ' }\n' \ - '}' + def __init__(self): + self.host_domains = [] + self.snmp_creds = [] + self.discover_acl = [] + self.diagram = mnet_config_diagram() + + def load(self, filename): + # load config + json_data = self.__load_json_conf(filename) + if (json_data == None): + return 0 + + self.host_domains = json_data['domains'] + self.snmp_creds = json_data['snmp'] + + # parse 'discover' block ACL entries + for acl in json_data['discover']: + try: + entry = mnet_discover_acl(acl) + except Exception as e: + print(e) + return 0 + + self.discover_acl.append(entry) + + json_diagram = json_data.get('diagram', None) + if (json_diagram != None): + self.diagram.node_text_size = json_diagram.get('node_text_size', 8) + self.diagram.link_text_size = json_diagram.get('link_text_size', 7) + self.diagram.title_text_size = json_diagram.get('title_text_size', 15) + self.diagram.get_stack_members = json_diagram.get('get_stack_members', False) + self.diagram.get_vss_members = json_diagram.get('get_vss_members', False) + self.diagram.expand_stackwise = json_diagram.get('expand_stackwise', False) + self.diagram.expand_vss = json_diagram.get('expand_vss', False) + self.diagram.expand_lag = json_diagram.get('expand_lag', True) + self.diagram.group_vpc = json_diagram.get('group_vpc', False) + self.diagram.node_text = json_diagram.get('node_text', self.diagram.node_text) + + return 1 + + def __load_json_conf(self, json_file): + json_data = None + + try: + json_data = json.loads(open(json_file).read()) + except: + print('Invalid JSON file or file not found.') + return None + + return json_data + + def generate_new(self): + return '{\n' \ + ' "snmp" : [\n' \ + ' { "community":"private", "ver":2 },\n' \ + ' { "community":"public", "ver":2 }\n' \ + ' ],\n' \ + ' "domains" : [\n' \ + ' ".company.net",\n' \ + ' ".company.com"\n' \ + ' ],\n' \ + ' "discover" : [\n' \ + ' "permit ip 10.0.0.0/8",\n' \ + ' "permit ip 192.168.1.0/24",\n' \ + ' "permit ip 0.0.0.0/32"\n' \ + ' ],\n' \ + ' "diagram" : {\n' \ + ' "node_text_size" : 10,\n' \ + ' "link_text_size" : 9,\n' \ + ' "title_text_size" : 15,\n' \ + ' "get_stack_members" : 0,\n' \ + ' "get_vss_members" : 0,\n' \ + ' "expand_stackwise" : 0,\n' \ + ' "expand_vss" : 0,\n' \ + ' "expand_lag" : 1,\n' \ + ' "group_vpc" : 0\n' \ + ' }\n' \ + '}' diff --git a/mnetsuite/graph.py b/mnetsuite/graph.py deleted file mode 100755 index 5630f57..0000000 --- a/mnetsuite/graph.py +++ /dev/null @@ -1,799 +0,0 @@ -#!/usr/bin/python - -''' - MNet Suite - graph.py - - Michael Laforest - mjlaforest@gmail.com - - Copyright (C) 2015 Michael Laforest - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -''' - -import sys -import getopt -import dot_parser -import pydot -import datetime -import os -import binascii - -from snmp import * -from config import mnet_config -from util import * -from node import * -from _version import __version__ - - -class mnet_graph_dot_node: - ntype = None - shape = None - style = None - peripheries = 0 - label = None - - def __init__(self): - self.ntype = 'single' - self.shape = 'ellipse' - self.style = 'solid' - self.peripheries = 1 - self.label = '' - self.vss_label = '' - - -class mnet_graph: - root_node = None - - nodes = [] - max_depth = 0 - config = None - - def __init__(self): - self.config = mnet_config() - - def load_config(self, config_file): - if (config_file): - self.config.load(config_file) - - def set_max_depth(self, depth): - self.max_depth = depth - - - def _reset_crawled(self): - for n in self.nodes: - n.crawled = 0 - - - def crawl(self, ip): - # pull info for this node - node = self._get_node(ip, 0, 'root') - if (node != None): - self._crawl_node(node, 0) - - self.root_node = node - - # we may have missed chassis info - for n in self.nodes: - if ((n.serial == None) | (n.plat == None) | (n.ios == None)): - n.opts.get_chassis_info = True - n.query_node() - - return - - - def _print_step(self, ip, name, indicator, depth, discovered_proto, can_connect): - if (discovered_proto == 'cdp'): - sys.stdout.write('[ cdp]') - elif (discovered_proto == 'lldp'): - sys.stdout.write('[lldp]') - else: - sys.stdout.write(' ') - - sys.stdout.write(indicator) - - for i in range(0, depth): - sys.stdout.write('.') - - if (can_connect == 1): - print('%s (%s)' % (name, ip)) - else: - print('UNKNOWN (%s) << UNABLE TO CONNECT WITH SNMP' % ip) - - - def _get_node(self, ip, depth, discovered_proto): - node = mnet_node() - node.name = 'UNKNOWN' - node.ip = [ip] - - # vmware ESX reports the IP as 0.0.0.0 - # return a minimal node since we don't have - # a real IP. - # LLDP can return an empty string for IPs. - if ((ip == '0.0.0.0') | (ip == '')): - self.nodes.append(node) - return node - - # see if we know about this node by its IP first. - # this would save us an SNMP query for the hostname. - for ex in self.nodes: - for exip in ex.ip: - if (exip == ip): - return ex - - # find valid credentials for this node - if (node.try_snmp_creds(self.config.snmp_creds) == 0): - self._print_step(ip, None, '+', depth, discovered_proto, 0) - self.nodes.append(node) - return node - - node.name = node._get_system_name(self.config.host_domains) - - # verify this node isn't already in our visited - # list by checking for its hostname - for ex in self.nodes: - if (ex.name == node.name): - for exip in ex.ip: - if (exip == ip): - return ex - ex.ip.append(ip) - return ex - - # print some info to stdout - self._print_step(ip, node.name, '+', depth, discovered_proto, 1) - - node.opts.get_router = True - node.opts.get_ospf_id = True - node.opts.get_bgp_las = True - node.opts.get_hsrp_pri = True - node.opts.get_hsrp_vip = True - node.opts.get_serial = self.config.graph.include_serials - node.opts.get_stack = True - node.opts.get_stack_details = self.config.graph.get_stack_members - node.opts.get_vss = True - node.opts.get_vss_details = self.config.graph.get_vss_members - node.opts.get_svi = self.config.graph.include_svi - node.opts.get_lo = self.config.graph.include_lo - - node.query_node() - self.nodes.append(node) - - return node - - - # - # Crawl device at this IP. - # Recurse down a level if 'depth' > 0 - # - def _crawl_node(self, node, depth): - if (node == None): - return - - if (depth >= self.max_depth): - return - - if (node.crawled > 0): - return - node.crawled = 1 - - # vmware ESX can report IP as 0.0.0.0 - # If we are allowing 0.0.0.0/32 in the config, - # then we added it as a leaf, but don't crawl it - if (node.ip[0] == '0.0.0.0'): - return - - # may be a leaf we couldn't connect to previously - if (node.snmpobj.success == 0): - return - - # print some info to stdout - self._print_step(node.ip[0], node.name, '>', depth, '', 1) - - # get the cached snmp credentials - snmpobj = node.snmpobj - - # list of valid neighbors to crawl next - valid_neighbors = [] - - # get list of CDP neighbors - cdp_neighbors = node.get_cdp_neighbors() - - # get list of LLDP neighbors - lldp_neighbors = node.get_lldp_neighbors() - - if ((cdp_neighbors == None) & (lldp_neighbors == None)): - return - - neighbors = cdp_neighbors + lldp_neighbors - - for n in neighbors: - # if the remote IP is not allowed, stop processing it here - if (self.is_node_allowed(n.remote_ip) == 0): - continue - - # excluded device name? - stop = 0 - for d in self.config.exclude_hosts: - if (re.search(d, n.remote_name)): - stop = 1 - break - if (stop): - continue - - # get the child info - if (n.remote_ip != 'UNKNOWN'): - child = self._get_node(n.remote_ip, depth+1, n.discovered_proto) - if (child != None): - # if we couldn't pull info from SNMP fill in what we know - if (child.snmpobj.success == 0): - child.name = shorten_host_name(n.remote_name, self.config.host_domains) - self._print_step(n.remote_ip, n.remote_name, '+', depth, n.discovered_proto, 1) - - # CDP/LLDP advertises the platform - child.plat = n.remote_platform - child.ios = n.remote_ios - - # link child to parent - n.node = child - if (self.add_link(node, n) == 1): - valid_neighbors.append(child) - - # crawl the valid neighbors - for n in valid_neighbors: - self._crawl_node(n, depth+1) - - - # - # Returns 1 if the IP is allowed to be crawled. - # - def is_node_allowed(self, ip): - if ((ip == 'UNKNOWN') | (ip == '')): - return 1 - - ipaddr = None - if (USE_NETADDR): - ipaddr = IPAddress(ip) - - # check exclude nodes - for e in self.config.exclude_subnets: - if (USE_NETADDR): - if (ip in IPNetwork(e)): - return 0 - else: - if (is_ipv4_in_cidr(ip, e)): - return 0 - - # check allowed subnets - if ((self.config.allowed_subnets == None) | (len(self.config.allowed_subnets) == 0)): - return 1 - - for s in self.config.allowed_subnets: - if (USE_NETADDR): - if (ipaddr in IPNetwork(s)): - return 1 - else: - if (is_ipv4_in_cidr(ip, s)): - return 1 - - return 0 - - - # - # Add or update a link. - # Return - # 0 - Found an existing link and updated it - # 1 - Added as a new link - # - def add_link(self, node, link): - if (link.node.crawled == 1): - # both nodes have been crawled, - # so try to update existing reverse link info - # instead of adding a new link - for n in self.nodes: - # find the child, which was the original parent - if (n.name == link.node.name): - # find the existing link - for ex_link in n.links: - if ((ex_link.node.name == node.name) & (ex_link.local_port == link.remote_port)): - if ((link.local_if_ip != 'UNKNOWN') & (ex_link.remote_if_ip == None)): - ex_link.remote_if_ip = link.local_if_ip - - if ((link.local_lag != 'UNKNOWN') & (ex_link.remote_lag == None)): - ex_link.remote_lag = link.local_lag - - if ((len(link.local_lag_ips) == 0) & len(ex_link.remote_lag_ips)): - ex_link.remote_lag_ips = link.local_lag_ips - - if ((link.local_native_vlan != None) & (ex_link.remote_native_vlan == None)): - ex_link.remote_native_vlan = link.local_native_vlan - - if ((link.local_allowed_vlans != None) & (ex_link.remote_allowed_vlans == None)): - ex_link.remote_allowed_vlans = link.local_allowed_vlans - - return 0 - else: - for ex_link in node.links: - if ((ex_link.node.name == link.node.name) & (ex_link.local_port == link.local_port)): - # haven't crawled yet but somehow we have this link twice. - # maybe from different discovery processes? - return 0 - - node.add_link(link) - return 1 - - - def _output_stdout(self, node): - if (node == None): - return (0, 0) - if (node.crawled > 0): - return (0, 0) - node.crawled = 1 - - ret_nodes = 1 - ret_links = 0 - - print('-----------------------------------------') - print(' Name: %s' % node.name) - print(' IP: %s' % node.ip[0]) - print(' Platform: %s' % node.plat) - print(' IOS Ver: %s' % node.ios) - - if ((node.vss.enabled == 0) & (node.stack.count == 0)): - print(' Serial: %s' % node.serial) - - print(' Routing: %s' % ('yes' if (node.router == 1) else 'no')) - print(' OSPF ID: %s' % node.ospf_id) - print(' BGP LAS: %s' % node.bgp_las) - print(' HSRP Pri: %s' % node.hsrp_pri) - print(' HSRP VIP: %s' % node.hsrp_vip) - - if (node.vss.enabled): - print(' VSS Mode: %i' % node.vss.enabled) - print('VSS Domain: %s' % node.vss.domain) - print(' VSS Slot 0:') - print(' IOS: %s' % node.vss.members[0].ios) - print(' Serial: %s' % node.vss.members[0].serial) - print(' Platform: %s' % node.vss.members[0].plat) - print(' VSS Slot 1:') - print(' IOS: %s' % node.vss.members[1].ios) - print(' Serial: %s' % node.vss.members[1].serial) - print(' Platform: %s' % node.vss.members[1].plat) - - print(' Stack Cnt: %i' % node.stack.count) - - if ((node.stack.count > 0) & (self.config.graph.get_stack_members)): - print(' Stack members:') - for smem in node.stack.members: - print(' Switch Number: %s' % (smem.num)) - print(' Role: %s' % (smem.role)) - print(' Priority: %s' % (smem.pri)) - print(' MAC: %s' % (smem.mac)) - print(' Platform: %s' % (smem.plat)) - print(' Image: %s' % (smem.img)) - print(' Serial: %s' % (smem.serial)) - - print(' Loopbacks:') - if (self.config.graph.include_lo == False): - print(' Not configured.') - else: - for lo in node.loopbacks: - for lo_ip in lo.ips: - print(' %s - %s' % (lo.name, lo_ip)) - - print(' SVIs:') - if (self.config.graph.include_svi == False): - print(' Not configured.') - else: - for svi in node.svis: - for ip in svi.ip: - print(' SVI %s - %s' % (svi.vlan, ip)) - - print(' Links:') - for link in node.links: - lag = '' - if ((link.local_lag != None) | (link.remote_lag != None)): - lag = 'LAG[%s:%s]' % (link.local_lag or '', link.remote_lag or '') - print(' %s -> %s:%s %s' % (link.local_port, link.node.name, link.remote_port, lag)) - ret_links += 1 - - for link in node.links: - rn, rl = self._output_stdout(link.node) - ret_nodes += rn - ret_links += rl - - return (ret_nodes, ret_links) - - - def output_stdout(self): - self._reset_crawled() - - print('-----') - print('----- DEVICES') - print('-----') - num_nodes, num_links = self._output_stdout(self.root_node) - - print('Discovered devices: %i' % num_nodes) - print('Discovered links: %i' % num_links) - - - def _output_dot_get_node(self, graph, node): - dot_node = mnet_graph_dot_node() - dot_node.ntype = 'single' - dot_node.shape = 'ellipse' - dot_node.style = 'solid' - dot_node.peripheries = 1 - dot_node.label = '' - - dot_node.label = '%s' % node.name - - if (node.ip[0] != ''): - dot_node.label += '
%s' % node.ip[0] - - if ((node.stack.count == 0) | (self.config.graph.get_stack_members == 0)): - # show platform here or break it down by stack/vss later - dot_node.label += '
%s' % node.plat - - if ((self.config.graph.include_serials == 1) & (node.stack.count == 0) & (node.vss.enabled == 0)): - dot_node.label += '
%s' % node.serial - - dot_node.label += '
%s' % node.ios - - if (node.vss.enabled == 1): - if (self.config.graph.expand_vss == 1): - dot_node.ntype = 'vss' - else: - # group VSS into one graph node - dot_node.peripheries = 2 - s1 = '' - s2 = '' - if (self.config.graph.include_serials == 1): - s1 = ' - %s' % node.vss.members[0].serial - s2 = ' - %s' % node.vss.members[1].serial - - dot_node.label += '
VSS %s' % node.vss.domain - dot_node.label += '
VSS 0 - %s%s' % (node.vss.members[0].plat, s1) - dot_node.label += '
VSS 1 - %s%s' % (node.vss.members[1].plat, s2) - - if (node.stack.count > 0): - if (self.config.graph.expand_stackwise == 1): - dot_node.ntype = 'stackwise' - else: - # group Stackwise into one graph node - dot_node.peripheries = node.stack.count - - dot_node.label += '
Stackwise %i' % node.stack.count - - if (self.config.graph.get_stack_members): - for smem in node.stack.members: - serial = '' - if (self.config.graph.include_serials == 1): - serial = ' - %s' % smem.serial - dot_node.label += '
SW %s - %s%s (%s)' % (smem.num, smem.plat, serial, smem.role) - - if (node.router == 1): - dot_node.shape = 'diamond' - if (node.bgp_las != None): - dot_node.label += '
BGP %s' % node.bgp_las - if (node.ospf_id != None): - dot_node.label += '
OSPF %s' % node.ospf_id - if (node.hsrp_pri != None): - dot_node.label += '
HSRP VIP %s' \ - '
HSRP Pri %s' % (node.hsrp_vip, node.hsrp_pri) - - if (self.config.graph.include_lo == True): - for lo in node.loopbacks: - for lo_ip in lo.ips: - dot_node.label += '
%s - %s' % (lo.name, lo_ip) - - if (self.config.graph.include_svi == True): - for svi in node.svis: - for ip in svi.ip: - dot_node.label += '
VLAN %s - %s' % (svi.vlan, ip) - - return dot_node - - - def _output_dot(self, graph, node): - if (node == None): - return (0, 0) - if (node.crawled > 0): - return (0, 0) - node.crawled = 1 - - dot_node = self._output_dot_get_node(graph, node) - - if (dot_node.ntype == 'single'): - graph.add_node( - pydot.Node( - name = node.name, - label = '<%s>' % dot_node.label, - style = dot_node.style, - shape = dot_node.shape, - peripheries = dot_node.peripheries - ) - ) - elif (dot_node.ntype == 'vss'): - cluster = pydot.Cluster( - graph_name = node.name, - suppress_disconnected = False, - labelloc = 't', - labeljust = 'c', - fontsize = self.config.graph.node_text_size, - label = '<
VSS %s>' % node.vss.domain - ) - for i in range(0, 2): - serial = '' - if (self.config.graph.include_serials == 1): - serial = ' - %s' % node.vss.members[i].serial - - vss_label = 'VSS %i - %s%s' % (i, node.vss.members[i].plat, serial) - - cluster.add_node( - pydot.Node( - name = '%s[mnetVSS%i]' % (node.name, i+1), - label = '<%s
%s>' % (dot_node.label, vss_label), - style = dot_node.style, - shape = dot_node.shape, - peripheries = dot_node.peripheries - ) - ) - graph.add_subgraph(cluster) - elif (dot_node.ntype == 'stackwise'): - cluster = pydot.Cluster( - graph_name = node.name, - suppress_disconnected = False, - labelloc = 't', - labeljust = 'c', - fontsize = self.config.graph.node_text_size, - label = '<
Stackwise>' - ) - for i in range(0, node.stack.count): - serial = '' - if (self.config.graph.include_serials == 1): - serial = ' - %s' % node.stack.members[i].serial - - smem = node.stack.members[i] - sw_label = 'SW %i (%s)
%s%s' % (i, smem.role, smem.plat, serial) - - cluster.add_node( - pydot.Node( - name = '%s[mnetSW%i]' % (node.name, i+1), - label = '<%s
%s>' % (dot_node.label, sw_label), - style = dot_node.style, - shape = dot_node.shape, - peripheries = dot_node.peripheries - ) - ) - graph.add_subgraph(cluster) - - lags = [] - for link in node.links: - self._output_dot(graph, link.node) - - if ((self.config.graph.expand_lag == 1) | (link.local_lag == 'UNKNOWN')): - self._output_dot_link(graph, node, link, 0) - else: - found = 0 - for lag in lags: - if (link.local_lag == lag): - found = 1 - break - if (found == 0): - lags.append(link.local_lag) - self._output_dot_link(graph, node, link, 1) - - - def _output_dot_link(self, graph, node, link, draw_as_lag): - link_color = 'black' - link_style = 'solid' - - if (draw_as_lag): - link_label = 'LAG' - members = 0 - for l in node.links: - if (l.local_lag == link.local_lag): - members += 1 - link_label += '\n%i Members' % members - else: - link_label = 'P:%s\nC:%s' % (link.local_port, link.remote_port) - - is_lag = 1 if (link.local_lag != 'UNKNOWN') else 0 - - if (draw_as_lag == 0): - # LAG as member - if (is_lag): - local_lag_ip = '' - remote_lag_ip = '' - if (len(link.local_lag_ips)): - local_lag_ip = ' - %s' % link.local_lag_ips[0] - if (len(link.remote_lag_ips)): - remote_lag_ip = ' - %s' % link.remote_lag_ips[0] - - link_label += '\nLAG Member' - - if ((local_lag_ip == '') & (remote_lag_ip == '')): - link_label += '\nP:%s | C:%s' % (link.local_lag, link.remote_lag) - else: - link_label += '\nP:%s%s' % (link.local_lag, local_lag_ip) - link_label += '\nC:%s%s' % (link.remote_lag, remote_lag_ip) - - # IP Addresses - if ((link.local_if_ip != 'UNKNOWN') & (link.local_if_ip != None)): - link_label += '\nP:%s' % link.local_if_ip - if ((link.remote_if_ip != 'UNKNOWN') & (link.remote_if_ip != None)): - link_label += '\nC:%s' % link.remote_if_ip - else: - # LAG as grouping - for l in node.links: - if (l.local_lag == link.local_lag): - link_label += '\nP:%s | C:%s' % (l.local_port, l.remote_port) - - local_lag_ip = '' - remote_lag_ip = '' - - if (len(link.local_lag_ips)): - local_lag_ip = ' - %s' % link.local_lag_ips[0] - if (len(link.remote_lag_ips)): - remote_lag_ip = ' - %s' % link.remote_lag_ips[0] - - if ((local_lag_ip == '') & (remote_lag_ip == '')): - link_label += '\nP:%s | C:%s' % (link.local_lag, link.remote_lag) - else: - link_label += '\nP:%s%s' % (link.local_lag, local_lag_ip) - link_label += '\nC:%s%s' % (link.remote_lag, remote_lag_ip) - - - if (link.link_type == '1'): - # Trunk = Bold/Blue - link_color = 'blue' - link_style = 'bold' - - if ((link.local_native_vlan == link.remote_native_vlan) | (link.remote_native_vlan == None)): - link_label += '\nNative %s' % link.local_native_vlan - else: - link_label += '\nNative P:%s C:%s' % (link.local_native_vlan, link.remote_native_vlan) - - if (link.local_allowed_vlans == link.remote_allowed_vlans): - link_label += '\nAllowed %s' % link.local_allowed_vlans - else: - link_label += '\nAllowed P:%s' % link.local_allowed_vlans - if (link.remote_allowed_vlans != None): - link_label += '\nAllowed C:%s' % link.remote_allowed_vlans - elif (link.link_type is None): - # Routed = Bold/Red - link_color = 'red' - link_style = 'bold' - else: - # Switched access, include VLAN ID in label - if (link.vlan != None): - link_label += '\nVLAN %s' % link.vlan - - edge_src = node.name - edge_dst = link.node.name - lmod = get_module_from_interf(link.local_port) - rmod = get_module_from_interf(link.remote_port) - - if (self.config.graph.expand_vss == 1): - if (node.vss.enabled == 1): - edge_src = '%s[mnetVSS%s]' % (node.name, lmod) - if (link.node.vss.enabled == 1): - edge_dst = '%s[mnetVSS%s]' % (link.node.name, rmod) - - if (self.config.graph.expand_stackwise == 1): - if (node.stack.count > 0): - edge_src = '%s[mnetSW%s]' % (node.name, lmod) - if (link.node.stack.count > 0): - edge_dst = '%s[mnetSW%s]' % (link.node.name, rmod) - - edge = pydot.Edge( - edge_src, edge_dst, - dir = 'forward', - label = link_label, - color = link_color, - style = link_style - ) - - graph.add_edge(edge) - - - - def output_dot(self, dot_file, title): - self._reset_crawled() - - title_text_size = self.config.graph.title_text_size - credits = '' \ - '' \ - '' \ - '' \ - '
' \ - '$title$
' \ - '$date$
' \ - '' \ - 'Generated by MNet Suite $ver$
' \ - 'Written by Michael Laforest

' \ - '
' % (title_text_size, title_text_size-2) - - today = datetime.datetime.now() - today = today.strftime('%Y-%m-%d %H:%M') - credits = credits.replace('$ver$', __version__) - credits = credits.replace('$date$', today) - credits = credits.replace('$title$', title) - - node_text_size = self.config.graph.node_text_size - link_text_size = self.config.graph.link_text_size - - graph = pydot.Dot( - graph_type = 'graph', - labelloc = 'b', - labeljust = 'r', - fontsize = node_text_size, - label = '<%s>' % credits - ) - graph.set_node_defaults( - fontsize = link_text_size - ) - graph.set_edge_defaults( - fontsize = link_text_size, - labeljust = 'l' - ) - - # add all of the nodes and links - self._output_dot(graph, self.root_node) - - # get file extension - file_name, file_ext = os.path.splitext(dot_file) - - output_func = getattr(graph, 'write_' + file_ext.lstrip('.')) - if (output_func == None): - print('Error: Output type "%s" does not exist.' % file_ext) - else: - output_func(dot_file) - print('Created graph: %s' % dot_file) - - - def output_catalog(self, filename): - try: - f = open(filename, 'w') - except: - print('Unable to open catalog file "%s"' % filename) - return - - for n in self.nodes: - # get info that we may not have yet - n.opts.get_serial = True - n.opts.get_plat = True - n.opts.get_bootf = True - n.query_node() - - if (n.stack.count > 0): - # stackwise - for smem in n.stack.members: - serial = smem.serial or 'NOT CONFIGURED TO POLL' - plat = smem.plat or 'NOT CONFIGURED TO POLL' - f.write('"%s","%s","%s","%s","%s","STACK","%s"\n' % (n.name, n.ip[0], plat, n.ios, serial, n.bootfile)) - elif (n.vss.enabled != 0): - #vss - for i in range(0, 2): - serial = n.vss.members[i].serial - plat = n.vss.members[i].plat - ios = n.vss.members[i].ios - f.write('"%s","%s","%s","%s","%s","VSS","%s"\n' % (n.name, n.ip[0], plat, ios, serial, n.bootfile)) - else: - # stand alone - f.write('"%s","%s","%s","%s","%s","","%s"\n' % (n.name, n.ip[0], n.plat, n.ios, n.serial, n.bootfile)) - - f.close() - diff --git a/mnetsuite/network.py b/mnetsuite/network.py new file mode 100644 index 0000000..1176368 --- /dev/null +++ b/mnetsuite/network.py @@ -0,0 +1,510 @@ +#!/usr/bin/python + +''' + MNet Suite + network.py + + Michael Laforest + mjlaforest@gmail.com + + Copyright (C) 2015-2018 Michael Laforest + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +from timeit import default_timer as timer +from .config import mnet_config +from .util import * +from .node import * + + +class mnet_network: + # def set_max_depth(self, depth) + # def discover(self, ip) + # def output_stdout(self) + # def output_dot(self, dot_file, title) + # def output_catalog(self, filename) + DCODE_ROOT = 0x01 + DCODE_ERR_SNMP = 0x02 + DCODE_DISCOVERED = 0x04 + DCODE_STEP_INTO = 0x08 + DCODE_CDP = 0x10 + DCODE_LLDP = 0x20 + DCODE_INCLUDE = 0x40 + DCODE_LEAF = 0x80 + + DCODE_ROOT_STR = '[root]' + DCODE_ERR_SNMP_STR = '!' + DCODE_DISCOVERED_STR = '+' + DCODE_STEP_INTO_STR = '>' + DCODE_CDP_STR = '[ cdp]' + DCODE_LLDP_STR = '[lldp]' + DCODE_INCLUDE_STR = 'i' + DCODE_LEAF_STR = 'L' + + def __init__(self, conf): + self.root_node = None + self.nodes = [] + self.max_depth = 0 + self.config = conf + + def __str__(self): + return ('' % (self.root_node.name, len(self.nodes))) + def __repr__(self): + return self.__str__() + + + def set_max_depth(self, depth): + self.max_depth = depth + + + def reset_discovered(self): + for n in self.nodes: + n.discovered = 0 + + + def discover(self, ip): + ''' + Discover the network starting at the defined root node IP. + Recursively enumerate the network tree up to self.depth. + Populates self.nodes[] as a list of discovered nodes in the + network with self.root_node being the root. + + This function will discover the network with minimal information. + It is enough to define the structure of the network but will not + include much data on each node. Call discover_details() after this + to update the self.nodes[] array with more info. + ''' + + print('Discovery codes:\n' \ + ' . depth %s connection error\n' \ + ' %s discovering node %s numerating adjacencies\n' \ + ' %s include node %s leaf node\n' % + (mnet_network.DCODE_ERR_SNMP_STR, + mnet_network.DCODE_DISCOVERED_STR, mnet_network.DCODE_STEP_INTO_STR, + mnet_network.DCODE_INCLUDE_STR, mnet_network.DCODE_LEAF_STR) + ) + + print('Discovering network...') + + # Start the process of querying this node and recursing adjacencies. + node, new_node = self.__query_node(ip, 'UNKNOWN') + self.root_node = node + + if (node != None): + self.__print_step(node.ip[0], node.name, 0, mnet_network.DCODE_ROOT|mnet_network.DCODE_DISCOVERED) + self.__discover_node(node, 0) + + # we may have missed chassis info + for n in self.nodes: + if ((n.serial == None) | (n.plat == None) | (n.ios == None)): + n.opts.get_chassis_info = True + if (n.serial == None): + n.opts.get_serial = True + if (n.ios == None): + n.opts.get_ios = True + if (n.plat == None): + n.opts.get_plat = True + n.query_node() + + + def discover_details(self): + ''' + Enumerate the discovered nodes from discover() and update the + nodes in the array with additional info. + ''' + if (self.root_node == None): + return + + print('\nCollecting node details...') + + ni = 0 + for n in self.nodes: + ni = ni + 1 + + indicator = '+' + if (n.snmpobj.success == 0): + indicator = '!' + + sys.stdout.write('[%i/%i]%s %s (%s)' % (ni, len(self.nodes), indicator, n.name, n.snmpobj._ip)) + sys.stdout.flush() + + # set what details to discover for this node + n.opts.get_router = True + n.opts.get_ospf_id = True + n.opts.get_bgp_las = True + n.opts.get_hsrp_pri = True + n.opts.get_hsrp_vip = True + n.opts.get_serial = True + n.opts.get_stack = True + n.opts.get_stack_details = self.config.diagram.get_stack_members + n.opts.get_vss = True + n.opts.get_vss_details = self.config.diagram.get_vss_members + n.opts.get_svi = True + n.opts.get_lo = True + n.opts.get_vpc = True + n.opts.get_ios = True + n.opts.get_plat = True + + start = timer() + n.query_node() + end = timer() + print(' %.2f sec' % (end - start)) + + # There is some back fill information we can populate now that + # we know all there is to know. + print('\nBack filling node details...') + + for n in self.nodes: + # Find and link VPC nodes together for easy reference later + if ((n.vpc_domain != None) & (n.vpc_peerlink_node == None)): + for link in n.links: + if ((link.local_port == n.vpc_peerlink_if) | (link.local_lag == n.vpc_peerlink_if)): + n.vpc_peerlink_node = link.node + link.node.vpc_peerlink_node = n + break + + + def __print_step(self, ip, name, depth, dcodes): + if (dcodes & mnet_network.DCODE_DISCOVERED): + sys.stdout.write('%-3i' % len(self.nodes)) + else: + sys.stdout.write(' ') + + if (dcodes & mnet_network.DCODE_INCLUDE): + # flip this off cause we didn't even try + dcodes = dcodes & ~mnet_network.DCODE_ERR_SNMP + + if (dcodes & mnet_network.DCODE_ROOT): sys.stdout.write( mnet_network.DCODE_ROOT_STR ) + elif (dcodes & mnet_network.DCODE_CDP): sys.stdout.write( mnet_network.DCODE_CDP_STR ) + elif (dcodes & mnet_network.DCODE_LLDP): sys.stdout.write( mnet_network.DCODE_LLDP_STR ) + else: sys.stdout.write(' ') + + status = '' + if (dcodes & mnet_network.DCODE_ERR_SNMP): status += mnet_network.DCODE_ERR_SNMP_STR + if (dcodes & mnet_network.DCODE_LEAF): status += mnet_network.DCODE_LEAF_STR + elif (dcodes & mnet_network.DCODE_INCLUDE): status += mnet_network.DCODE_INCLUDE_STR + if (dcodes & mnet_network.DCODE_DISCOVERED): status += mnet_network.DCODE_DISCOVERED_STR + elif (dcodes & mnet_network.DCODE_STEP_INTO): status += mnet_network.DCODE_STEP_INTO_STR + sys.stdout.write('%3s' % status) + + for i in range(0, depth): + sys.stdout.write('.') + + name = util.shorten_host_name(name, self.config.host_domains) + print('%s (%s)' % (name, ip)) + + + def __query_node(self, ip, host): + ''' + Query this node for info about itself. + + Args: + ip: IP Address of the node. + host: Hostname of this known (if known from CDP/LLDP) + + Returns: + mnet_node: Node of this object + int: Newly discovered node=1, already discovered=0 + ''' + + host = util.shorten_host_name(host, self.config.host_domains) + node_new = 1 + node, node_updated = self.__get_known_node(ip, host) + + if (node == None): + # new node + node = mnet_node() + node.name = host + node.ip = [ip] + else: + # existing node + node_new = 0 + if (node.snmpobj.success == 1): + # we already queried this node successfully - return it + return (node, node_new) + + if (ip == 'UNKNOWN'): + if (node_new): + self.nodes.append(node) + return (node, node_new|node_updated) + + node.name = host + + # vmware ESX reports the IP as 0.0.0.0 + # LLDP can return an empty string for IPs. + if ((ip == '0.0.0.0') | (ip == '')): + if (node_new): + self.nodes.append(node) + return (node, node_new|node_updated) + + # find valid credentials for this node + if (node.try_snmp_creds(self.config.snmp_creds) == 0): + if (node_new): + self.nodes.append(node) + return (node, node_new) + + node.name = node.get_system_name(self.config.host_domains) + if (node.name != host): + # the hostname changed (cdp/lldp vs snmp)! + # double check we don't already know about this node + if (node_new): + node2, node_updated2 = self.__get_known_node(ip, host) + if ((node2 != None) & (node_updated2 == 0)): + return (node, 0) + node_updated = node_updated2 + + # Finally, if we still don't have a name, use the IP. + # e.g. Maybe CDP/LLDP was empty and we dont have good credentials + # for this device. A blank name can break Dot. + if ((node.name == None) | (node.name == '')): + node.name = node.get_ipaddr() + + # if this is a new non-updated node, save it to the list + if ((node_new == 1) & (node_updated == 0)): + self.nodes.append(node) + + node.query_node() + return (node, 1) + + + def __get_known_node(self, ip, host): + ''' + Look for known nodes by IP and HOST. + If found by HOST, add the IP if not already known. + + Return: + node: Node if found + updated: 1=updated, 0=not updated + ''' + # already known by IP ? + for ex in self.nodes: + for exip in ex.ip: + if (exip == '0.0.0.0'): + continue + if (exip == ip): + return (ex, 0) + + # already known by HOST ? + node = self.__get_known_node_by_host(host) + if (node != None): + # node already known + if (ip not in node.ip): + node.ip.append(ip) + return (node, 1) + return (node, 0) + + return (None, 0) + + + def __discover_node(self, node, depth): + ''' + Given a node, recursively enumerate its adjacencies + until we reach the specified depth (>0). + + Args: + node: mnet_node object to enumerate. + depth: The depth left that we can go further away from the root. + ''' + if (node == None): + return + + if (depth >= self.max_depth): + return + + if (node.discovered > 0): + return + node.discovered = 1 + + # vmware ESX can report IP as 0.0.0.0 + # If we are allowing 0.0.0.0/32 in the config, + # then we added it as a leaf, but don't discover it + if (node.ip[0] == '0.0.0.0'): + return + + # may be a leaf we couldn't connect to previously + if (node.snmpobj.success == 0): + return + + # print some info to stdout + dcodes = mnet_network.DCODE_STEP_INTO + if (depth == 0): + dcodes |= mnet_network.DCODE_ROOT + self.__print_step(node.ip[0], node.name, depth, dcodes) + + # get the cached snmp credentials + snmpobj = node.snmpobj + + # list of valid neighbors to discover next + valid_neighbors = [] + + # get list of CDP neighbors + cdp_neighbors = node.get_cdp_neighbors() + + # get list of LLDP neighbors + lldp_neighbors = node.get_lldp_neighbors() + + if ((cdp_neighbors == None) & (lldp_neighbors == None)): + return + + neighbors = cdp_neighbors + lldp_neighbors + + for n in neighbors: + # some neighbors may not advertise IP addresses - default them to 0.0.0.0 + if (n.remote_ip == None): + n.remote_ip = '0.0.0.0' + + # check the ACL + acl_action = self.__match_node_acl(n.remote_ip, n.remote_name) + if (acl_action == 'deny'): + # deny inclusion of this node + continue + + # the code to display to stdout about this discovery + dcodes = mnet_network.DCODE_DISCOVERED + + child = None + new_node = 1 + if (acl_action == 'include'): + # include this node but do not discover it + child = mnet_node() + child.ip = [n.remote_ip] + dcodes |= mnet_network.DCODE_INCLUDE + else: + # discover this node + child, new_node = self.__query_node(n.remote_ip, n.remote_name) + + # if we couldn't pull info from SNMP fill in what we know + if (child.snmpobj.success == 0): + child.name = util.shorten_host_name(n.remote_name, self.config.host_domains) + dcodes |= mnet_network.DCODE_ERR_SNMP + + if (new_node == 1): + # report this new node to stdout. + # this could be a repeat either through + # cylical diagrams or redundant links. + if (acl_action == 'leaf'): dcodes |= mnet_network.DCODE_LEAF + if (n.discovered_proto == 'cdp'): dcodes |= mnet_network.DCODE_CDP + if (n.discovered_proto == 'lldp'): dcodes |= mnet_network.DCODE_LLDP + self.__print_step(n.remote_ip, n.remote_name, depth+1, dcodes) + + # CDP/LLDP advertises the platform + child.plat = n.remote_platform + child.ios = n.remote_ios + + # add the discovered node to the link object and link to the parent + n.node = child + self.__add_link(node, n) + + # if we need to discover this node then add it to the list + if ((new_node == 1) & (acl_action != 'leaf') & (acl_action != 'include')): + valid_neighbors.append(child) + + # discover the valid neighbors + for n in valid_neighbors: + self.__discover_node(n, depth+1) + + + def __match_node_acl(self, ip, host): + for acl in self.config.discover_acl: + if (acl.type == 'ip'): + # ___ ip ipaddr + if (self.__match_ip(ip, acl.str)): + return acl.action + elif (acl.type == 'host'): + # ___ host hoststr + if (self.__match_host(host, acl.str)): + return acl.action + return 'deny' + + + def __match_ip(self, ip, cidr): + if (cidr == 'any'): + return 1 + + validate = re.match('^([0-2]?[0-9]?[0-9]\.){3}[0-2]?[0-9]?[0-9]$', ip) + if (validate == None): + return 0 + + if (USE_NETADDR): + if (ip in IPNetwork(cidr)): + return 1 + else: + if (util.is_ipv4_in_cidr(ip, cidr)): + return 1 + return 0 + + + def __match_host(self, host, pattern): + if (host == '*'): + return 1 + if (re.search(pattern, host)): + return 1 + return 0 + + # + # Add or update a link. + # Return + # 0 - Found an existing link and updated it + # 1 - Added as a new link + # + def __add_link(self, node, link): + if (link.node.discovered == 1): + # both nodes have been discovered, + # so try to update existing reverse link info + # instead of adding a new link + for n in self.nodes: + # find the child, which was the original parent + if (n.name == link.node.name): + # find the existing link + for ex_link in n.links: + if ((ex_link.node.name == node.name) & (ex_link.local_port == link.remote_port)): + if ((link.local_if_ip != 'UNKNOWN') & (ex_link.remote_if_ip == None)): + ex_link.remote_if_ip = link.local_if_ip + + if ((link.local_lag != 'UNKNOWN') & (ex_link.remote_lag == None)): + ex_link.remote_lag = link.local_lag + + if ((len(link.local_lag_ips) == 0) & len(ex_link.remote_lag_ips)): + ex_link.remote_lag_ips = link.local_lag_ips + + if ((link.local_native_vlan != None) & (ex_link.remote_native_vlan == None)): + ex_link.remote_native_vlan = link.local_native_vlan + + if ((link.local_allowed_vlans != None) & (ex_link.remote_allowed_vlans == None)): + ex_link.remote_allowed_vlans = link.local_allowed_vlans + + return 0 + else: + for ex_link in node.links: + if ((ex_link.node.name == link.node.name) & (ex_link.local_port == link.local_port)): + # haven't discovered yet but somehow we have this link twice. + # maybe from different discovery processes? + return 0 + + node.add_link(link) + return 1 + + + def __get_known_node_by_host(self, hostname): + ''' + Determine if the node is already known by hostname. + If it is, return it. + ''' + for n in self.nodes: + if (n.name == hostname): + return n + return None + diff --git a/mnetsuite/node.py b/mnetsuite/node.py index fb35ed4..773ab1a 100755 --- a/mnetsuite/node.py +++ b/mnetsuite/node.py @@ -1,848 +1,672 @@ #!/usr/bin/python ''' - MNet Suite - node.py + MNet Suite + node.py - Michael Laforest - mjlaforest@gmail.com + Michael Laforest + mjlaforest@gmail.com - Copyright (C) 2015 Michael Laforest + Copyright (C) 2015-2018 Michael Laforest - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ''' -from snmp import * -from util import * import sys +from .snmp import * +from .util import * +from .node_stack import mnet_node_stack, mnet_node_stack_member +from .node_vss import mnet_node_vss, mnet_node_vss_member + class mnet_node_link: - ''' - Generic link to another node. - CDP and LLDP neighbors are discovered - and returned as mnet_node_link objects. - ''' - # the linked node - node = None - - # description of the link - link_type = None - remote_ip = None - remote_name = None - vlan = None - local_native_vlan = None - local_allowed_vlans = None - remote_native_vlan = None - remote_allowed_vlans = None - local_port = None - remote_port = None - local_lag = None - remote_lag = None - local_lag_ips = [] - remote_lag_ips = [] - local_if_ip = None - remote_if_ip = None - remote_platform = None - remote_ios = None - remote_mac = None - discovered_proto = None - - def __init__( - self, - node = None, - link_type = None, - remote_ip = None, - remote_name = None, - vlan = None, - local_allowed_vlans = None, - local_native_vlan = None, - remote_allowed_vlans = None, - remote_native_vlan = None, - local_port = None, - remote_port = None, - local_lag = None, - remote_lag = None, - local_lag_ips = [], - remote_lag_ips = [], - local_if_ip = None, - remote_if_ip = None, - remote_platform = None, - remote_ios = None, - remote_mac = None, - discovered_proto = None - ): - self.node = node - self.link_type = link_type - self.remote_ip = remote_ip - self.remote_name = remote_name - self.vlan = vlan - self.local_native_vlan = local_native_vlan - self.local_allowed_vlans = local_allowed_vlans - self.remote_native_vlan = remote_native_vlan - self.remote_allowed_vlans = remote_allowed_vlans - self.local_port = local_port - self.remote_port = remote_port - self.local_lag = local_lag - self.remote_lag = remote_lag - self.local_lag_ips = local_lag_ips - self.remote_lag_ips = remote_lag_ips - self.local_if_ip = local_if_ip - self.remote_if_ip = remote_if_ip - self.remote_platform = remote_platform - self.remote_ios = remote_ios - self.remote_mac = remote_mac - self.discovered_proto = discovered_proto + ''' + Generic link to another node. + CDP and LLDP neighbors are discovered + and returned as mnet_node_link objects. + ''' + + def __init__(self): + # the linked node + self.node = None + + # details about the link + self.link_type = None + self.remote_ip = None + self.remote_name = None + self.vlan = None + self.local_native_vlan = None + self.local_allowed_vlans = None + self.remote_native_vlan = None + self.remote_allowed_vlans = None + self.local_port = None + self.remote_port = None + self.local_lag = None + self.remote_lag = None + self.local_lag_ips = None + self.remote_lag_ips = None + self.local_if_ip = None + self.remote_if_ip = None + self.remote_platform = None + self.remote_ios = None + self.remote_mac = None + self.discovered_proto = None + + def __str__(self): + return ('' % (self.local_port, self.node.name, self.remote_port)) + def __repr__(self): + return self.__str__() class mnet_node_svi: - vlan = None - ip = None - - def __init__(self, vlan): - self.vlan = vlan - self.ip = [] + def __init__(self, vlan): + self.vlan = vlan + self.ip = [] + def __str__(self): + return ('' % (self.vlan, self.ip)) + def __repr__(self): + return self.__str__() class mnet_node_lo: - name = None - ips = [] - - def __init__(self, name, ips): - self.name = name.replace('Loopback', 'lo') - self.ips = ips - - -class mnet_node_stack_member: - num = 0 - role = 0 - pri = 0 - mac = None - img = None - serial = None - plat = None - - def __init__(self): - self.num = 0 - self.role = 0 - self.pri = 0 - self.mac = None - self.img = None - self.serial = None - self.plat = None - - -class mnet_node_stack: - members = [] - count = 0 - - def __init__(self, snmpobj = None, get_details = 0): - self.members = [] - self.count = 0 - - if (snmpobj != None): - self.get_members(snmpobj, get_details) - - def get_members(self, snmpobj, get_details): - vbtbl = snmpobj.get_bulk(OID_STACK) - if (vbtbl == None): - return None - - if (get_details == 0): - self.count = 0 - for row in vbtbl: - for n, v in row: - n = str(n) - if (n.startswith(OID_STACK_NUM + '.')): - self.count += 1 - - if (self.count == 1): - self.count = 0 - return - - serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) - platf_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) - - for row in vbtbl: - for n, v in row: - n = str(n) - if (n.startswith(OID_STACK_NUM + '.')): - m = mnet_node_stack_member() - - t = n.split('.') - idx = t[14] - - m.num = v - m.role = snmpobj.cache_lookup(vbtbl, OID_STACK_ROLE + '.' + idx) - m.pri = snmpobj.cache_lookup(vbtbl, OID_STACK_PRI + '.' + idx) - m.mac = snmpobj.cache_lookup(vbtbl, OID_STACK_MAC + '.' + idx) - m.img = snmpobj.cache_lookup(vbtbl, OID_STACK_IMG + '.' + idx) - - m.serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + idx) - m.plat = snmpobj.cache_lookup(platf_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + idx) - - if (m.role == '1'): - m.role = 'master' - elif (m.role == '2'): - m.role = 'member' - elif (m.role == '3'): - m.role = 'notMember' - elif (m.role == '4'): - m.role = 'standby' - - mac_seg = [m.mac[x:x+4] for x in xrange(2, len(m.mac), 4)] - m.mac = '.'.join(mac_seg) - - self.members.append(m) - - self.count = len(self.members) - if (self.count == 1): - self.count = 0 - - return - - -class mnet_node_vss_member: - ios = None - serial = None - plat = None - - def __init__(self): - self.ios = None - self.serial = None - self.plat = None - - -class mnet_node_vss: - members = [] - enabled = 0 - domain = None - - def __init__(self, snmpobj = None, get_details = 0): - self.members = [ mnet_node_vss_member(), mnet_node_vss_member() ] - enabled = 0 - domain = None - - if (snmpobj != None): - self.get_members(snmpobj, get_details) - - def get_members(self, snmpobj, get_details): - self.enabled = 1 if (snmpobj.get_val(OID_VSS_MODE) == '2') else 0 - if (self.enabled == 0): - return - - self.domain = snmpobj.get_val(OID_VSS_DOMAIN) - - if (get_details == 0): - return - - class_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_CLASS) - ios_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SOFTWARE) - serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) - plat_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) - - module = 0 - - for row in class_vbtbl: - for n, v in row: - if (v == 9): - n = str(n) - t = n.split('.') - modidx = t[12] - if (module > 1): - print('[E] More than 2 modules found for VSS device! Skipping after the second...') - return - - self.members[module].ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + modidx) - self.members[module].serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + modidx) - self.members[module].plat = snmpobj.cache_lookup(plat_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + modidx) - module += 1 + def __init__(self, name, ips): + self.name = name.replace('Loopback', 'lo') + self.ips = ips + def __str__(self): + return ('' % (self.name, self.ips)) + def __repr__(self): + return self.__str__() class mnet_node: - class _node_opts: - get_name = False - get_ip = False - get_plat = False - get_ios = False - get_router = False - get_ospf_id = False - get_bgp_las = False - get_hsrp_pri = False - get_hsrp_vip = False - get_serial = False - get_stack = False - get_stack_details = False - get_vss = False - get_vss_details = False - get_svi = False - get_lo = False - get_bootf = False - get_chassis_info = False - - def __init__(self): - self.reset() - - def reset(self): - self.get_name = False - self.get_ip = False - self.get_plat = False - self.get_ios = False - self.get_router = False - self.get_ospf_id = False - self.get_bgp_las = False - self.get_hsrp_pri = False - self.get_hsrp_vip = False - self.get_serial = False - self.get_stack = False - self.get_stack_details = False - self.get_vss = False - self.get_vss_details = False - self.get_svi = False - self.get_lo = False - self.get_bootf = False - self.get_chassis_info = False - - - opts = None - snmpobj = mnet_snmp() - crawled = 0 - links = [] - - name = None - ip = [] - plat = None - ios = None - router = None - ospf_id = None - bgp_las = None - hsrp_pri = None - hsrp_vip = None - serial = None - bootfile = None - - svis = [] - loopbacks = [] - stack = None - vss = None - - # cached MIB trees - cdp_vbtbl = None - lldp_vbtbl = None - link_type_vbtbl = None - lag_vbtbl = None - vlan_vbtbl = None - ifname_vbtbl = None - ifip_vbtbl = None - svi_vbtbl = None - ethif_vbtbl = None - trk_allowed_vbtbl = None - trk_native_vbtbl = None - - def __init__(self): - self.opts = mnet_node._node_opts() - self.snmpobj = mnet_snmp() - - self.links = [] - self.crawled = 0 - - self.name = None - self.ip = None - self.plat = None - self.ios = None - self.router = None - self.ospf_id = None - self.bgp_las = None - self.hsrp_pri = None - self.hsrp_vip = None - self.serial = None - self.bootfile = None - - self.svis = [] - self.loopbacks = [] - - self.stack = mnet_node_stack() - self.vss = mnet_node_vss() - - self.cdp_vbtbl = None - self.ldp_vbtbl = None - self.link_type_vbtbl = None - self.lag_vbtbl = None - self.vlan_vbtbl = None - self.ifname_vbtbl = None - self.ifip_vbtbl = None - self.svi_vbtbl = None - self.ethif_vbtbl = None - self.trk_allowed_vbtbl = None - self.trk_native_vbtbl = None - - - def add_link(self, link): - self.links.append(link) - - - # find valid credentials for this node - def try_snmp_creds(self, snmp_creds): - if (self.snmpobj.success == 0): - self.snmpobj._ip = self.ip[0] - if (self.snmpobj.get_cred(snmp_creds) == 0): - return 0 - return 1 - - - # Query this node. - # Set .opts and .snmp_creds before calling. - def query_node(self): - if (self.snmpobj.ver == 0): - # call try_snmp_creds() first or it failed to find good creds - return 0 - - snmpobj = self.snmpobj - - # router - if (self.opts.get_router == True): - if (self.router == None): - self.router = 1 if (snmpobj.get_val(OID_IP_ROUTING) == '1') else 0 - - if (self.router == 1): - # OSPF - if (self.opts.get_ospf_id == True): - self.ospf_id = snmpobj.get_val(OID_OSPF) - if (self.ospf_id != None): - self.ospf_id = snmpobj.get_val(OID_OSPF_ID) - - # BGP - if (self.opts.get_bgp_las == True): - self.bgp_las = snmpobj.get_val(OID_BGP_LAS) - if (self.bgp_las == '0'): # 4500x is reporting 0 with disabled - self.bgp_las = None - - # HSRP - if (self.opts.get_hsrp_pri == True): - self.hsrp_pri = snmpobj.get_val(OID_HSRP_PRI) - if (self.hsrp_pri != None): - self.hsrp_vip = snmpobj.get_val(OID_HSRP_VIP) - - # stack - if (self.opts.get_stack): - self.stack = mnet_node_stack(snmpobj, self.opts.get_stack_details) - - # vss - if (self.opts.get_vss): - self.vss = mnet_node_vss(snmpobj, self.opts.get_vss_details) - - # serial - if ((self.opts.get_serial == 1) & (self.stack.count == 0) & (self.vss.enabled == 0)): - self.serial = snmpobj.get_val(OID_SYS_SERIAL) - - # SVI - if (self.opts.get_svi == True): - if (self.svi_vbtbl == None): - self.svi_vbtbl = snmpobj.get_bulk(OID_SVI_VLANIF) - - if (self.ifip_vbtbl == None): - self.ifip_vbtbl = snmpobj.get_bulk(OID_IF_IP) - - for row in self.svi_vbtbl: - for n, v in row: - n = str(n) - vlan = n.split('.')[14] - svi = mnet_node_svi(vlan) - svi_ips = self._get_cidrs_from_ifidx(v) - svi.ip.extend(svi_ips) - self.svis.append(svi) - - # loopback - if (self.opts.get_lo == True): - self.ethif_vbtbl = snmpobj.get_bulk(OID_ETH_IF) - - if (self.ifip_vbtbl == None): - self.ifip_vbtbl = snmpobj.get_bulk(OID_IF_IP) - - for row in self.ethif_vbtbl: - for n, v in row: - n = str(n) - if (n.startswith(OID_ETH_IF_TYPE) & (v == 24)): - ifidx = n.split('.')[10] - lo_name = snmpobj.cache_lookup(self.ethif_vbtbl, OID_ETH_IF_DESC + '.' + ifidx) - lo_ips = self._get_cidrs_from_ifidx(ifidx) - lo = mnet_node_lo(lo_name, lo_ips) - self.loopbacks.append(lo) - - # bootfile - if (self.opts.get_bootf): - self.bootfile = snmpobj.get_val(OID_SYS_BOOT) - - # chassis info (serial, IOS, platform) - if (self.opts.get_chassis_info): - self._get_chassis_info() - - # reset the get options - self.opts.reset() - return 1 - - - def _get_cidrs_from_ifidx(self, ifidx): - ips = [] - - for ifrow in self.ifip_vbtbl: - for ifn, ifv in ifrow: - ifn = str(ifn) - if (ifn.startswith(OID_IF_IP_ADDR)): - if (str(ifv) == str(ifidx)): - t = ifn.split('.') - ip = ".".join(t[10:]) - mask = self.snmpobj.cache_lookup(self.ifip_vbtbl, OID_IF_IP_NETM + ip) - nbits = get_net_bits_from_mask(mask) - cidr = '%s/%i' % (ip, nbits) - ips.append(cidr) - return ips - - - def _cache_common_mibs(self): - if (self.link_type_vbtbl == None): - self.link_type_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_VTP) - - if (self.lag_vbtbl == None): - self.lag_vbtbl = self.snmpobj.get_bulk(OID_LAG_LACP) - - if (self.vlan_vbtbl == None): - self.vlan_vbtbl = self.snmpobj.get_bulk(OID_IF_VLAN) - - if (self.ifname_vbtbl == None): - self.ifname_vbtbl = self.snmpobj.get_bulk(OID_IFNAME) - - if (self.trk_allowed_vbtbl == None): - self.trk_allowed_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_ALLOW) - - if (self.trk_native_vbtbl == None): - self.trk_native_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_NATIVE) - - if (self.ifip_vbtbl == None): - self.ifip_vbtbl = self.snmpobj.get_bulk(OID_IF_IP) - - - # - # Get a list of CDP neighbors. - # Returns a list of mnet_node_link's - # - def get_cdp_neighbors(self): - neighbors = [] - snmpobj = self.snmpobj - - # get list of CDP neighbors - self.cdp_vbtbl = snmpobj.get_bulk(OID_CDP) - if (self.cdp_vbtbl == None): - print 'No CDP Neighbors Found.' - return None - - # cache some common MIB trees - self._cache_common_mibs() - - for row in self.cdp_vbtbl: - for name, val in row: - name = str(name) - # process only if this row is a CDP_DEVID - if (name.startswith(OID_CDP_DEVID) == 0): - continue - - t = name.split('.') - ifidx = t[14] - ifidx2 = t[15] - - # get remote IP - rip = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_IPADDR + '.' + ifidx + '.' + ifidx2) - rip = convert_ip_int_str(rip) - - # get local port - lport = self._get_ifname(ifidx) - - # get remote port - rport = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_DEVPORT + '.' + ifidx + '.' + ifidx2) - rport = shorten_port_name(rport) - - # get remote platform - rplat = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_DEVPLAT + '.' + ifidx + '.' + ifidx2) - - # get IOS version - rios = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_IOS + '.' + ifidx + '.' + ifidx2) - if (rios != None): - try: - rios = binascii.unhexlify(rios[2:]) - except: - pass - rios = self._format_ios_ver(rios) - - link = self._get_node_link_info(ifidx, ifidx2) - link.remote_name = val.prettyPrint() - link.remote_ip = rip - link.discovered_proto = 'cdp' - link.local_port = lport - link.remote_port = rport - link.remote_plat = rplat - link.remote_ios = rios - - neighbors.append(link) - - return neighbors - - - # - # Get a list of LLDP neighbors. - # Returns a list of mnet_node_link's - # - def get_lldp_neighbors(self): - neighbors = [] - snmpobj = self.snmpobj - - self.lldp_vbtbl = snmpobj.get_bulk(OID_LLDP) - if (self.lldp_vbtbl == None): - print 'No LLDP Neighbors Found.' - return None - - self._cache_common_mibs() - - for row in self.lldp_vbtbl: - for name, val in row: - name = str(name) - if (name.startswith(OID_LLDP_TYPE) == 0): - continue - - t = name.split('.') - ifidx = t[12] - ifidx2 = t[13] - - rip = '' - for r in self.lldp_vbtbl: - for n, v in r: - n = str(n) - if (n.startswith(OID_LLDP_DEVADDR + '.' + ifidx + '.' + ifidx2)): - t2 = n.split('.') - rip = '.'.join(t2[16:]) - - - lport = self._get_ifname(ifidx) - - rport = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVPORT + '.' + ifidx + '.' + ifidx2) - rport = shorten_port_name(rport) - - devid = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVID + '.' + ifidx + '.' + ifidx2) - try: - mac_seg = [devid[x:x+4] for x in xrange(2, len(devid), 4)] - devid = '.'.join(mac_seg) - except: - pass - - rimg = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVDESC + '.' + ifidx + '.' + ifidx2) - if (rimg != None): - try: - rimg = binascii.unhexlify(rimg[2:]) - except: - pass - rimg = self._format_ios_ver(rimg) - - name = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVNAME + '.' + ifidx + '.' + ifidx2) - if ((name == None) | (name == '')): - name = devid - - link = self._get_node_link_info(ifidx, ifidx2) - link.remote_ip = rip - link.remote_name = name - link.discovered_proto = 'lldp' - link.local_port = lport - link.remote_port = rport - link.remote_plat = None - link.remote_ios = rimg - link.remote_mac = devid - - neighbors.append(link) - - return neighbors - - - def _get_node_link_info(self, ifidx, ifidx2): - snmpobj = self.snmpobj - - # get link type (trunk ?) - link_type = snmpobj.cache_lookup(self.link_type_vbtbl, OID_TRUNK_VTP + '.' + ifidx) - - native_vlan = None - allowed_vlans = 'All' - if (link_type == '1'): - native_vlan = snmpobj.cache_lookup(self.trk_native_vbtbl, OID_TRUNK_NATIVE + '.' + ifidx) - - allowed_vlans = snmpobj.cache_lookup(self.trk_allowed_vbtbl, OID_TRUNK_ALLOW + '.' + ifidx) - allowed_vlans = self._parse_allowed_vlans(allowed_vlans) - - # get LAG membership - lag = snmpobj.cache_lookup(self.lag_vbtbl, OID_LAG_LACP + '.' + ifidx) - lag_ifname = self._get_ifname(lag) - lag_ips = self._get_cidrs_from_ifidx(lag) - - # get VLAN info - vlan = snmpobj.cache_lookup(self.vlan_vbtbl, OID_IF_VLAN + '.' + ifidx) - - # get IP address - lifips = self._get_cidrs_from_ifidx(ifidx) - - link = mnet_node_link(remote_ip = None, - link_type = link_type, - vlan = vlan, - local_native_vlan = native_vlan, - local_allowed_vlans = allowed_vlans, - local_port = None, - remote_port = None, - local_lag = lag_ifname, - remote_lag = None, - local_lag_ips = lag_ips, - remote_lag_ips = [], - local_if_ip = lifips[0] if len(lifips) else None, - remote_if_ip = None, - remote_platform = None, - remote_ios = None, - remote_name = None, - discovered_proto = None) - return link - - - def _parse_allowed_vlans(self, allowed_vlans): - if (allowed_vlans.startswith('0x') == False): - return 'All' - - ret = '' - group = 0 - op = 0 - - for i in range(2, len(allowed_vlans)): - v = int(allowed_vlans[i], 16) - for b in range(0, 4): - a = v & (0x1 << (3 - b)) - vlan = ((i-2)*4)+b - - if (a): - if (op == 1): - group += 1 - else: - if (len(ret)): - if (group > 1): - ret += '-' - ret += str(vlan - 1) if vlan else '1' - else: - ret += ',%i' % vlan - else: - ret += str(vlan) - group = 0 - op = 1 - else: - if (op == 1): - if (len(ret)): - if (group > 1): - ret += '-%i' % (vlan - 1) - op = 0 - group = 0 - - if (op): - if (ret == '1'): - return 'All' - if (group): - ret += '-1001' - else: - ret += ',1001' - - return ret if len(ret) else 'All' - - - def _get_chassis_info(self): - # Get: - # Serial number - # Platform - # IOS - # Slow but reliable method by using SNMP directly. - # Usually we will get this via CDP. - snmpobj = self.snmpobj - - if ((self.stack.count > 0) | (self.vss.enabled == 1)): - # Use opts.get_stack_details - # or opts.get_vss_details - # for this. - return - - class_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_CLASS) - serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) - platf_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) - ios_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SOFTWARE) - - if (class_vbtbl == None): - return - - for row in class_vbtbl: - for n, v in row: - n = str(n) - if (v != ENTPHYCLASS_CHASSIS): - continue - - t = n.split('.') - idx = t[12] - - self.serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + idx) - self.plat = snmpobj.cache_lookup(platf_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + idx) - self.ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + idx) - - # modular switches might have IOS on a module rather than chassis - if (self.ios == ''): - for row in class_vbtbl: - for n, v in row: - n = str(n) - if (v != ENTPHYCLASS_MODULE): - continue - - t = n.split('.') - idx = t[12] - - self.ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + idx) - if (self.ios != ''): - break - - if (self.ios != ''): - break - self.ios = self._format_ios_ver(self.ios) - - return - - # - # Lookup and format an interface name from a cache table of indexes. - # - def _get_ifname(self, ifidx): - if ((ifidx == None) | (ifidx == OID_ERR)): - return 'UNKNOWN' - - str = self.snmpobj.cache_lookup(self.ifname_vbtbl, OID_IFNAME + '.' + ifidx) - str = shorten_port_name(str) - - return str or 'UNKNOWN' - - - def _get_system_name(self, domains): - return shorten_host_name(self.snmpobj.get_val(OID_SYSNAME), domains) - - - def _format_ios_ver(self, img): - img_s = re.search('(Version:? |CCM:)([^ ,$]*)', img) - if (img_s): - if (img_s.group(1) == 'CCM:'): - return 'CCM %s' % img_s.group(2) - return img_s.group(2) - - return img + class _node_opts: + + def __init__(self): + self.reset() + + def reset(self): + self.get_name = False + self.get_ip = False + self.get_plat = False + self.get_ios = False + self.get_router = False + self.get_ospf_id = False + self.get_bgp_las = False + self.get_hsrp_pri = False + self.get_hsrp_vip = False + self.get_serial = False + self.get_stack = False + self.get_stack_details = False + self.get_vss = False + self.get_vss_details = False + self.get_svi = False + self.get_lo = False + self.get_bootf = False + self.get_chassis_info = False + self.get_vpc = False + + def __init__(self): + self.opts = mnet_node._node_opts() + self.snmpobj = mnet_snmp() + self.links = [] + self.discovered = 0 + self.name = None + self.ip = None + self.plat = None + self.ios = None + self.router = None + self.ospf_id = None + self.bgp_las = None + self.hsrp_pri = None + self.hsrp_vip = None + self.serial = None + self.bootfile = None + self.svis = [] + self.loopbacks = [] + self.vpc_peerlink_if = None + self.vpc_peerlink_node = None + self.vpc_domain = None + self.stack = mnet_node_stack() + self.vss = mnet_node_vss() + self.cdp_vbtbl = None + self.ldp_vbtbl = None + self.link_type_vbtbl = None + self.lag_vbtbl = None + self.vlan_vbtbl = None + self.ifname_vbtbl = None + self.ifip_vbtbl = None + self.svi_vbtbl = None + self.ethif_vbtbl = None + self.trk_allowed_vbtbl = None + self.trk_native_vbtbl = None + + + def __str__(self): + return ('' % + (self.name, self.ip, self.plat, self.ios, self.serial, self.router, self.vss, self.stack)) + def __repr__(self): + return self.__str__() + + + def add_link(self, link): + self.links.append(link) + + + # find valid credentials for this node. + # try each known IP until one works + def try_snmp_creds(self, snmp_creds): + if (self.snmpobj.success == 0): + for ipaddr in self.ip: + if ((ipaddr == '0.0.0.0') | (ipaddr == 'UNKNOWN') | (ipaddr == '')): + continue + self.snmpobj._ip = ipaddr + if (self.snmpobj.get_cred(snmp_creds) == 1): + return 1 + return 0 + + + # Query this node. + # Set .opts and .snmp_creds before calling. + def query_node(self): + if (self.snmpobj.ver == 0): + # call try_snmp_creds() first or it failed to find good creds + return 0 + + snmpobj = self.snmpobj + + # router + if (self.opts.get_router == True): + if (self.router == None): + self.router = 1 if (snmpobj.get_val(OID_IP_ROUTING) == '1') else 0 + + if (self.router == 1): + # OSPF + if (self.opts.get_ospf_id == True): + self.ospf_id = snmpobj.get_val(OID_OSPF) + if (self.ospf_id != None): + self.ospf_id = snmpobj.get_val(OID_OSPF_ID) + + # BGP + if (self.opts.get_bgp_las == True): + self.bgp_las = snmpobj.get_val(OID_BGP_LAS) + if (self.bgp_las == '0'): # 4500x is reporting 0 with disabled + self.bgp_las = None + + # HSRP + if (self.opts.get_hsrp_pri == True): + self.hsrp_pri = snmpobj.get_val(OID_HSRP_PRI) + if (self.hsrp_pri != None): + self.hsrp_vip = snmpobj.get_val(OID_HSRP_VIP) + + # stack + if (self.opts.get_stack): + self.stack = mnet_node_stack(snmpobj, self.opts) + + # vss + if (self.opts.get_vss): + self.vss = mnet_node_vss(snmpobj, self.opts) + + # serial + if ((self.opts.get_serial == 1) & (self.stack.count == 0) & (self.vss.enabled == 0)): + self.serial = snmpobj.get_val(OID_SYS_SERIAL) + + # SVI + if (self.opts.get_svi == True): + if (self.svi_vbtbl == None): + self.svi_vbtbl = snmpobj.get_bulk(OID_SVI_VLANIF) + + if (self.ifip_vbtbl == None): + self.ifip_vbtbl = snmpobj.get_bulk(OID_IF_IP) + + for row in self.svi_vbtbl: + for n, v in row: + n = str(n) + vlan = n.split('.')[14] + svi = mnet_node_svi(vlan) + svi_ips = self.__get_cidrs_from_ifidx(v) + svi.ip.extend(svi_ips) + self.svis.append(svi) + + # loopback + if (self.opts.get_lo == True): + self.ethif_vbtbl = snmpobj.get_bulk(OID_ETH_IF) + + if (self.ifip_vbtbl == None): + self.ifip_vbtbl = snmpobj.get_bulk(OID_IF_IP) + + for row in self.ethif_vbtbl: + for n, v in row: + n = str(n) + if (n.startswith(OID_ETH_IF_TYPE) & (v == 24)): + ifidx = n.split('.')[10] + lo_name = snmpobj.cache_lookup(self.ethif_vbtbl, OID_ETH_IF_DESC + '.' + ifidx) + lo_ips = self.__get_cidrs_from_ifidx(ifidx) + lo = mnet_node_lo(lo_name, lo_ips) + self.loopbacks.append(lo) + + # bootfile + if (self.opts.get_bootf): + self.bootfile = snmpobj.get_val(OID_SYS_BOOT) + + # chassis info (serial, IOS, platform) + if (self.opts.get_chassis_info): + self.__get_chassis_info() + + # VPC peerlink + if (self.opts.get_vpc): + self.vpc_domain, self.vpc_peerlink_if = self.__get_vpc_info(self.ethif_vbtbl) + + # reset the get options + self.opts.reset() + return 1 + + + def __get_cidrs_from_ifidx(self, ifidx): + ips = [] + + for ifrow in self.ifip_vbtbl: + for ifn, ifv in ifrow: + ifn = str(ifn) + if (ifn.startswith(OID_IF_IP_ADDR)): + if (str(ifv) == str(ifidx)): + t = ifn.split('.') + ip = ".".join(t[10:]) + mask = self.snmpobj.cache_lookup(self.ifip_vbtbl, OID_IF_IP_NETM + ip) + nbits = util.get_net_bits_from_mask(mask) + cidr = '%s/%i' % (ip, nbits) + ips.append(cidr) + return ips + + + def __cache_common_mibs(self): + if (self.link_type_vbtbl == None): + self.link_type_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_VTP) + + if (self.lag_vbtbl == None): + self.lag_vbtbl = self.snmpobj.get_bulk(OID_LAG_LACP) + + if (self.vlan_vbtbl == None): + self.vlan_vbtbl = self.snmpobj.get_bulk(OID_IF_VLAN) + + if (self.ifname_vbtbl == None): + self.ifname_vbtbl = self.snmpobj.get_bulk(OID_IFNAME) + + if (self.trk_allowed_vbtbl == None): + self.trk_allowed_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_ALLOW) + + if (self.trk_native_vbtbl == None): + self.trk_native_vbtbl = self.snmpobj.get_bulk(OID_TRUNK_NATIVE) + + if (self.ifip_vbtbl == None): + self.ifip_vbtbl = self.snmpobj.get_bulk(OID_IF_IP) + + + # + # Get a list of CDP neighbors. + # Returns a list of mnet_node_link's + # + def get_cdp_neighbors(self): + neighbors = [] + snmpobj = self.snmpobj + + # get list of CDP neighbors + self.cdp_vbtbl = snmpobj.get_bulk(OID_CDP) + if (self.cdp_vbtbl == None): + print('No CDP Neighbors Found.') + return None + + # cache some common MIB trees + self.__cache_common_mibs() + + for row in self.cdp_vbtbl: + for name, val in row: + name = str(name) + # process only if this row is a CDP_DEVID + if (name.startswith(OID_CDP_DEVID) == 0): + continue + + t = name.split('.') + ifidx = t[14] + ifidx2 = t[15] + + # get remote IP + rip = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_IPADDR + '.' + ifidx + '.' + ifidx2) + rip = util.convert_ip_int_str(rip) + + # get local port + lport = self.__get_ifname(ifidx) + + # get remote port + rport = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_DEVPORT + '.' + ifidx + '.' + ifidx2) + rport = util.shorten_port_name(rport) + + # get remote platform + rplat = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_DEVPLAT + '.' + ifidx + '.' + ifidx2) + + # get IOS version + rios = snmpobj.cache_lookup(self.cdp_vbtbl, OID_CDP_IOS + '.' + ifidx + '.' + ifidx2) + if (rios != None): + try: + rios = binascii.unhexlify(rios[2:]) + except: + pass + rios = self.__format_ios_ver(rios) + + link = self.__get_node_link_info(ifidx, ifidx2) + link.remote_name = val.prettyPrint() + link.remote_ip = rip + link.discovered_proto = 'cdp' + link.local_port = lport + link.remote_port = rport + link.remote_plat = rplat + link.remote_ios = rios + + neighbors.append(link) + + return neighbors + + + # + # Get a list of LLDP neighbors. + # Returns a list of mnet_node_link's + # + def get_lldp_neighbors(self): + neighbors = [] + snmpobj = self.snmpobj + + self.lldp_vbtbl = snmpobj.get_bulk(OID_LLDP) + if (self.lldp_vbtbl == None): + print('No LLDP Neighbors Found.') + return None + + self.__cache_common_mibs() + + for row in self.lldp_vbtbl: + for name, val in row: + name = str(name) + if (name.startswith(OID_LLDP_TYPE) == 0): + continue + + t = name.split('.') + ifidx = t[12] + ifidx2 = t[13] + + rip = '' + for r in self.lldp_vbtbl: + for n, v in r: + n = str(n) + if (n.startswith(OID_LLDP_DEVADDR + '.' + ifidx + '.' + ifidx2)): + t2 = n.split('.') + rip = '.'.join(t2[16:]) + + + lport = self.__get_ifname(ifidx) + + rport = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVPORT + '.' + ifidx + '.' + ifidx2) + rport = util.shorten_port_name(rport) + + devid = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVID + '.' + ifidx + '.' + ifidx2) + try: + mac_seg = [devid[x:x+4] for x in xrange(2, len(devid), 4)] + devid = '.'.join(mac_seg) + except: + pass + + rimg = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVDESC + '.' + ifidx + '.' + ifidx2) + if (rimg != None): + try: + rimg = binascii.unhexlify(rimg[2:]) + except: + pass + rimg = self.__format_ios_ver(rimg) + + name = snmpobj.cache_lookup(self.lldp_vbtbl, OID_LLDP_DEVNAME + '.' + ifidx + '.' + ifidx2) + if ((name == None) | (name == '')): + name = devid + + link = self.__get_node_link_info(ifidx, ifidx2) + link.remote_ip = rip + link.remote_name = name + link.discovered_proto = 'lldp' + link.local_port = lport + link.remote_port = rport + link.remote_plat = None + link.remote_ios = rimg + link.remote_mac = devid + + neighbors.append(link) + + return neighbors + + + def __get_node_link_info(self, ifidx, ifidx2): + snmpobj = self.snmpobj + + # get link type (trunk ?) + link_type = snmpobj.cache_lookup(self.link_type_vbtbl, OID_TRUNK_VTP + '.' + ifidx) + + native_vlan = None + allowed_vlans = 'All' + if (link_type == '1'): + native_vlan = snmpobj.cache_lookup(self.trk_native_vbtbl, OID_TRUNK_NATIVE + '.' + ifidx) + + allowed_vlans = snmpobj.cache_lookup(self.trk_allowed_vbtbl, OID_TRUNK_ALLOW + '.' + ifidx) + allowed_vlans = self.__parse_allowed_vlans(allowed_vlans) + + # get LAG membership + lag = snmpobj.cache_lookup(self.lag_vbtbl, OID_LAG_LACP + '.' + ifidx) + lag_ifname = self.__get_ifname(lag) + lag_ips = self.__get_cidrs_from_ifidx(lag) + + # get VLAN info + vlan = snmpobj.cache_lookup(self.vlan_vbtbl, OID_IF_VLAN + '.' + ifidx) + + # get IP address + lifips = self.__get_cidrs_from_ifidx(ifidx) + + link = mnet_node_link() + link.link_type = link_type + link.vlan = vlan + link.local_native_vlan = native_vlan + link.local_allowed_vlans = allowed_vlans + link.local_lag = lag_ifname + link.local_lag_ips = lag_ips + link.remote_lag_ips = [] + link.local_if_ip = lifips[0] if len(lifips) else None + + return link + + + def __parse_allowed_vlans(self, allowed_vlans): + if (allowed_vlans.startswith('0x') == False): + return 'All' + + ret = '' + group = 0 + op = 0 + + for i in range(2, len(allowed_vlans)): + v = int(allowed_vlans[i], 16) + for b in range(0, 4): + a = v & (0x1 << (3 - b)) + vlan = ((i-2)*4)+b + + if (a): + if (op == 1): + group += 1 + else: + if (len(ret)): + if (group > 1): + ret += '-' + ret += str(vlan - 1) if vlan else '1' + else: + ret += ',%i' % vlan + else: + ret += str(vlan) + group = 0 + op = 1 + else: + if (op == 1): + if (len(ret)): + if (group > 1): + ret += '-%i' % (vlan - 1) + op = 0 + group = 0 + + if (op): + if (ret == '1'): + return 'All' + if (group): + ret += '-1001' + else: + ret += ',1001' + + return ret if len(ret) else 'All' + + + def __get_chassis_info(self): + # Get: + # Serial number + # Platform + # IOS + # Slow but reliable method by using SNMP directly. + # Usually we will get this via CDP. + snmpobj = self.snmpobj + + if ((self.stack.count > 0) | (self.vss.enabled == 1)): + # Use opts.get_stack_details + # or opts.get_vss_details + # for this. + return + + class_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_CLASS) + + if (self.opts.get_serial): serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) + if (self.opts.get_plat): platf_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) + if (self.opts.get_ios): ios_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SOFTWARE) + + if (class_vbtbl == None): + return + + for row in class_vbtbl: + for n, v in row: + n = str(n) + if (v != ENTPHYCLASS_CHASSIS): + continue + + t = n.split('.') + idx = t[12] + + if (self.opts.get_serial): self.serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + idx) + if (self.opts.get_plat): self.plat = snmpobj.cache_lookup(platf_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + idx) + if (self.opts.get_ios): self.ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + idx) + + if (self.opts.get_ios): + # modular switches might have IOS on a module rather than chassis + if (self.ios == ''): + for row in class_vbtbl: + for n, v in row: + n = str(n) + if (v != ENTPHYCLASS_MODULE): + continue + t = n.split('.') + idx = t[12] + self.ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + idx) + if (self.ios != ''): + break + if (self.ios != ''): + break + self.ios = self.__format_ios_ver(self.ios) + + return + + # + # Lookup and format an interface name from a cache table of indexes. + # + def __get_ifname(self, ifidx): + if ((ifidx == None) | (ifidx == OID_ERR)): + return 'UNKNOWN' + + str = self.snmpobj.cache_lookup(self.ifname_vbtbl, OID_IFNAME + '.' + ifidx) + str = util.shorten_port_name(str) + + return str or 'UNKNOWN' + + + def get_system_name(self, domains): + return util.shorten_host_name(self.snmpobj.get_val(OID_SYSNAME), domains) + + + # + # Normalize a reporeted software vesion string. + # + def __format_ios_ver(self, img): + x = img + if (type(img) == bytes): + x = img.decode("utf-8") + + try: + img_s = re.search('(Version:? |CCM:)([^ ,$]*)', x) + except: + return img + + if (img_s): + if (img_s.group(1) == 'CCM:'): + return 'CCM %s' % img_s.group(2) + return img_s.group(2) + + return img + + + def get_ipaddr(self): + ''' + Return the best IP address for this device. + Returns the first matching IP: + - Lowest Loopback interface + - Lowest SVI address/known IP + ''' + # Loopbacks - first interface + if (len(self.loopbacks)): + ips = self.loopbacks[0].ips + ips.sort() + return util.strip_slash_masklen(ips[0]) + + # SVIs + all known - lowest address + ips = [] + for svi in self.svis: + ips.extend(svi.ip) + ips.extend(self.ip) + ips.sort() + if (len(ips)): + return util.strip_slash_masklen(ips[0]) + + return '' + + + def __get_vpc_info(self, ifarr): + ''' + If VPC is enabled, + Return the VPC domain and interface name of the VPC peerlink. + ''' + tbl = self.snmpobj.get_bulk(OID_VPC_PEERLINK_IF) + if ((tbl == None) | (len(tbl) == 0)): + return (None, None) + domain = mnet_snmp.get_last_oid_token(tbl[0][0][0]) + ifidx = str(tbl[0][0][1]) + ifname = self.snmpobj.cache_lookup(ifarr, OID_ETH_IF_DESC + '.' + ifidx) + ifname = util.shorten_port_name(ifname) + return (domain, ifname) + diff --git a/mnetsuite/node_stack.py b/mnetsuite/node_stack.py new file mode 100644 index 0000000..4d41d6a --- /dev/null +++ b/mnetsuite/node_stack.py @@ -0,0 +1,127 @@ +#!/usr/bin/python + +''' + MNet Suite + node_stack.py + + Michael Laforest + mjlaforest@gmail.com + + Copyright (C) 2015-2018 Michael Laforest + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +from .snmp import * +from .util import * +import sys + + +class mnet_node_stack_member: + + def __init__(self): + self.opts = None + self.num = 0 + self.role = 0 + self.pri = 0 + self.mac = None + self.img = None + self.serial = None + self.plat = None + + def __str__(self): + return ('' % (self.num, self.role, self.serial)) + def __repr__(self): + return self.__str__() + + +class mnet_node_stack: + + def __init__(self, snmpobj = None, opts = None): + self.members = [] + self.count = 0 + self.enabled = 0 + self.opts = opts + + if (snmpobj != None): + self.get_members(snmpobj) + + + def __str__(self): + return ('' % (self.enabled, self.count, self.members)) + def __repr__(self): + return self.__str__() + + + def get_members(self, snmpobj): + if (self.opts == None): + return + + vbtbl = snmpobj.get_bulk(OID_STACK) + if (vbtbl == None): + return None + + if (self.opts.get_stack_details == 0): + self.count = 0 + for row in vbtbl: + for n, v in row: + n = str(n) + if (n.startswith(OID_STACK_NUM + '.')): + self.count += 1 + + if (self.count == 1): + self.count = 0 + return + + if (self.opts.get_serial): serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) + if (self.opts.get_plat): platf_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) + + for row in vbtbl: + for n, v in row: + n = str(n) + if (n.startswith(OID_STACK_NUM + '.')): + # Get info on this stack member and add to the list + m = mnet_node_stack_member() + t = n.split('.') + idx = t[14] + + m.num = v + m.role = snmpobj.cache_lookup(vbtbl, OID_STACK_ROLE + '.' + idx) + m.pri = snmpobj.cache_lookup(vbtbl, OID_STACK_PRI + '.' + idx) + m.mac = snmpobj.cache_lookup(vbtbl, OID_STACK_MAC + '.' + idx) + m.img = snmpobj.cache_lookup(vbtbl, OID_STACK_IMG + '.' + idx) + + if (self.opts.get_serial): m.serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + idx) + if (self.opts.get_plat): m.plat = snmpobj.cache_lookup(platf_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + idx) + + if (m.role == '1'): + m.role = 'master' + elif (m.role == '2'): + m.role = 'member' + elif (m.role == '3'): + m.role = 'notMember' + elif (m.role == '4'): + m.role = 'standby' + + mac_seg = [m.mac[x:x+4] for x in range(2, len(m.mac), 4)] + m.mac = '.'.join(mac_seg) + self.members.append(m) + + self.count = len(self.members) + if (self.count == 1): + self.count = 0 + if (self.count > 0): + self.enabled = 1 + diff --git a/mnetsuite/node_vss.py b/mnetsuite/node_vss.py new file mode 100644 index 0000000..0998a05 --- /dev/null +++ b/mnetsuite/node_vss.py @@ -0,0 +1,99 @@ +#!/usr/bin/python + +''' + MNet Suite + node_vss.py + + Michael Laforest + mjlaforest@gmail.com + + Copyright (C) 2015-2018 Michael Laforest + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +import sys +from .snmp import * +from .util import * + + +class mnet_node_vss_member: + def __init__(self): + self.opts = None + self.ios = None + self.serial = None + self.plat = None + + def __str__(self): + return ('' % (self.serial, self.plat)) + def __repr__(self): + return self.__str__() + + +class mnet_node_vss: + def __init__(self, snmpobj = None, opts = None): + self.members = [ mnet_node_vss_member(), mnet_node_vss_member() ] + self.enabled = 0 + self.domain = None + self.opts = opts + + if (snmpobj != None): + self.get_members(snmpobj) + + def __str__(self): + return ('' % (self.enabled, self.domain, self.members)) + def __repr__(self): + return self.__str__() + + def get_members(self, snmpobj): + # check if VSS is enabled + self.enabled = 1 if (snmpobj.get_val(OID_VSS_MODE) == '2') else 0 + if (self.enabled == 0): + return + + if (self.opts == None): + return + + self.domain = snmpobj.get_val(OID_VSS_DOMAIN) + + if (self.opts.get_vss_details == 0): + return + + # pull some VSS-related info + module_vbtbl = snmpobj.get_bulk(OID_VSS_MODULES) + + if (self.opts.get_ios): ios_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SOFTWARE) + if (self.opts.get_serial): serial_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_SERIAL) + if (self.opts.get_plat): plat_vbtbl = snmpobj.get_bulk(OID_ENTPHYENTRY_PLAT) + + chassis = 0 + + # enumerate VSS modules and find chassis info + for row in module_vbtbl: + for n,v in row: + if (v == 1): + modidx = str(n).split('.')[14] + # we want only chassis - line card module have no software + ios = snmpobj.cache_lookup(ios_vbtbl, OID_ENTPHYENTRY_SOFTWARE + '.' + modidx) + + if (ios != ''): + if (self.opts.get_ios): self.members[chassis].ios = ios + if (self.opts.get_plat): self.members[chassis].plat = snmpobj.cache_lookup(plat_vbtbl, OID_ENTPHYENTRY_PLAT + '.' + modidx) + if (self.opts.get_serial): self.members[chassis].serial = snmpobj.cache_lookup(serial_vbtbl, OID_ENTPHYENTRY_SERIAL + '.' + modidx) + chassis += 1 + + if (chassis > 1): + return + diff --git a/mnetsuite/output.py b/mnetsuite/output.py new file mode 100644 index 0000000..6a05d14 --- /dev/null +++ b/mnetsuite/output.py @@ -0,0 +1,37 @@ +#!/usr/bin/python + +''' + MNet Suite + output.py + + Michael Laforest + mjlaforest@gmail.com + + Copyright (C) 2015-2018 Michael Laforest + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +from .config import mnet_config +from ._version import __version__ + +class mnet_output: + + def __init__(self): + self.type = 'base' + + def generate(self): + raise Exception('mnet_output.generate() called direct') + diff --git a/mnetsuite/output_catalog.py b/mnetsuite/output_catalog.py new file mode 100644 index 0000000..0718fe3 --- /dev/null +++ b/mnetsuite/output_catalog.py @@ -0,0 +1,72 @@ +#!/usr/bin/python + +''' + MNet Suite + output_catalog.py + + Michael Laforest + mjlaforest@gmail.com + + Copyright (C) 2015-2018 Michael Laforest + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +from .config import mnet_config +from .network import mnet_network +from .output import mnet_output +from ._version import __version__ + + +class mnet_output_catalog: + + def __init__(self, network): + mnet_output.__init__(self) + self.network = network + self.config = network.config + + def generate(self, filename): + try: + f = open(filename, 'w') + except: + print('Unable to open catalog file "%s"' % filename) + return + + for n in self.network.nodes: + # get info that we may not have yet + n.opts.get_serial = True + n.opts.get_plat = True + n.opts.get_bootf = True + n.query_node() + + if (n.stack.count > 0): + # stackwise + for smem in n.stack.members: + serial = smem.serial or 'NOT CONFIGURED TO POLL' + plat = smem.plat or 'NOT CONFIGURED TO POLL' + f.write('"%s","%s","%s","%s","%s","STACK","%s"\n' % (n.name, n.ip[0], plat, n.ios, serial, n.bootfile)) + elif (n.vss.enabled != 0): + #vss + for i in range(0, 2): + serial = n.vss.members[i].serial + plat = n.vss.members[i].plat + ios = n.vss.members[i].ios + f.write('"%s","%s","%s","%s","%s","VSS","%s"\n' % (n.name, n.ip[0], plat, ios, serial, n.bootfile)) + else: + # stand alone + f.write('"%s","%s","%s","%s","%s","","%s"\n' % (n.name, n.ip[0], n.plat, n.ios, n.serial, n.bootfile)) + + f.close() + diff --git a/mnetsuite/output_diagram.py b/mnetsuite/output_diagram.py new file mode 100644 index 0000000..14c6703 --- /dev/null +++ b/mnetsuite/output_diagram.py @@ -0,0 +1,499 @@ +#!/usr/bin/python + +''' + MNet Suite + output_diagram.py + + Michael Laforest + mjlaforest@gmail.com + + Copyright (C) 2015-2018 Michael Laforest + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +import pydot +import datetime +import os + +from .config import mnet_config +from .network import mnet_network +from .output import mnet_output +from .util import * +from ._version import __version__ + + +class mnet_diagram_dot_node: + def __init__(self): + self.ntype = 'single' + self.shape = 'ellipse' + self.style = 'solid' + self.peripheries = 1 + self.label = '' + self.vss_label = '' + + +class mnet_output_diagram: + + def __init__(self, network): + mnet_output.__init__(self) + self.network = network + self.config = network.config + + def generate(self, dot_file, title): + self.network.reset_discovered() + + title_text_size = self.config.diagram.title_text_size + credits = '' \ + '' \ + '' \ + '' \ + '
' \ + '$title$
' \ + '$date$
' \ + '' \ + 'Generated by mnet suite $ver$
' \ + 'Michael Laforest

' \ + '
' % (title_text_size, title_text_size-2) + + today = datetime.datetime.now() + today = today.strftime('%Y-%m-%d %H:%M') + credits = credits.replace('$ver$', __version__) + credits = credits.replace('$date$', today) + credits = credits.replace('$title$', title) + + node_text_size = self.config.diagram.node_text_size + link_text_size = self.config.diagram.link_text_size + + diagram = pydot.Dot( + graph_type = 'graph', + labelloc = 'b', + labeljust = 'r', + fontsize = node_text_size, + label = '<%s>' % credits + ) + diagram.set_node_defaults( + fontsize = link_text_size + ) + diagram.set_edge_defaults( + fontsize = link_text_size, + labeljust = 'l' + ) + + # add all of the nodes and links + self.__generate(diagram, self.network.root_node) + + + # expand output string + files = util.expand_path_pattern(dot_file) + for f in files: + # get file extension + file_name, file_ext = os.path.splitext(f) + output_func = getattr(diagram, 'write_' + file_ext.lstrip('.')) + if (output_func == None): + print('Error: Output type "%s" does not exist.' % file_ext) + else: + output_func(f) + print('Created diagram: %s' % f) + + + def __generate(self, diagram, node): + if (node == None): + return (0, 0) + if (node.discovered > 0): + return (0, 0) + node.discovered = 1 + + dot_node = self.__get_node(diagram, node) + + if (dot_node.ntype == 'single'): + diagram.add_node( + pydot.Node( + name = node.name, + label = '<%s>' % dot_node.label, + style = dot_node.style, + shape = dot_node.shape, + peripheries = dot_node.peripheries + ) + ) + elif (dot_node.ntype == 'vss'): + cluster = pydot.Cluster( + graph_name = node.name, + suppress_disconnected = False, + labelloc = 't', + labeljust = 'c', + fontsize = self.config.diagram.node_text_size, + label = '<
VSS %s>' % node.vss.domain + ) + for i in range(0, 2): + # {vss.} vars + nlabel = dot_node.label.format(vss=node.vss.members[i]) + cluster.add_node( + pydot.Node( + name = '%s[mnetVSS%i]' % (node.name, i+1), + label = '<%s>' % nlabel, + style = dot_node.style, + shape = dot_node.shape, + peripheries = dot_node.peripheries + ) + ) + diagram.add_subgraph(cluster) + elif (dot_node.ntype == 'vpc'): + cluster = pydot.Cluster( + graph_name = node.name, + suppress_disconnected = False, + labelloc = 't', + labeljust = 'c', + fontsize = self.config.diagram.node_text_size, + label = '<
VPC %s>' % node.vpc_domain + ) + cluster.add_node( + pydot.Node( + name = node.name, + label = '<%s>' % dot_node.label, + style = dot_node.style, + shape = dot_node.shape, + peripheries = dot_node.peripheries + ) + ) + if (node.vpc_peerlink_node != None): + node2 = node.vpc_peerlink_node + node2.discovered = 1 + dot_node2 = self.__get_node(diagram, node2) + cluster.add_node( + pydot.Node( + name = node2.name, + label = '<%s>' % dot_node2.label, + style = dot_node2.style, + shape = dot_node2.shape, + peripheries = dot_node2.peripheries + ) + ) + diagram.add_subgraph(cluster) + elif (dot_node.ntype == 'stackwise'): + cluster = pydot.Cluster( + graph_name = node.name, + suppress_disconnected = False, + labelloc = 't', + labeljust = 'c', + fontsize = self.config.diagram.node_text_size, + label = '<
Stackwise>' + ) + for i in range(0, node.stack.count): + # {stack.} vars + if (len(node.stack.members) == 0): + nlabel = dot_node.label + else: + nlabel = dot_node.label.format(stack=node.stack.members[i]) + cluster.add_node( + pydot.Node( + name = '%s[mnetSW%i]' % (node.name, i+1), + label = '<%s>' % nlabel, + style = dot_node.style, + shape = dot_node.shape, + peripheries = dot_node.peripheries + ) + ) + diagram.add_subgraph(cluster) + + lags = [] + for link in node.links: + self.__generate(diagram, link.node) + + # determine if this link should be broken out or not + expand_lag = 0 + if (self.config.diagram.expand_lag == 1): + expand_lag = 1 + elif (link.local_lag == 'UNKNOWN'): + expand_lag = 1 + elif (self.__does_lag_span_devs(link.local_lag, node.links) > 1): + # a LAG could span different devices, eg Nexus. + # in this case we should always break it out, otherwise we could + # get an unlinked node in the diagram. + expand_lag = 1 + + if (expand_lag == 1): + self.__create_link(diagram, node, link, 0) + else: + found = 0 + for lag in lags: + if (link.local_lag == lag): + found = 1 + break + if (found == 0): + lags.append(link.local_lag) + self.__create_link(diagram, node, link, 1) + + + def __get_node(self, diagram, node): + dot_node = mnet_diagram_dot_node() + dot_node.ntype = 'single' + dot_node.shape = 'ellipse' + dot_node.style = 'solid' + dot_node.peripheries = 1 + dot_node.label = '' + + # get the node text + dot_node.label = self.__get_node_text(diagram, node, self.config.diagram.node_text) + + # set the node properties + if (node.vss.enabled == 1): + if (self.config.diagram.expand_vss == 1): + dot_node.ntype = 'vss' + else: + # group VSS into one diagram node + dot_node.peripheries = 2 + + if (node.stack.count > 0): + if (self.config.diagram.expand_stackwise == 1): + dot_node.ntype = 'stackwise' + else: + # group Stackwise into one diagram node + dot_node.peripheries = node.stack.count + + if (node.vpc_domain != None): + if (self.config.diagram.group_vpc == 1): + dot_node.ntype = 'vpc' + + if (node.router == 1): + dot_node.shape = 'diamond' + + return dot_node + + + def __create_link(self, diagram, node, link, draw_as_lag): + link_color = 'black' + link_style = 'solid' + link_label = '' + + if ((link.local_port == node.vpc_peerlink_if) | (link.local_lag == node.vpc_peerlink_if)): + link_label += 'VPC ' + + if (draw_as_lag): + link_label += 'LAG' + members = 0 + for l in node.links: + if (l.local_lag == link.local_lag): + members += 1 + link_label += '\n%i Members' % members + else: + link_label += 'P:%s\nC:%s' % (link.local_port, link.remote_port) + + is_lag = 1 if (link.local_lag != 'UNKNOWN') else 0 + + if (draw_as_lag == 0): + # LAG as member + if (is_lag): + local_lag_ip = '' + remote_lag_ip = '' + if (len(link.local_lag_ips)): + local_lag_ip = ' - %s' % link.local_lag_ips[0] + if (len(link.remote_lag_ips)): + remote_lag_ip = ' - %s' % link.remote_lag_ips[0] + + link_label += '\nLAG Member' + + if ((local_lag_ip == '') & (remote_lag_ip == '')): + link_label += '\nP:%s | C:%s' % (link.local_lag, link.remote_lag) + else: + link_label += '\nP:%s%s' % (link.local_lag, local_lag_ip) + link_label += '\nC:%s%s' % (link.remote_lag, remote_lag_ip) + + # IP Addresses + if ((link.local_if_ip != 'UNKNOWN') & (link.local_if_ip != None)): + link_label += '\nP:%s' % link.local_if_ip + if ((link.remote_if_ip != 'UNKNOWN') & (link.remote_if_ip != None)): + link_label += '\nC:%s' % link.remote_if_ip + else: + # LAG as grouping + for l in node.links: + if (l.local_lag == link.local_lag): + link_label += '\nP:%s | C:%s' % (l.local_port, l.remote_port) + + local_lag_ip = '' + remote_lag_ip = '' + + if (len(link.local_lag_ips)): + local_lag_ip = ' - %s' % link.local_lag_ips[0] + if (len(link.remote_lag_ips)): + remote_lag_ip = ' - %s' % link.remote_lag_ips[0] + + if ((local_lag_ip == '') & (remote_lag_ip == '')): + link_label += '\nP:%s | C:%s' % (link.local_lag, link.remote_lag) + else: + link_label += '\nP:%s%s' % (link.local_lag, local_lag_ip) + link_label += '\nC:%s%s' % (link.remote_lag, remote_lag_ip) + + + if (link.link_type == '1'): + # Trunk = Bold/Blue + link_color = 'blue' + link_style = 'bold' + + if ((link.local_native_vlan == link.remote_native_vlan) | (link.remote_native_vlan == None)): + link_label += '\nNative %s' % link.local_native_vlan + else: + link_label += '\nNative P:%s C:%s' % (link.local_native_vlan, link.remote_native_vlan) + + if (link.local_allowed_vlans == link.remote_allowed_vlans): + link_label += '\nAllowed %s' % link.local_allowed_vlans + else: + link_label += '\nAllowed P:%s' % link.local_allowed_vlans + if (link.remote_allowed_vlans != None): + link_label += '\nAllowed C:%s' % link.remote_allowed_vlans + elif (link.link_type is None): + # Routed = Bold/Red + link_color = 'red' + link_style = 'bold' + else: + # Switched access, include VLAN ID in label + if (link.vlan != None): + link_label += '\nVLAN %s' % link.vlan + + edge_src = node.name + edge_dst = link.node.name + lmod = util.get_module_from_interf(link.local_port) + rmod = util.get_module_from_interf(link.remote_port) + + if (self.config.diagram.expand_vss == 1): + if (node.vss.enabled == 1): + edge_src = '%s[mnetVSS%s]' % (node.name, lmod) + if (link.node.vss.enabled == 1): + edge_dst = '%s[mnetVSS%s]' % (link.node.name, rmod) + + if (self.config.diagram.expand_stackwise == 1): + if (node.stack.count > 0): + edge_src = '%s[mnetSW%s]' % (node.name, lmod) + if (link.node.stack.count > 0): + edge_dst = '%s[mnetSW%s]' % (link.node.name, rmod) + + edge = pydot.Edge( + edge_src, edge_dst, + dir = 'forward', + label = link_label, + color = link_color, + style = link_style + ) + + diagram.add_edge(edge) + + + def __does_lag_span_devs(self, lag_name, links): + if (lag_name == None): + return 0 + + devs = [] + for link in links: + if (link.local_lag == lag_name): + if (link.node.name not in devs): + devs.append(link.node.name) + + return len(devs) + + + def __eval_if_block(self, if_cond, node): + # evaluate condition + if_cond_eval = if_cond.format(node=node, config=self.config).strip() + try: + if eval(if_cond_eval): + return 1 + except: + if ((if_cond_eval != '0') & (if_cond_eval != 'None') & (if_cond_eval != '')): + return 1 + else: + return 0 + + return 0 + + + def __get_node_text(self, diagram, node, fmt): + ''' + Generate the node text given the format string 'fmt' + ''' + fmt_proc = fmt + + # IF blocks + while (1): + if_block = re.search('<%if ([^%]*): ([^%]*)%>', fmt_proc) + if (if_block == None): + break + + # evaluate condition + if_cond = if_block[1] + if_val = if_block[2] + if (self.__eval_if_block(if_cond, node) == 0): + if_val = '' + fmt_proc = fmt_proc[:if_block.span()[0]] + if_val + fmt_proc[if_block.span()[1]:] + + # {node.ip} = best IP + ip = node.get_ipaddr() + fmt_proc = fmt_proc.replace('{node.ip}', ip) + + # stackwise + stack_block = re.search('<%stack ([^%]*)%>', fmt_proc) + if (stack_block != None): + if (node.stack.count == 0): + # no stackwise, remove this + fmt_proc = fmt_proc[:stack_block.span()[0]] + fmt_proc[stack_block.span()[1]:] + else: + val = '' + if (self.config.diagram.expand_stackwise == 0): + if (self.config.diagram.get_stack_members): + for smem in node.stack.members: + nval = stack_block[1] + nval = nval.replace('{stack.num}', str(smem.num)) + nval = nval.replace('{stack.plat}', smem.plat) + nval = nval.replace('{stack.serial}', smem.serial) + nval = nval.replace('{stack.role}', smem.role) + val += nval + fmt_proc = fmt_proc[:stack_block.span()[0]] + val + fmt_proc[stack_block.span()[1]:] + + # loopbacks + loopback_block = re.search('<%loopback ([^%]*)%>', fmt_proc) + if (loopback_block != None): + val = '' + for lo in node.loopbacks: + for lo_ip in lo.ips: + nval = loopback_block[1] + nval = nval.replace('{lo.name}', lo.name) + nval = nval.replace('{lo.ip}', lo_ip) + val += nval + fmt_proc = fmt_proc[:loopback_block.span()[0]] + val + fmt_proc[loopback_block.span()[1]:] + + # SVIs + svi_block = re.search('<%svi ([^%]*)%>', fmt_proc) + if (svi_block != None): + val = '' + for svi in node.svis: + for svi_ip in svi.ip: + nval = svi_block[1] + nval = nval.replace('{svi.vlan}', svi.vlan) + nval = nval.replace('{svi.ip}', svi_ip) + val += nval + fmt_proc = fmt_proc[:svi_block.span()[0]] + val + fmt_proc[svi_block.span()[1]:] + + # replace {stack.} with magic + fmt_proc = re.sub('{stack\.(([a-zA-Z])*)}', '$stack2354$\g<1>$stack2354$', fmt_proc) + fmt_proc = re.sub('{vss\.(([a-zA-Z])*)}', '$vss2354$\g<1>$vss2354$', fmt_proc) + + # {node.} variables + fmt_proc = fmt_proc.format(node=node) + + # replace magics + fmt_proc = re.sub('\$stack2354\$(([a-zA-Z])*)\$stack2354\$', '{stack.\g<1>}', fmt_proc) + fmt_proc = re.sub('\$vss2354\$(([a-zA-Z])*)\$vss2354\$', '{vss.\g<1>}', fmt_proc) + + return fmt_proc + diff --git a/mnetsuite/output_stdout.py b/mnetsuite/output_stdout.py new file mode 100644 index 0000000..c0251c9 --- /dev/null +++ b/mnetsuite/output_stdout.py @@ -0,0 +1,132 @@ +#!/usr/bin/python + +''' + MNet Suite + output_stdout.py + + Michael Laforest + mjlaforest@gmail.com + + Copyright (C) 2015-2018 Michael Laforest + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +import os + +from .config import mnet_config +from .network import mnet_network +from .output import mnet_output +from ._version import __version__ + + +class mnet_output_stdout: + + def __init__(self, network): + mnet_output.__init__(self) + self.network = network + self.config = network.config + + def generate(self): + self.network.reset_discovered() + + print('-----') + print('----- DEVICES') + print('-----') + num_nodes, num_links = self.__generate(self.network.root_node) + + print('Discovered devices: %i' % num_nodes) + print('Discovered links: %i' % num_links) + + def __generate(self, node): + if (node == None): + return (0, 0) + if (node.discovered > 0): + return (0, 0) + node.discovered = 1 + + ret_nodes = 1 + ret_links = 0 + + print('-----------------------------------------') + print(' Name: %s' % node.name) + print(' IP: %s' % node.get_ipaddr()) + print(' Platform: %s' % node.plat) + print(' IOS Ver: %s' % node.ios) + + if ((node.vss.enabled == 0) & (node.stack.count == 0)): + print(' Serial: %s' % node.serial) + + print(' Routing: %s' % ('yes' if (node.router == 1) else 'no')) + print(' OSPF ID: %s' % node.ospf_id) + print(' BGP LAS: %s' % node.bgp_las) + print(' HSRP Pri: %s' % node.hsrp_pri) + print(' HSRP VIP: %s' % node.hsrp_vip) + + if (node.vss.enabled): + print(' VSS Mode: %i' % node.vss.enabled) + print('VSS Domain: %s' % node.vss.domain) + print(' VSS Slot 0:') + print(' IOS: %s' % node.vss.members[0].ios) + print(' Serial: %s' % node.vss.members[0].serial) + print(' Platform: %s' % node.vss.members[0].plat) + print(' VSS Slot 1:') + print(' IOS: %s' % node.vss.members[1].ios) + print(' Serial: %s' % node.vss.members[1].serial) + print(' Platform: %s' % node.vss.members[1].plat) + + if ((node.stack.count > 0) & (self.config.diagram.get_stack_members)): + print(' Stack Cnt: %i' % node.stack.count) + print(' Stack members:') + for smem in node.stack.members: + print(' Switch Number: %s' % (smem.num)) + print(' Role: %s' % (smem.role)) + print(' Priority: %s' % (smem.pri)) + print(' MAC: %s' % (smem.mac)) + print(' Platform: %s' % (smem.plat)) + print(' Image: %s' % (smem.img)) + print(' Serial: %s' % (smem.serial)) + + if (node.vpc_domain != None): + print('VPC Domain: %s' % (node.vpc_domain)) + print(' VPC plink: %s' % (node.vpc_peerlink_if)) + if (node.vpc_peerlink_node != None): + print(' VPC plink: %s (%s)' % (node.vpc_peerlink_node.name, node.vpc_peerlink_node.get_ipaddr())) + + print(' Loopbacks:') + for lo in node.loopbacks: + for lo_ip in lo.ips: + print(' %s - %s' % (lo.name, lo_ip)) + + print(' SVIs:') + for svi in node.svis: + for ip in svi.ip: + print(' SVI %s - %s' % (svi.vlan, ip)) + + print(' Links:') + for link in node.links: + lag = '' + if ((link.local_lag != None) | (link.remote_lag != None)): + lag = 'LAG[%s:%s]' % (link.local_lag or '', link.remote_lag or '') + print(' %s -> %s:%s %s' % (link.local_port, link.node.name, link.remote_port, lag)) + ret_links += 1 + + for link in node.links: + rn, rl = self.__generate(link.node) + ret_nodes += rn + ret_links += rl + + return (ret_nodes, ret_links) + diff --git a/mnetsuite/snmp.py b/mnetsuite/snmp.py index 96c922e..c5c8c79 100755 --- a/mnetsuite/snmp.py +++ b/mnetsuite/snmp.py @@ -1,227 +1,238 @@ #!/usr/bin/python ''' - MNet Suite - snmp.py + MNet Suite + snmp.py - Michael Laforest - mjlaforest@gmail.com + Michael Laforest + mjlaforest@gmail.com - Copyright (C) 2015 Michael Laforest + Copyright (C) 2015-2018 Michael Laforest - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ''' from pysnmp.entity.rfc3413.oneliner import cmdgen SNMP_PORT = 161 -OID_SYSNAME = '1.3.6.1.2.1.1.5.0' +OID_SYSNAME = '1.3.6.1.2.1.1.5.0' -OID_SYS_SERIAL = '1.3.6.1.4.1.9.3.6.3.0' -OID_SYS_BOOT = '1.3.6.1.4.1.9.2.1.73.0' +OID_SYS_SERIAL = '1.3.6.1.4.1.9.3.6.3.0' +OID_SYS_BOOT = '1.3.6.1.4.1.9.2.1.73.0' -OID_IFNAME = '1.3.6.1.2.1.31.1.1.1.1' # + ifidx (BULK) +OID_IFNAME = '1.3.6.1.2.1.31.1.1.1.1' # + ifidx (BULK) -OID_CDP = '1.3.6.1.4.1.9.9.23.1.2.1.1' # (BULK) -OID_CDP_IPADDR = '1.3.6.1.4.1.9.9.23.1.2.1.1.4' -OID_CDP_IOS = '1.3.6.1.4.1.9.9.23.1.2.1.1.5' -OID_CDP_DEVID = '1.3.6.1.4.1.9.9.23.1.2.1.1.6' # + .ifidx.53 -OID_CDP_DEVPORT = '1.3.6.1.4.1.9.9.23.1.2.1.1.7' -OID_CDP_DEVPLAT = '1.3.6.1.4.1.9.9.23.1.2.1.1.8' -OID_CDP_INT = '1.3.6.1.4.1.9.9.23.1.1.1.1.' # 6.ifidx +OID_CDP = '1.3.6.1.4.1.9.9.23.1.2.1.1' # (BULK) +OID_CDP_IPADDR = '1.3.6.1.4.1.9.9.23.1.2.1.1.4' +OID_CDP_IOS = '1.3.6.1.4.1.9.9.23.1.2.1.1.5' +OID_CDP_DEVID = '1.3.6.1.4.1.9.9.23.1.2.1.1.6' # + .ifidx.53 +OID_CDP_DEVPORT = '1.3.6.1.4.1.9.9.23.1.2.1.1.7' +OID_CDP_DEVPLAT = '1.3.6.1.4.1.9.9.23.1.2.1.1.8' +OID_CDP_INT = '1.3.6.1.4.1.9.9.23.1.1.1.1.' # 6.ifidx -OID_LLDP = '1.0.8802.1.1.2.1.4' -OID_LLDP_TYPE = '1.0.8802.1.1.2.1.4.1.1.4.0' -OID_LLDP_DEVID = '1.0.8802.1.1.2.1.4.1.1.5.0' -OID_LLDP_DEVPORT = '1.0.8802.1.1.2.1.4.1.1.7.0' -OID_LLDP_DEVNAME = '1.0.8802.1.1.2.1.4.1.1.9.0' -OID_LLDP_DEVDESC = '1.0.8802.1.1.2.1.4.1.1.10.0' -OID_LLDP_DEVADDR = '1.0.8802.1.1.2.1.4.2.1.5.0' +OID_LLDP = '1.0.8802.1.1.2.1.4' +OID_LLDP_TYPE = '1.0.8802.1.1.2.1.4.1.1.4.0' +OID_LLDP_DEVID = '1.0.8802.1.1.2.1.4.1.1.5.0' +OID_LLDP_DEVPORT = '1.0.8802.1.1.2.1.4.1.1.7.0' +OID_LLDP_DEVNAME = '1.0.8802.1.1.2.1.4.1.1.9.0' +OID_LLDP_DEVDESC = '1.0.8802.1.1.2.1.4.1.1.10.0' +OID_LLDP_DEVADDR = '1.0.8802.1.1.2.1.4.2.1.5.0' -OID_TRUNK_ALLOW = '1.3.6.1.4.1.9.9.46.1.6.1.1.4' # + ifidx (Allowed VLANs) -OID_TRUNK_NATIVE = '1.3.6.1.4.1.9.9.46.1.6.1.1.5' # + ifidx (Native VLAN) -OID_TRUNK_VTP = '1.3.6.1.4.1.9.9.46.1.6.1.1.14' # + ifidx (VTP Status) -OID_LAG_LACP = '1.2.840.10006.300.43.1.2.1.1.12' # + ifidx (BULK) +OID_TRUNK_ALLOW = '1.3.6.1.4.1.9.9.46.1.6.1.1.4' # + ifidx (Allowed VLANs) +OID_TRUNK_NATIVE = '1.3.6.1.4.1.9.9.46.1.6.1.1.5' # + ifidx (Native VLAN) +OID_TRUNK_VTP = '1.3.6.1.4.1.9.9.46.1.6.1.1.14' # + ifidx (VTP Status) +OID_LAG_LACP = '1.2.840.10006.300.43.1.2.1.1.12' # + ifidx (BULK) -OID_IP_ROUTING = '1.3.6.1.2.1.4.1.0' -OID_IF_VLAN = '1.3.6.1.4.1.9.9.68.1.2.2.1.2' # + ifidx (BULK) +OID_IP_ROUTING = '1.3.6.1.2.1.4.1.0' +OID_IF_VLAN = '1.3.6.1.4.1.9.9.68.1.2.2.1.2' # + ifidx (BULK) -OID_IF_IP = '1.3.6.1.2.1.4.20.1' # (BULK) -OID_IF_IP_ADDR = '1.3.6.1.2.1.4.20.1.2' # + a.b.c.d = ifid -OID_IF_IP_NETM = '1.3.6.1.2.1.4.20.1.3.' # + a.b.c.d +OID_IF_IP = '1.3.6.1.2.1.4.20.1' # (BULK) +OID_IF_IP_ADDR = '1.3.6.1.2.1.4.20.1.2' # + a.b.c.d = ifid +OID_IF_IP_NETM = '1.3.6.1.2.1.4.20.1.3.' # + a.b.c.d -OID_SVI_VLANIF = '1.3.6.1.4.1.9.9.128.1.1.1.1.3' # cviRoutedVlanIfIndex +OID_SVI_VLANIF = '1.3.6.1.4.1.9.9.128.1.1.1.1.3' # cviRoutedVlanIfIndex -OID_ETH_IF = '1.3.6.1.2.1.2.2.1' # ifEntry -OID_ETH_IF_TYPE = '1.3.6.1.2.1.2.2.1.3' # ifEntry.ifType 24=loopback -OID_ETH_IF_DESC = '1.3.6.1.2.1.2.2.1.2' # ifEntry.ifDescr +OID_ETH_IF = '1.3.6.1.2.1.2.2.1' # ifEntry +OID_ETH_IF_TYPE = '1.3.6.1.2.1.2.2.1.3' # ifEntry.ifType 24=loopback +OID_ETH_IF_DESC = '1.3.6.1.2.1.2.2.1.2' # ifEntry.ifDescr -OID_OSPF = '1.3.6.1.2.1.14.1.2.0' -OID_OSPF_ID = '1.3.6.1.2.1.14.1.1.0' +OID_OSPF = '1.3.6.1.2.1.14.1.2.0' +OID_OSPF_ID = '1.3.6.1.2.1.14.1.1.0' -OID_BGP_LAS = '1.3.6.1.2.1.15.2.0' +OID_BGP_LAS = '1.3.6.1.2.1.15.2.0' -OID_HSRP_PRI = '1.3.6.1.4.1.9.9.106.1.2.1.1.3.1.10' -OID_HSRP_VIP = '1.3.6.1.4.1.9.9.106.1.2.1.1.11.1.10' +OID_HSRP_PRI = '1.3.6.1.4.1.9.9.106.1.2.1.1.3.1.10' +OID_HSRP_VIP = '1.3.6.1.4.1.9.9.106.1.2.1.1.11.1.10' -OID_STACK = '1.3.6.1.4.1.9.9.500' -OID_STACK_NUM = '1.3.6.1.4.1.9.9.500.1.2.1.1.1' -OID_STACK_ROLE = '1.3.6.1.4.1.9.9.500.1.2.1.1.3' -OID_STACK_PRI = '1.3.6.1.4.1.9.9.500.1.2.1.1.4' -OID_STACK_MAC = '1.3.6.1.4.1.9.9.500.1.2.1.1.7' -OID_STACK_IMG = '1.3.6.1.4.1.9.9.500.1.2.1.1.8' +OID_STACK = '1.3.6.1.4.1.9.9.500' +OID_STACK_NUM = '1.3.6.1.4.1.9.9.500.1.2.1.1.1' +OID_STACK_ROLE = '1.3.6.1.4.1.9.9.500.1.2.1.1.3' +OID_STACK_PRI = '1.3.6.1.4.1.9.9.500.1.2.1.1.4' +OID_STACK_MAC = '1.3.6.1.4.1.9.9.500.1.2.1.1.7' +OID_STACK_IMG = '1.3.6.1.4.1.9.9.500.1.2.1.1.8' -OID_VSS_MODULES = '1.3.6.1.4.1.9.9.388.1.4.1.1.1' # .modidx = 1 -OID_VSS_MODE = '1.3.6.1.4.1.9.9.388.1.1.4.0' -OID_VSS_DOMAIN = '1.3.6.1.4.1.9.9.388.1.1.1.0' +OID_VSS_MODULES = '1.3.6.1.4.1.9.9.388.1.4.1.1.1' # .modidx = 1 +OID_VSS_MODE = '1.3.6.1.4.1.9.9.388.1.1.4.0' +OID_VSS_DOMAIN = '1.3.6.1.4.1.9.9.388.1.1.1.0' -OID_ENTPHYENTRY_CLASS = '1.3.6.1.2.1.47.1.1.1.1.5' # + .modifx (3=chassis) (9=module) -OID_ENTPHYENTRY_SOFTWARE = '1.3.6.1.2.1.47.1.1.1.1.9' # + .modidx -OID_ENTPHYENTRY_SERIAL = '1.3.6.1.2.1.47.1.1.1.1.11' # + .modidx -OID_ENTPHYENTRY_PLAT = '1.3.6.1.2.1.47.1.1.1.1.13' # + .modidx +OID_ENTPHYENTRY_CLASS = '1.3.6.1.2.1.47.1.1.1.1.5' # + .modifx (3=chassis) (9=module) +OID_ENTPHYENTRY_SOFTWARE = '1.3.6.1.2.1.47.1.1.1.1.9' # + .modidx +OID_ENTPHYENTRY_SERIAL = '1.3.6.1.2.1.47.1.1.1.1.11' # + .modidx +OID_ENTPHYENTRY_PLAT = '1.3.6.1.2.1.47.1.1.1.1.13' # + .modidx + +OID_VPC_PEERLINK_IF = '1.3.6.1.4.1.9.9.807.1.4.1.1.2' # mnet-tracemac -OID_VLANS = '1.3.6.1.4.1.9.9.46.1.3.1.1.2' -OID_VLAN_CAM = '1.3.6.1.2.1.17.4.3.1.1' +OID_VLANS = '1.3.6.1.4.1.9.9.46.1.3.1.1.2' +OID_VLAN_CAM = '1.3.6.1.2.1.17.4.3.1.1' -OID_BRIDGE_PORTNUMS = '1.3.6.1.2.1.17.4.3.1.2' -OID_IFINDEX = '1.3.6.1.2.1.17.1.4.1.2' +OID_BRIDGE_PORTNUMS = '1.3.6.1.2.1.17.4.3.1.2' +OID_IFINDEX = '1.3.6.1.2.1.17.1.4.1.2' -OID_ERR = 'No Such Object currently exists at this OID' -OID_ERR_INST = 'No Such Instance currently exists at this OID' +OID_ERR = 'No Such Object currently exists at this OID' +OID_ERR_INST = 'No Such Instance currently exists at this OID' # OID_ENTPHYENTRY_CLASS values -ENTPHYCLASS_OTHER = 1 -ENTPHYCLASS_UNKNOWN = 2 -ENTPHYCLASS_CHASSIS = 3 -ENTPHYCLASS_BACKPLANE = 4 -ENTPHYCLASS_CONTAINER = 5 -ENTPHYCLASS_POWERSUPPLY = 6 -ENTPHYCLASS_FAN = 7 -ENTPHYCLASS_SENSOR = 8 -ENTPHYCLASS_MODULE = 9 -ENTPHYCLASS_PORT = 10 -ENTPHYCLASS_STACK = 11 -ENTPHYCLASS_PDU = 12 +ENTPHYCLASS_OTHER = 1 +ENTPHYCLASS_UNKNOWN = 2 +ENTPHYCLASS_CHASSIS = 3 +ENTPHYCLASS_BACKPLANE = 4 +ENTPHYCLASS_CONTAINER = 5 +ENTPHYCLASS_POWERSUPPLY = 6 +ENTPHYCLASS_FAN = 7 +ENTPHYCLASS_SENSOR = 8 +ENTPHYCLASS_MODULE = 9 +ENTPHYCLASS_PORT = 10 +ENTPHYCLASS_STACK = 11 +ENTPHYCLASS_PDU = 12 class mnet_snmp: - success = 0 - ver = 0 - v2_community = None - _ip = None - - def __init__(self, ip='0.0.0.0'): - self.success = 0 - self.ver = 0 - self.v2_community = None - self._ip = ip - - # - # Try to find valid SNMP credentials in the provided list. - # Returns 1 if success, 0 if failed. - # - def get_cred(self, snmp_creds): - for cred in snmp_creds: - # we don't currently support anything other than SNMPv2 - if (cred['ver'] != 2): - continue - - community = cred['community'] - - cmdGen = cmdgen.CommandGenerator() - errIndication, errStatus, errIndex, varBinds = cmdGen.getCmd( - cmdgen.CommunityData(community), - cmdgen.UdpTransportTarget((self._ip, SNMP_PORT)), - '1.3.6.1.2.1.1.5.0', - lookupNames = False, lookupValues = False - ) - if errIndication: - continue - else: - self.ver = 2 - self.success = 1 - self.v2_community = community - - return 1 - - return 0 - - # - # Get single SNMP value at OID. - # - def get_val(self, oid): - cmdGen = cmdgen.CommandGenerator() - errIndication, errStatus, errIndex, varBinds = cmdGen.getCmd( - cmdgen.CommunityData(self.v2_community), - cmdgen.UdpTransportTarget((self._ip, SNMP_PORT), retries=2), - oid, lookupNames = False, lookupValues = False - ) - - if errIndication: - print '[E] get_snmp_val(%s): %s' % (self.v2_community, errIndication) - else: - r = varBinds[0][1].prettyPrint() - if ((r == OID_ERR) | (r == OID_ERR_INST)): - return None - return r - - return None - - - # - # Get bulk SNMP value at OID. - # - # Returns 1 on success, 0 on failure. - # - def get_bulk(self, oid): - cmdGen = cmdgen.CommandGenerator() - errIndication, errStatus, errIndex, varBindTable = cmdGen.bulkCmd( - cmdgen.CommunityData(self.v2_community), - cmdgen.UdpTransportTarget((self._ip, SNMP_PORT), timeout=30, retries=2), - 0, 10, - oid, - lookupNames = False, lookupValues = False - ) - - if errIndication: - print '[E] get_snmp_bulk(%s): %s' % (self.v2_community, errIndication) - else: - ret = [] - for r in varBindTable: - for n, v in r: - n = str(n) - if (n.startswith(oid) == 0): - return ret - ret.append(r) - return ret - - return None - - - # - # Lookup a value from the return table of get_bulk() - # - def cache_lookup(self, varBindTable, name): - if (varBindTable == None): - return None - - for r in varBindTable: - for n, v in r: - n = str(n) - if (n == name): - return v.prettyPrint() - return None + success = 0 + ver = 0 + v2_community = None + _ip = None + + def __init__(self, ip='0.0.0.0'): + self.success = 0 + self.ver = 0 + self.v2_community = None + self._ip = ip + + # + # Try to find valid SNMP credentials in the provided list. + # Returns 1 if success, 0 if failed. + # + def get_cred(self, snmp_creds): + for cred in snmp_creds: + # we don't currently support anything other than SNMPv2 + if (cred['ver'] != 2): + continue + + community = cred['community'] + + cmdGen = cmdgen.CommandGenerator() + errIndication, errStatus, errIndex, varBinds = cmdGen.getCmd( + cmdgen.CommunityData(community), + cmdgen.UdpTransportTarget((self._ip, SNMP_PORT)), + '1.3.6.1.2.1.1.5.0', + lookupNames = False, lookupValues = False + ) + if errIndication: + continue + else: + self.ver = 2 + self.success = 1 + self.v2_community = community + + return 1 + + return 0 + + # + # Get single SNMP value at OID. + # + def get_val(self, oid): + cmdGen = cmdgen.CommandGenerator() + errIndication, errStatus, errIndex, varBinds = cmdGen.getCmd( + cmdgen.CommunityData(self.v2_community), + cmdgen.UdpTransportTarget((self._ip, SNMP_PORT), retries=2), + oid, lookupNames = False, lookupValues = False + ) + + if errIndication: + print('[E] get_snmp_val(%s): %s' % (self.v2_community, errIndication)) + else: + r = varBinds[0][1].prettyPrint() + if ((r == OID_ERR) | (r == OID_ERR_INST)): + return None + return r + + return None + + + # + # Get bulk SNMP value at OID. + # + # Returns 1 on success, 0 on failure. + # + def get_bulk(self, oid): + cmdGen = cmdgen.CommandGenerator() + errIndication, errStatus, errIndex, varBindTable = cmdGen.bulkCmd( + cmdgen.CommunityData(self.v2_community), + cmdgen.UdpTransportTarget((self._ip, SNMP_PORT), timeout=30, retries=2), + 0, 50, + oid, + lookupNames = False, lookupValues = False + ) + + if errIndication: + print('[E] get_snmp_bulk(%s): %s' % (self.v2_community, errIndication)) + else: + ret = [] + for r in varBindTable: + for n, v in r: + n = str(n) + if (n.startswith(oid) == 0): + return ret + ret.append(r) + return ret + + return None + + + # + # Lookup a value from the return table of get_bulk() + # + def cache_lookup(self, varBindTable, name): + if (varBindTable == None): + return None + + for r in varBindTable: + for n, v in r: + n = str(n) + if (n == name): + return v.prettyPrint() + return None + + + # + # Given an OID 1.2.3.4...x.y.z return z + # + def get_last_oid_token(oid): + _oid = oid.getOid() + ts = len(_oid) + return _oid[ts-1] diff --git a/mnetsuite/tracemac.py b/mnetsuite/tracemac.py index f140316..070fbc2 100755 --- a/mnetsuite/tracemac.py +++ b/mnetsuite/tracemac.py @@ -1,171 +1,192 @@ #!/usr/bin/python ''' - MNet Suite - tracemac.py + MNet Suite + tracemac.py - Michael Laforest - mjlaforest@gmail.com + Michael Laforest + mjlaforest@gmail.com - Copyright (C) 2015 Michael Laforest + Copyright (C) 2015-2018 Michael Laforest - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - - - # mnet-tracemac.py -r -m [-c ] + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ''' import os import re -from snmp import * -from config import mnet_config -from util import * -from _version import __version__ +from .snmp import * +from .config import mnet_config +from .util import * +from ._version import __version__ class mnet_tracemac: - config = None - nodes = [] - - def __init__(self): - self.config = mnet_config() - - def load_config(self, config_file): - if (config_file): - self.config.load(config_file) - - - # - # Connect to the node at the specified IP and search for the - # specified MAC address in the table. - # - # Returns the IP to the next node or None - # - def trace(self, ip, mac_addr): - snmpobj = mnet_snmp(ip) - - # find valid credentials for this node - if (snmpobj.get_cred(self.config.snmp_creds) == 0): - return None - - system_name = shorten_host_name(snmpobj.get_val(OID_SYSNAME), self.config.host_domains) - - print('%s (%s)' % (system_name, ip)) - - # check for loops - for n in self.nodes: - if (n == system_name): - print('\n**************************\n' - '*** ENCOUNTERED A LOOP ***\n' - '**************************\n\n' - 'This means the MAC address is likely attached to\n' - 'a transparent, unmanaged, or undiscoverable bridge\n' - 'somewhere between the nodes that were crawled.') - return None - self.nodes.append(system_name) - - # cache some common MIB trees - vlan_vbtbl = snmpobj.get_bulk(OID_VLANS) - - for vlan_row in vlan_vbtbl: - for vlan_n, vlan_v in vlan_row: - t = vlan_n.prettyPrint().split('.') - vlan = int(t[15]) - - if (vlan >= 1002): - continue - - # change our SNMP credentials - old_cred = snmpobj.v2_community - snmpobj.v2_community = old_cred + '@' + str(vlan) - - cam_vbtbl = snmpobj.get_bulk(OID_VLAN_CAM) - cam_match = None - - for cam_row in cam_vbtbl: - for cam_n, cam_v in cam_row: - if (mac_addr == cam_v): - cam_match = cam_n - break - if (cam_match != None): - break - - if (cam_match == None): - # try next VLAN - continue - - p = cam_match.prettyPrint().split('.') - bridge_portnum = snmpobj.get_val(OID_BRIDGE_PORTNUMS +'.'+p[11]+'.'+p[12]+ '.'+p[13]+'.'+p[14]+'.'+p[15]+'.'+p[16]) - - ifidx = snmpobj.get_val(OID_IFINDEX + '.' + bridge_portnum) - - # restore SNMP credentials - snmpobj.v2_community = old_cred - - port = snmpobj.get_val(OID_IFNAME + '.' + ifidx) - - print(' VLAN: %s' % vlan) - print(' Port: %s' % port) - - # get list of CDP neighbors - cdp_vbtbl = snmpobj.get_bulk(OID_CDP) - if (cdp_vbtbl == None): - return None - - for cdp_row in cdp_vbtbl: - for cdp_n, cdp_v in cdp_row: - # process only if this row is a CDP_DEVID - if (cdp_n.prettyPrint().startswith(OID_CDP_DEVID) == 0): - continue - - t = cdp_n.prettyPrint().split('.') - if (ifidx != t[14]): - continue - - # get remote IP - rip = snmpobj.cache_lookup(cdp_vbtbl, OID_CDP_IPADDR + '.' + ifidx + '.' + t[15]) - rip = convert_ip_int_str(rip) - - rname = shorten_host_name(cdp_v.prettyPrint(), self.config.host_domains) - - print(' Next Node: %s' % rname) - print(' Next Node IP: %s' % rip) - - return rip - - return None - - print(' MAC not found in CAM table.') - return None - - # - # Parse an ASCII MAC address string to a hex string. - # 1122.3344.5566 - # 11:22:33:44:55:66 - # - def parse_mac(self, mac_str): - mac_str = re.sub('[\.:]', '', mac_str) + def __init__(self, conf): + self.config = conf + self.nodes = [] + + # + # Connect to the node at the specified IP and search for the + # specified MAC address in the table. + # + # Returns the IP to the next node or None + # + def trace(self, ip, mac_addr): + mac_addr = re.sub('[\.:]', '', mac_addr) + if (mac_addr == None): + print('MAC address is invalid.') + + snmpobj = mnet_snmp(ip) + + # find valid credentials for this node + if (snmpobj.get_cred(self.config.snmp_creds) == 0): + return None + + system_name = util.shorten_host_name(snmpobj.get_val(OID_SYSNAME), self.config.host_domains) + + print('%s (%s)' % (system_name, ip)) + + # check for loops + for n in self.nodes: + if (n == system_name): + print('\n**************************\n' + '*** ENCOUNTERED A LOOP ***\n' + '**************************\n\n' + 'This means the MAC address is likely attached to\n' + 'a transparent, unmanaged, or undiscoverable bridge\n' + 'somewhere between the nodes that were crawled.') + return None + self.nodes.append(system_name) + + # cache some common MIB trees + vlan_vbtbl = snmpobj.get_bulk(OID_VLANS) + + for vlan_row in vlan_vbtbl: + for vlan_n, vlan_v in vlan_row: + # get VLAN ID from OID + vlan = mnet_snmp.get_last_oid_token(vlan_n) + if (vlan >= 1002): + continue + + # change our SNMP credentials + old_cred = snmpobj.v2_community + snmpobj.v2_community = old_cred + '@' + str(vlan) + + # get CAM table for this VLAN + cam_vbtbl = snmpobj.get_bulk(OID_VLAN_CAM) + cam_match = None + + print('Try VLAN %s' % vlan) + + for cam_row in cam_vbtbl: + for cam_n, cam_v in cam_row: + + cam_entry = self.mac_format_ascii(cam_v, 0) + + print('[VLAN %s](%s) = (%s) == %s' % (vlan, mac_addr, cam_entry, (mac_addr==cam_entry))) + + if (mac_addr == cam_entry): + cam_match = cam_n + break + + if (cam_match != None): + break + + if (cam_match == None): + # try next VLAN + continue + + # find the interface index + p = cam_match.getOid() + portnum_oid = '%s.%i.%i.%i.%i.%i.%i' % (OID_BRIDGE_PORTNUMS, p[11], p[12], p[13], p[14], p[15], p[16]) + bridge_portnum = snmpobj.get_val(portnum_oid) + ifidx = snmpobj.get_val(OID_IFINDEX + '.' + bridge_portnum) + + # restore SNMP credentials + snmpobj.v2_community = old_cred + + # get the interface description from the index + port = snmpobj.get_val(OID_IFNAME + '.' + ifidx) + + print(' VLAN: %s' % vlan) + print(' Port: %s' % port) + + # get list of CDP neighbors + cdp_vbtbl = snmpobj.get_bulk(OID_CDP) + if (cdp_vbtbl == None): + return None + + for cdp_row in cdp_vbtbl: + for cdp_n, cdp_v in cdp_row: + # process only if this row is a CDP_DEVID + if (cdp_n.prettyPrint().startswith(OID_CDP_DEVID) == 0): + continue + + t = cdp_n.prettyPrint().split('.') + if (ifidx != t[14]): + continue + + # get remote IP + rip = snmpobj.cache_lookup(cdp_vbtbl, OID_CDP_IPADDR + '.' + ifidx + '.' + t[15]) + rip = util.convert_ip_int_str(rip) + + rname = util.shorten_host_name(cdp_v.prettyPrint(), self.config.host_domains) + + print(' Next Node: %s' % rname) + print(' Next Node IP: %s' % rip) + + return rip + + return None + + print(' MAC not found in CAM table.') + return None + + + # + # Parse an ASCII MAC address string to a hex string. + # + def mac_ascii_to_hex(self, mac_str): + mac_str = re.sub('[\.:]', '', mac_str) + + if (len(mac_str) != 12): + return None + + mac_hex = '' + for i in range(0, len(mac_str), 2): + mac_hex += chr(int(mac_str[i:i+2], 16)) + + return mac_hex - if (len(mac_str) != 12): - return None + def mac_format_ascii(self, mac_hex, inc_dots): + ''' + Format an SNMP MAC string to ASCII - mac_hex = '' - for i in range(0, len(mac_str), 2): - mac_hex += chr(int(mac_str[i:i+2], 16)) - - return mac_hex + Args: + mac_hex: Value from SNMP + inc_dots: 1 to format as aabb.ccdd.eeff, 0 to format aabbccddeeff + Returns: + String representation of the mac_hex + ''' + v = mac_hex.prettyPrint()[2:] + ret = '' + for i in range(0, len(v), 4): + ret += v[i:i+4] + if ((inc_dots) & ((i+4) < len(v))): + ret += '.' + return ret diff --git a/mnetsuite/util.py b/mnetsuite/util.py index f85a8fe..c089d1c 100755 --- a/mnetsuite/util.py +++ b/mnetsuite/util.py @@ -1,140 +1,173 @@ #!/usr/bin/python ''' - MNet Suite - util.py + MNet Suite + util.py - Michael Laforest - mjlaforest@gmail.com + Michael Laforest + mjlaforest@gmail.com - Copyright (C) 2015 Michael Laforest + Copyright (C) 2015-2018 Michael Laforest - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License - as published by the Free Software Foundation; either version 2 - of the License, or (at your option) any later version. + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ''' # Set the below line =0 if you do not want to use PyNetAddr -USE_NETADDR = 1 +USE_NETADDR = 1 import re import struct import binascii -from snmp import * -from config import mnet_config +from .snmp import * +from .config import mnet_config if (USE_NETADDR == 1): - from netaddr import IPAddress, IPNetwork - - -def get_net_bits_from_mask(netm): - cidr = 0 - - mt = netm.split('.') - for b in range(0, 4): - v = int(mt[b]) - while (v > 0): - if (v & 0x01): - cidr += 1 - v = v >> 1 - - return cidr - - -# -# Return 1 if IP is in the CIDR range. -# -def is_ipv4_in_cidr(ip, cidr): - t = cidr.split('/') - cidr_ip = t[0] - cidr_m = t[1] - - o = cidr_ip.split('.') - cidr_ip = ((int(o[0])<<24) + (int(o[1]) << 16) + (int(o[2]) << 8) + (int(o[3]))) - - cidr_mb = 0 - zeros = 32 - int(cidr_m) - for b in range(0, zeros): - cidr_mb = (cidr_mb << 1) | 0x01 - cidr_mb = 0xFFFFFFFF & ~cidr_mb - - o = ip.split('.') - ip = ((int(o[0])<<24) + (int(o[1]) << 16) + (int(o[2]) << 8) + (int(o[3]))) - - return ((cidr_ip & cidr_mb) == (ip & cidr_mb)) - - -# -# Shorten the port name string. -# -def shorten_port_name(port): - if (port == OID_ERR): - return 'UNKNOWN' - - if (port != None): - port = port.replace('TenGigabitEthernet', 'te') - port = port.replace('GigabitEthernet', 'gi') - port = port.replace('FastEthernet', 'fa') - port = port.replace('Te', 'te') - port = port.replace('Gi', 'gi') - port = port.replace('Fa', 'fa') - - return port - - -# -# Shorten the hostname by removing any defined domain suffixes. -# -def shorten_host_name(host, domains): - if (host == None): - return 'UNKNOWN' - - # some Motorola devices report as hex strings -# if (host.startswith('0x')): -# try: -# host = binascii.unhexlify(host[2:]) -# except: -# pass - - # Nexus appends (SERIAL) to hosts - host = re.sub('\([^\(]*\)$', '', host) - for domain in domains: - host = host.replace(domain, '') - - host = re.sub('-', '_', host) - return host - - -# -# Return a string representation of an IPv4 address -# -def convert_ip_int_str(iip): - if ((iip != None) & (iip != '')): - ip = int(iip, 0) - ip = '%i.%i.%i.%i' % (((ip >> 24) & 0xFF), ((ip >> 16) & 0xFF), ((ip >> 8) & 0xFF), (ip & 0xFF)) - return ip - - return 'UNKNOWN' - - -def get_module_from_interf(port): - try: - s = re.search('[^\d]*(\d*)/\d*/\d*', port) - if (s): - return s.group(1) - except: - pass - - return '1' + from netaddr import IPAddress, IPNetwork + +class util: + + def get_net_bits_from_mask(netm): + cidr = 0 + mt = netm.split('.') + for b in range(0, 4): + v = int(mt[b]) + while (v > 0): + if (v & 0x01): + cidr += 1 + v = v >> 1 + + return cidr + + + # + # Return 1 if IP is in the CIDR range. + # + def is_ipv4_in_cidr(ip, cidr): + t = cidr.split('/') + cidr_ip = t[0] + cidr_m = t[1] + + o = cidr_ip.split('.') + cidr_ip = ((int(o[0])<<24) + (int(o[1]) << 16) + (int(o[2]) << 8) + (int(o[3]))) + + cidr_mb = 0 + zeros = 32 - int(cidr_m) + for b in range(0, zeros): + cidr_mb = (cidr_mb << 1) | 0x01 + cidr_mb = 0xFFFFFFFF & ~cidr_mb + + o = ip.split('.') + ip = ((int(o[0])<<24) + (int(o[1]) << 16) + (int(o[2]) << 8) + (int(o[3]))) + + return ((cidr_ip & cidr_mb) == (ip & cidr_mb)) + + + # + # Shorten the port name string. + # + def shorten_port_name(port): + if (port == OID_ERR): + return 'UNKNOWN' + + if (port != None): + port = port.replace('TenGigabitEthernet', 'te') + port = port.replace('GigabitEthernet', 'gi') + port = port.replace('FastEthernet', 'fa') + port = port.replace('port-channel', 'po') + port = port.replace('Te', 'te') + port = port.replace('Gi', 'gi') + port = port.replace('Fa', 'fa') + port = port.replace('Po', 'po') + + return port + + + # + # Shorten the hostname by removing any defined domain suffixes. + # + def shorten_host_name(_host, domains): + host = _host + if (_host == None): + return 'UNKNOWN' + + # some devices (eg Motorola) report as hex strings + if (_host.startswith('0x')): + try: + host = binascii.unhexlify(_host[2:]).decode('utf-8') + except: + # this can fail if the node gives us bad data - revert to original + # ex, lldp can advertise MAC as hostname, and it might not convert + # to ascii + host = _host + + # Nexus appends (SERIAL) to hosts + host = re.sub('\([^\(]*\)$', '', host) + for domain in domains: + host = host.replace(domain, '') + + # fix some stuff that can break Dot + host = re.sub('-', '_', host) + host = host.rstrip(' \r\n\0') + + return host + + + # + # Return a string representation of an IPv4 address + # + def convert_ip_int_str(iip): + if ((iip != None) & (iip != '')): + ip = int(iip, 0) + ip = '%i.%i.%i.%i' % (((ip >> 24) & 0xFF), ((ip >> 16) & 0xFF), ((ip >> 8) & 0xFF), (ip & 0xFF)) + return ip + + return 'UNKNOWN' + + + def get_module_from_interf(port): + try: + s = re.search('[^\d]*(\d*)/\d*/\d*', port) + if (s): + return s.group(1) + except: + pass + return '1' + + + def strip_slash_masklen(cidr): + try: + s = re.search('^(.*)/[0-9]{1,2}$', cidr) + if (s): + return s.group(1) + except: + pass + return cidr + + + def expand_path_pattern(str): + try: + match = re.search('{([^\}]*)}', str) + tokens = match[1].split('|') + except: + return [str] + + ret = [] + for token in tokens: + s = str.replace(match[0], token) + ret.append(s) + + return ret diff --git a/setup.py b/setup.py index 01ec19e..90d97a6 100644 --- a/setup.py +++ b/setup.py @@ -23,36 +23,37 @@ long_description = open('README.md').read() setup( - name = 'mnet', - version = _version.__version__, - author = 'Michael Laforest', - author_email = 'mjlaforest@gmail.com', - license = 'LICENSE', - url = 'http://github.com/MJL85/mnet/', - - description = 'MNet Suite is a collection of Python tools for network professionals.', - long_description = long_description, - keywords = 'python network cisco diagram snmp cdp', - - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Utilities' - ], - - packages = ['mnetsuite'], - include_package_data = True, - - scripts = [ 'mnet.py' ], - - install_requires = [ - 'pysnmp>=4.2.5', - 'pyparsing==2.0.6', - 'pydot2', - 'netaddr>=0.7.14' - ] + name = 'mnet', + version = _version.__version__, + author = 'Michael Laforest', + author_email = 'mjlaforest@gmail.com', + license = 'LICENSE', + url = 'http://github.com/MJL85/mnet/', + + description = 'MNet Suite is a collection of Python tools for network professionals.', + long_description = long_description, + keywords = 'python network cisco diagram snmp cdp', + + classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Utilities' + ], + + packages = ['mnetsuite'], + include_package_data = True, + + scripts = [ 'mnet.py' ], + + install_requires = [ + 'graphviz', + 'pysnmp', + 'pydot', + 'pysnmp>=4.2.5', + 'pyparsing', + 'netaddr>=0.7.14' + ] ) -