package eod_functions_V2; use strict; use warnings; use Cwd 'abs_path'; use File::Find; BEGIN { ##Discover and use required modules automatically at compile time my $ABS_PATH = abs_path($0); find(\&wanted, $ABS_PATH); sub wanted { if ( $_ eq "eod_templates_V2.pm"){unshift(@INC,$File::Find::dir);} } } use Data::Dumper; use Exporter; use eod_templates_V2 qw(:ALL); use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); $VERSION = 1.10; @ISA = qw(Exporter); @EXPORT = (); @EXPORT_OK = qw(print_help_and_exit populate_static_hashes populate_config_hash validate_command_line_args get_config_from_file); %EXPORT_TAGS = ( ALL => [qw(&print_help_and_exit &populate_static_hashes &populate_config_hash &validate_command_line_args &get_config_from_file)], ); ## BEGIN FUNCTIONS ################################################################################################# ## SUB HELP ######################################################################################################## sub print_help_and_exit { my $name = $_[0]; print " \nSECTION-USAGE: This script is highly versatile, make sure you familarize yourself with it properly, before using it\n All options are case sensitive. Option/Argument handling is unix standard.\n mandatory arguments: --readfile|-r read cli commands from file and perform them on all CPEs (default = $name.cfg. see section config file for help) --connection-proto|-c define the protocol to use to connect to the devices. (ssh OR telnet. No default setting) optional arguments: --prompt-based-auth|-p provide username + password for device access when asked. (default = enabled) WARNING: Supercedes LOGIN details in config file you provide! --file-based-auth|-f provide username + password for device access via the config-file. (default = disabled) --outfile|-o expects filename of csv to write the output to. (default = $name.csv) --no-quiet|-n log to STDOUT instead of logfile (mandatory if --debug is enabled, default = quiet) --debug|-d activate debug-log-level WARNING: do NOT use with multiple CPEsoutput is massive!!! (default = disabled) --help|-h print this help and exit --verbose|-v enable verbose feedback in outputfile. Depending on the executed commands this can be several hundred characters!!! (default = disabled) --Version|-V display extenensive version information and exit\n advanced optional arguments (case sensitive): --sendmail|-s expects comma separated list of e-mail addresses to send generated report to as argument (default = disabled) alternatively it can be invoked multiple times with different addresses. (First occurence will always be TO, all others CC) --max-connections|-m run script in forked mode (massively enhances performance) with the specified ammount of max child processes (1-25) (default = disabled) --tacacs|-t verify tacacs functionality on VPN-Hubsite before attempting to process devices. will abort script execution if tacacs service is unresponsive (default = disabled) \nSECTION-CONFIG-FILE: Keep in mind some devices handle commands case sensitive! The file that you provide will be reset to its defaults during script execution!!! open the default config file $name.CFG with an editor of your choice and study the instructions it contains. You may modify the default file or create a new one and pass it to the script with option -r Note: please report all unexpected behavior to ###. ty\n\n"; exit 0; } ### SUB create_data_structs ## DOES: populate data structs ###################################################### sub populate_static_hashes { my ($STATIC,$REPORT) = @_; %$STATIC = ( MODE_EN => "", MODE_CFG => "", MODE_WR => "", CHAR_DIS => "", CHAR_EN => "", CHAR_CFG => "", CMD_EN => "", CMD_CFG => "", CMD_WR => "", CHAR_NL => "", CHAR_INVALID => "", CHAR_PAGE => "", CMD_QUIT => "", CHAR_CONFIRM => "", CMD_CONFIRM => "", CMD_ROOTDIR => "", CMD_SKIP => "", CMD_SET_NO_PAGE => "", ); %$REPORT = ( UNREACHABLE => '0', OK => '0', NOK => '0', NOFEEDBACK => '0', ERROR => '0', ); } ### SUB populate_config ### Does: populate config hash with command line arguments ################################# sub populate_config_hash { my ($CONFIG,$arrayref) = @_; my ($name,$prompt,$configf,$quiet,$outf,$debug,$batchsize,$proto,$tacacs,$verbose,$header,$mailref,$path) = @{ $arrayref }; my $range = '1000000'; my $rand = int(rand($range)); ##generate random temp-db filename to allow for simultanous script execution #put first element in array as mail_to and all others as CC my $mailto = shift @{ $mailref }; my $mailcc = join '', @{ $mailref}; %$CONFIG = ( username => "", password =>"", verbose => "$verbose", quiet => "$quiet", hubsite => '1.1.1.1', log => "$name.log", lockfile => "$name.lock", tempdb => "$path/$rand.db", tacuser => "user", tacpass => "pass", cfg => "$configf", outcsv => "$outf", batchsize => "$batchsize", mail_to => "$mailto", mail_from => 'NMS-script@me.net', mail_sub => "", mail_cc => "$mailcc", mail_data => "", proto => "$proto", tacacs => "$tacacs", ); if ($prompt == 1){ get_username_and_password_from_stdin(\%$CONFIG); #if -p was selected, get username and password now otherwise when parsing config-file } } ### SUB get_username_and_password_from_stdin ### DOES: gets username and password from stdin ######################## sub get_username_and_password_from_stdin { my $CONFIG = $_[0]; eval { local $SIG{ALRM} = sub { die("timeout waiting for user input. Aborting script execution\n") }; alarm 10; print "please enter device or tacacs username\n"; $CONFIG->{username} = ; alarm 0; chomp $CONFIG->{username}; alarm 10; print "please enter device or tacacs password\n"; $CONFIG->{password} = ; alarm 0; chomp $CONFIG->{password}; }; if ($@){ die($@); } } ### SUB get_username_and_password_from_file ### DOES: parses username and password string + store in hash ######## sub get_username_and_password_from_file { my ($CONFIG,$line) = @_; my @temp = split(/\=/, $line); #split setting and value my $string = lc $temp[0]; $string =~ s/your-//; unless(! defined( $temp[1]) ){ #config file setting supersedes prompt, unless its undefined $CONFIG->{$string} = $temp[1]; #wichever it was is now in the CONFIG hash as a key(username or password) with the corresponding value } } ### SUB validate_command_line_args ### DOES: perform various sanity checks on command line arguments ############# sub validate_command_line_args { my ($name,$prompt,$configf,$quiet,$outf,$debug,$batchsize,$proto,$tacacs,$verbose,$header,$maildst,$path) = @{ $_[0] }; if ( $header ){ print get_tmpl('version'); exit 0; } unless($proto =~ /ssh|telnet/i){ die("$0:ERROR: Connection protocol must be specified.\n"); } unless( -e $configf){ die("$0:ERROR: $configf not found in $path. typo or missing path?\n");} if ($debug == 1 && $quiet == 1){ die("$0:ERROR: log-level=debug can NOT be used in quiet mode to avoid massive logfiles!\n");} if ( $batchsize > 30 ){ warn("$0:WARNING: More than 25 simultanous connections are NOT allowed!\nReducing Max_Connections to 25.\n"); $batchsize=25; }elsif ( $batchsize < 0 ){ warn("$0:WARNING: Negative Max_Connections are not permitted, forking has been disabled.\n"); $batchsize = 0; } } ## SUB process_static_config_setting ### DOES: check if setting is valid and store in hash or discard ############# sub process_static_config_setting { my ($STATIC,$line) = @_; $line =~ s/DEFINE//; my @t=split(/=/,$line); #split statement into key => value pairs $t[0] =~ s/\s*//; $t[1] =~ s/\\//; if ( exists $STATIC->{$t[0]} ){ $STATIC->{$t[0]}=$t[1]; #key is supported, set value }else{ warn("$0:WARNING $t[0] is NOT a valid static configuration option and will be ignored!\n"); #key is not supported and will be ignored } } ## SUB get_config_from_file ### DOES: process config file and set populate hashes accordingly ###################### sub get_config_from_file { my ($DATA,$CONFIG,$STATIC) = @_; my $count = 0; open(my $input, "<", "$CONFIG->{cfg}") or die("$0:ERROR unable to read from $CONFIG->{cfg}. ERR: $?/$!"); while (<$input>){ chomp $_; if ( $_ =~ /your-username/i || $_ =~ /your-password/i ){ #this line contains either the username or the password, it's irrelevant which get_username_and_password_from_file($CONFIG,$_); next; } if ( $_ =~ m/^#/ || ! $_ =~ /[a-z]|[A-Z]/ ){ next; } #ignore commented and empty lines, as well as password + username. if ( $_ =~ m/^DEFINE/ && $_ =~ /\=/){ #process config definitions process_static_config_setting($STATIC,$_); next; } if ( $_ =~ m/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):/ ){ #line contains a valid ipv4 IP addr, this is a device-commands set my (@metachars,@translations); populate_static_arrays(\@metachars,\@translations); $_ = substitute_escaped_metachars($_,\@metachars,\@translations); #substitute escaped metachars to prevent config parser from interpreting them my @temp = split(/,/, $_); #split line from config-file into command sub-sets, first one containing device IP as well my (@commands,@matches,$ip); for (my $i=0; $i < @temp; $i++){ #process each subset if ($i == 0){ my @temp1 = split(/:/, $temp[0]); #this is the first subset, it contains the IP $ip = $temp1[0]; $count++; $temp[0] =~ s/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)://; #remove IP from subset } my @arr; if ($temp[$i] =~ /\=/ ){ #this subset contains pattern-matching instructions my @temp2 = split(/\=/, $temp[$i]); #split subset into further subsets, first containing the command, the rest are patterns $temp2[0] = substitute_placeholders($temp2[0],\@metachars,\@translations); #all splitting is done on this string, revert it back to its original form $commands[$i] = $temp2[0]; #this is the command for (my $c=1; $c < @temp2; $c++){ #these are all the patterns $temp2[$c] = substitute_placeholders($temp2[$c],\@metachars,\@translations); ##undo prior substitutions $arr[$c] = $temp2[$c]; } $matches[$i] = \@arr; }else{ #this subset does not contain pattern matching instructions $temp[$i] = substitute_placeholders($temp[$i],\@metachars,\@translations); ##undo prior substitutions $commands[$i] = $temp[$i]; $arr[$i] = "no-match-hook"; ##set flag for easy identification later on in the script $matches[$i] = \@arr; } } $$DATA{$ip}->{commands} = \@commands; $$DATA{$ip}->{matches} = \@matches; } } $CONFIG->{devices_count} = $count; if ( $count == 0 ) { die("$0:ERROR $CONFIG->{cfg} does not contain valid Device-instruction-sets\n");} close($input); open(my $input1, ">", "$CONFIG->{cfg}") or warn("$0:WARNING unable to write to $CONFIG->{cfg}. CLEAR IT manually! ERR: $!/$?"); print $input1 get_tmpl("config"); #replace specified config file with template for security reasons close($input1); while ( my($key,$value) = (each %$STATIC)){ unless(defined($value) && $value ne ""){ die("$0:ERROR: $key definition missing in $CONFIG->{cfg}.Aborting Execution.\n");} #missing key definition, this can not be tolerated } } ### SUB populate_static_arrays ### DOES: populate static arrays metachars and translations ######################## sub populate_static_arrays { my ($meta_ref,$trans_ref) = @_; @{ $meta_ref } = (':','=','!',','); #declare config metachars @{ $trans_ref } = ('#00','#01','#02','#03') #declare bi-directional placeholders } ### SUB substitute_escaped_metachars ### DOES: substitute ctrl-chars for escaped metachars in $LINE ############### sub substitute_escaped_metachars { my ($line,$meta_ref,$trans_ref) = @_; for (my $i=0; $i<@{ $meta_ref };$i++){ $line =~ s/\#\Q$meta_ref->[$i]/$trans_ref->[$i]/g; ## hardcoded ESCSEQ => # } return $line; } ### SUB substitute_placeholders ### DOES: revert string to its original form priot to escapeing #################### sub substitute_placeholders { my ($line,$meta_ref,$trans_ref) = @_; for (my $i=0; $i<@{ $trans_ref };$i++){ $line =~ s/$trans_ref->[$i]/$meta_ref->[$i]/g; } return $line; } 1;