package cmw; ## ## $Id: cmw.pm.in 3000 2015-01-06 18:47:49Z heas $ ## ## rancid @VERSION@ ## Copyright (c) 1997-2015 by Terrapin Communications, Inc. ## All rights reserved. ## ## This code is derived from software contributed to and maintained by ## Terrapin Communications, Inc. by Henry Kilmer, John Heasley, Andrew Partan, ## Pete Whiting, Austin Schutz, and Andrew Fort. ## ## Redistribution and use in source and binary forms, with or without ## modification, are permitted provided that the following conditions ## are met: ## 1. Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## 2. Redistributions in binary form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in the ## documentation and/or other materials provided with the distribution. ## 3. All advertising materials mentioning features or use of this software ## must display the following acknowledgement: ## This product includes software developed by Terrapin Communications, ## Inc. and its contributors for RANCID. ## 4. Neither the name of Terrapin Communications, Inc. nor the names of its ## contributors may be used to endorse or promote products derived from ## this software without specific prior written permission. ## 5. It is requested that non-binding fixes and modifications be contributed ## back to Terrapin Communications, Inc. ## ## THIS SOFTWARE IS PROVIDED BY Terrapin Communications, INC. AND CONTRIBUTORS ## ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED ## TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ## PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COMPANY OR CONTRIBUTORS ## BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ## CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ## SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ## INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ## CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE ## POSSIBILITY OF SUCH DAMAGE. # # RANCID - Really Awesome New Cisco confIg Differ # # cmw.pm - Comware (Huawei/H3C/3com/HP) rancid procedures # # https://sites.google.com/site/jrbinks/code/rancid/cmwrancid use 5.010; use strict 'vars'; use warnings; no warnings 'uninitialized'; require(Exporter); our @ISA = qw(Exporter); use rancid @VERSION@; our $login; @ISA = qw(Exporter rancid main); #XXX @Exporter::EXPORT = qw($VERSION @commandtable %commands @commands); # XXX #our @EXPORT = qw(iproutesort iprouteval); # load-time initialization sub import { 0; } # post-open(collection file) initialization sub init { $login = "cmwlogin"; # add content lines and separators ProcessHistory("","","","!RANCID-CONTENT-TYPE: $devtype\n!\n"); 0; } # main loop of input of device output sub inloop { my($INPUT, $OUTPUT) = @_; my($cmd, $rval); TOP: while(<$INPUT>) { tr/\015//d; if (/[\]>#]\a?\s*quit/) { #if (/[>#]\s?exit$/) { $clean_run = 1; last; } if (/^Error:/) { print STDOUT ("$host $login error: $_"); print STDERR ("$host $login error: $_") if ($debug); $clean_run = 0; last; } while (/[\]>#]\a?\s*($cmds_regexp)\s*$/) { $cmd = $1; if (!defined($prompt)) { # Extract the prompt: look for something not [ or < at the start # of the line, until either ] or > or # is reached: $prompt = ($_ =~ /^([^\]>#]+[\]>]\a?)/)[0]; $prompt =~ s/([][}{)(\\])/\\$1/g; print STDERR ("PROMPT MATCH: $prompt\n") if ($debug); } print STDERR ("HIT COMMAND:$_") if ($debug); if (! defined($commands{$cmd})) { print STDERR "$host: found unexpected command - \"$cmd\"\n"; $clean_run = 0; last TOP; } $rval = &{$commands{$cmd}}($INPUT, $OUTPUT, $cmd); delete($commands{$cmd}); if ($rval == -1) { $clean_run = 0; last TOP; } } } } # dummy function sub DoNothing {print STDOUT;} # This is a sort routine that will sort on the # ip route when the ip route is anywhere in # the strings. sub iproutesort { my(%lines) = @_; my($i) = 0; my(@sorted_lines); foreach my $iproute (sort sortbyiproute keys %lines) { $sorted_lines[$i] = $lines{$iproute}; $i++; } @sorted_lines; } ## XXX Re-evaluate based on new routines, and consider IPv6: # These two routines will sort based upon IP route sub iprouteval { my(@a) = ($_[0] =~ m#^(\d+)\.(\d+)\.(\d+)\.(\d+)/(\d+)$#); $a[4] + ($a[3] + 256 * ($a[2] + 256 * ($a[1] + 256 * $a[0]))); } sub sortbyiproute { &iprouteval($a) <=> &iprouteval($b); } # Clean up lines on input, particularly ANSI characters as a result # of us not being able to turn off per-session terminal paging sub filter_lines { my ($l) = (@_); #s/^\033\[42D +\033\[42D(.+)$/$1/; #s/\033\133\064\062\104\s*\033\133\064\062\104//g; $l =~ s/\033\133\064\062\104\s+\033\133\064\062\104//g; $l =~ s/\033\133\061\066\104\s+\033\133\061\066\104//g; $l =~ s/\033\133\064\062\104//g; $l =~ s/\033\133\061\062\104//g; $l =~ s/.*\[37D(.*)/$1/g; # MA5600 # Probably not needed: $l =~ s/\s*---- More ----\s*//; $l =~ s/^ //; # Comware7 $l =~ s/Synchronization is finished.//g; return $l; } # Some commands are not supported on some models or versions # of code. # Remove the associated error messages, and rancid will ensure that # these are not treated as "missed" commands sub command_not_valid { my ($l) = (@_); if ( $l =~ /% Too many parameters found at '\^' position/ || /% Unrecognized command found at '\^' position/ || /% Incomplete command found at '\^' position./ || /(% )?Wrong parameter found at '\^' position/ || /% Wrong device .+/ ) { return(1); } else { return(0); } } # Some commands are not authorized under the current # user's permissions sub command_not_auth { my ($l) = (@_); if ( $l =~ /Permission denied\./ ) { return(1); } else { return(0); } } # Some output lines are always skipped sub skip_pattern { my ($l) = (@_); if ( $l =~ /^\s+\^$/ || /^$/ ) { return(1); } else { return(0); } } sub DisplayFib { my($INPUT, $OUTPUT, $cmd) = @_; my($dest, $nexthop, $flag, $outint, $label); print STDERR " In DisplayFib: $_" if ($debug); chomp; # Display the command we're processing in the output: ProcessHistory("FIB","","","!\n! '$cmd':\n!\n"); while (<$INPUT>) { tr/\015//d; last if(/^\s*$prompt/); chomp; $_ = filter_lines($_); return(1) if command_not_valid($_); return(-1) if command_not_auth($_); next if skip_pattern($_); next if /^Destination count: \d+ FIB entry count: \d+/; # Chop out some detail that changes over time (Comware 3): s/(\s+)TimeStamp\s+/$1/; # TimeStamp column heading ProcessHistory("FIB","","","! $_\n"); if ( m,Destination/Mask, ) { while (<$INPUT>) { tr/\015//d; last if(/^\s*$prompt/); chomp; $_ = filter_lines($_); # Chop out some detail that changes over time (Comware 3): s/(\s+)t\[\d+\]\s+/$1/; # TimeStamp data # "display fib" on comware7 shows host entries for things # learned via arp too. For a distribution router, that's all # the devices on subnets routed by it! # If we filter out all "UH" entries that are NOT InLoop, we # get acceptable output. # # So we want to keep: # # 0.0.0.0/32 127.0.0.1 UH InLoop0 Null # # but reject: # # 130.159.44.161/32 130.159.44.161 UH Vlan44 Null # # However I've a feeling that this is a problematic # solution, and some object to the notion that rancid # should be representing such potentially dynamic data in # the first place, which is why we created the # $display_fib flag for rancid 2, and in rancid 3 one # can modify the command table in rancid.types.conf ($dest, $nexthop, $flag, $outint, $label) = split; next if ( $flag eq 'UH' && $outint !~ /InLoop/ ); #ProcessHistory("FIB", "cmw::iproutesort", "$dest", "! $_\n"); ProcessHistory("FIB", "ipsort", "$dest", "! $_\n"); } ProcessHistory("FIB", "", "", "!\n"); # return here to ensure that we don't keep swallowing the # next command's output by returning to the surrounding # while loop return(0); } } return(0); } sub DisplayIPRoutes { my($INPUT, $OUTPUT, $cmd) = @_; my($key,$line,$spaces); print STDERR " In DisplayIPRoutes: $_" if ($debug); chomp; # Display the command we're processing in the output: ProcessHistory("IPR","","","!\n! '$cmd':\n!\n"); while (<$INPUT>) { tr/\015//d; last if(/^\s*$prompt/); chomp; $_ = filter_lines($_); return(1) if command_not_valid($_); return(-1) if command_not_auth($_); next if skip_pattern($_); ProcessHistory("IPR","","","! $_\n"); if ( m,Destination/Mask, ) { my $lastkey = ""; my $lastspaces = ""; while (<$INPUT>) { tr/\015//d; last if(/^\s*$prompt/); chomp; $_ = filter_lines($_); # If the key is blank, indicating multiple nexthops for # a particular route, then we use the previous one if ( m/^\s+(.+)/ ) { $key = $lastkey; $line = $key . $lastspaces . $1; #ProcessHistory("IPR", "cmw::iproutesort", "$key", "! $line\n"); ProcessHistory("IPR", "ipsort", "$key", "! $line\n"); } if ( m/^(\S+)(\s+).+/ ) { $key = $1; $line = $_; $spaces = $2; #ProcessHistory("IPR", "cmw::iproutesort", "$key", "! $line\n"); ProcessHistory("IPR", "ipsort", "$key", "! $line\n"); # Remember these, we may need them on the next pass $lastkey = $key; $lastspaces = $spaces; } } ProcessHistory("IPR", "", "", "!\n"); # return here to ensure that we don't keep swallowing the # next command's output by returning to the surrounding # while loop return(0); } } return(0); } ## This routine processes general output of "display" commands sub CommentOutput { my($INPUT, $OUTPUT, $cmd) = @_; print STDERR " In CommentOutput: $_" if ($debug); chomp; # Display the command we're processing in the output: ProcessHistory("COMMENTS", "", "", "!\n! '$cmd':\n!\n"); while (<$INPUT>) { tr/\015//d; # If we find the prompt, we're done # Ordinarily this matches from the start of the line, however # we've seen circumstances at least in Comware7 where the # prompt is preceded by whitespace, like so: # ^M^M ^Mdisplay boot-loader^M last if(/^\s*$prompt/); chomp; # filter out some junk $_ = filter_lines($_); return(1) if command_not_valid($_); return(-1) if command_not_auth($_); next if skip_pattern($_); # Now we skip or modify some lines from various commands to # remove irrelevant content, or to avoid insignificant diffs # 'display local-user': s/\s+Current AccessNum:.+$//; # 'display version': next if (/^(Uptime is \d|.+ [Uu]ptime is \d).+$/); # No longer necessary since skipping the whole Uptime line: # Mangle these lines: #s/(.*)[Uu]ptime.*.weeks.*.days*.*hours*.*minutes*(.*)/$1 $2/; #s/(.*)[Uu]ptime.*days*.*hours*.*minutes*(.*)/$1 $2/; # MSRs display a 'last reboot' time, but sometimes the seconds # vary by one or two (presumably internal rounding), so simply make # the last digit a fixed '0'. It would probably be safer to make # the last two digits a fixed '00'. # (Thx Alexander Belokopytov) s/(^Last reboot.+)\d$/${1}0/; # 'dir ' commands if ( $cmd =~ /^dir / ) { # First field is just an index number, chop it out s/^\s+\d+\s+(.+)/ $1/; # Remove filenames that are updated frequently next if ( /logfile\.log$/ || /lauth\.dat$/ || /ifindex\.dat$/ || /startup\.mdb$/ || /private-data\.txt$/ || /.+ KB total \(.+ KB free/ || /.+ KB total \(.+ KB free/ || /\.trash/ ); } # 'display ospf brief'/'display ospf' if ( $cmd =~ 'display ospf( brief)?' ) { #next if (/^(Ospf is not enabled yet|Info: OSPF routing process is not enabled|The feature OSPF has not been enabled.).+$/); next if (/^\s+SPF (Computation|Scheduled|calculation) Count:.+$/i); } if ( $cmd eq 'display power' ) { next if (/^(\s+Input Power).+$/); } if ( $cmd eq 'display poe powersupply' ) { next if (/^(PSE Total Power Consumption|PSE Available Power|PSE Peak Value|PSE Average Value).+$/); } if ( $cmd eq 'display ntp-service status' ) { next unless m/(Clock status|Clock stratum|Reference clock ID)/i; } if ( $cmd eq 'display transceiver interface' ) { s/^(\S+ transceiver information:).+$/$1/; # filter random garbage s/^Error: The transceiver is absent.$/ No transceiver present./; s/^Error: The combo port is inactive.$/ Inactive combo port./; } # Add the processed lines to the output buffer: ProcessHistory("COMMENTS","","","! $_\n"); } # Add a blank comment line to the output buffer ProcessHistory("COMMENTS", "", "", "!\n"); return(0); } ## This routine processes a "display current" sub DisplayCurrent { my($INPUT, $OUTPUT, $cmd) = @_; print STDERR " In DisplayCurrent: $_" if ($debug); while (<$INPUT>) { tr/\015//d; last if(/^\s*$prompt/); chomp; $_ = filter_lines($_); return(1) if command_not_valid($_); return(-1) if command_not_auth($_); next if skip_pattern($_); return(0) if ($found_end); # Filter out some sensitive data: if ( $filter_commstr && /^ ?(snmp-agent (target-host.+securityname|usm-user|community (read|write)) )(\S+)/ ) { ProcessHistory("","","","! $1$'\n"); next; } if ( $filter_pwds >= 1 && /^ ?(password (?:simple|cipher|hash) )(\S+)/ || /^ ?(super password( role level-\d)( level \d)? (cipher|simple|hash) )(\S+)/ || /^ ?(set authentication password (cipher|simple|hash) )(\S+)/ || /^ ?(key (?:authentication|accounting) )(\S+)/ ) { ProcessHistory("","","","! $1$'\n"); next; } # filter ssh public keys of devices connected to from this device if (/^ ?(public-key-code begin)/ && $filter_pwds >= 2) { ProcessHistory("","","","!$1\n"); ProcessHistory("","","","! \n"); while (<$INPUT>) { tr/\015//d; next if /^$/; next if /^\s+[[:xdigit:]]$/; if (/(^ public-key-code end)/) { ProcessHistory("","","","!$1\n"); last; } } next; } # Filter mac addresses dynamically added to config next if (/^ ?mac-address security.+$/); ProcessHistory("", "", "", "$_\n"); # end of config if (/^return/) { $found_end = 1; return(0); } } return(0); } 1; __END__