#!/usr/bin/perl # # Dynamic route parser # Combines MTR and Ping features # # use strict; use warnings; use POSIX qw(strftime); use Scalar::Util qw(looks_like_number); use Socket; my $HELP=<<EO_HELP; Pandora FMS plugin for route parse Usage: $0 -t target [options] OPTIONS -c n number of tests (n) --no-ping 1 disable ping mode --no-mtr 1 disable mtr mode -s 1 symmetric routing (1) *default asymmetric routing (0) *Warning* in ping mode, the maximum number of steps detected is 9 EO_HELP ################################################################################ # Imported methods ################################################################################ ################################################################################ # Mix hashses ################################################################################ sub merge_hashes { my $_h1 = shift; my $_h2 = shift; my %ret = (%{$_h1}, %{$_h2}); return \%ret; } ################################################################################ # Check if a value is in an array ################################################################################ sub in_array($$){ my ($array, $value) = @_; if (empty($value)) { return 0; } my %params = map { $_ => 1 } @{$array}; if (exists($params{$value})) { return 1; } return 0; } ################################################################################ # Check if a given variable contents a number ################################################################################ sub to_number($) { my $n = shift; if(empty($n)) { return undef; } if ($n =~ /[\d+,]*\d+\.\d+/) { # American notation $n =~ s/,//g; } elsif ($n =~ /[\d+\.]*\d+,\d+/) { # Spanish notation $n =~ s/\.//g; $n =~ s/,/./g; } if(looks_like_number($n)) { return $n; } return undef; } ################################################################################ # Erase blank spaces before and after the string ################################################################################ sub trim($){ my $string = shift; if (empty ($string)){ return ""; } $string =~ s/\r//g; chomp ($string); $string =~ s/^\s+//g; $string =~ s/\s+$//g; return $string; } ################################################################################ # Empty ################################################################################ sub empty($){ my $str = shift; if (! (defined ($str)) ){ return 1; } if(looks_like_number($str)){ return 0; } if (ref ($str) eq "ARRAY") { return (($#{$str}<0)?1:0); } if (ref ($str) eq "HASH") { my @tmp = keys %{$str}; return (($#tmp<0)?1:0); } if ($str =~ /^\ *[\n\r]{0,2}\ *$/) { return 1; } return 0; } ################################################################################ # is Enabled ################################################################################ sub is_enabled($){ my $value = shift; if ((defined ($value)) && ($value > 0)){ # return true return 1; } #return false return 0; } ################################################################################ # print_module ################################################################################ sub print_module ($$;$){ my $config = shift; my $data = shift; my $not_print_flag = shift; if ((ref($data) ne "HASH") || (!defined $data->{name})) { return undef; } my $xml_module = ""; # If not a string type, remove all blank spaces! if ($data->{type} !~ m/string/){ $data->{value} = trim($data->{value}); } $data->{tags} = $data->{tags}?$data->{tags}:($config->{MODULE_TAG_LIST}?$config->{MODULE_TAG_LIST}:undef); $data->{interval} = $data->{interval}?$data->{interval}:($config->{MODULE_INTERVAL}?$config->{MODULE_INTERVAL}:undef); $data->{module_group} = $data->{module_group}?$data->{module_group}:($config->{MODULE_GROUP}?$config->{MODULE_GROUP}:undef); # Global instructions (if defined) $data->{unknown_instructions} = $config->{unknown_instructions} unless (defined($data->{unknown_instructions}) || (!defined($config->{unknown_instructions}))); $data->{warning_instructions} = $config->{warning_instructions} unless (defined($data->{warning_instructions}) || (!defined($config->{warning_instructions}))); $data->{critical_instructions} = $config->{critical_instructions} unless (defined($data->{critical_instructions}) || (!defined($config->{critical_instructions}))); $xml_module .= "<module>\n"; $xml_module .= "\t<name><![CDATA[" . $data->{name} . "]]></name>\n"; $xml_module .= "\t<type>" . $data->{type} . "</type>\n"; if (ref ($data->{value}) eq "ARRAY") { $xml_module .= "\t<datalist>\n"; foreach (@{$data->{value}}) { $xml_module .= "\t<data><![CDATA[" . $data->{value} . "]]></data>\n"; } $xml_module .= "\t</datalist>\n"; } else { $xml_module .= "\t<data><![CDATA[" . $data->{value} . "]]></data>\n"; } if ( !(empty($data->{desc}))) { $xml_module .= "\t<description><![CDATA[" . $data->{desc} . "]]></description>\n"; } if ( !(empty ($data->{unit})) ) { $xml_module .= "\t<unit><![CDATA[" . $data->{unit} . "]]></unit>\n"; } if (! (empty($data->{interval})) ) { $xml_module .= "\t<module_interval><![CDATA[" . $data->{interval} . "]]></module_interval>\n"; } if (! (empty($data->{tags})) ) { $xml_module .= "\t<tags>" . $data->{tags} . "</tags>\n"; } if (! (empty($data->{module_group})) ) { $xml_module .= "\t<module_group>" . $data->{module_group} . "</module_group>\n"; } if (! (empty($data->{module_parent})) ) { $xml_module .= "\t<module_parent>" . $data->{module_parent} . "</module_parent>\n"; } if (! (empty($data->{wmin})) ) { $xml_module .= "\t<min_warning><![CDATA[" . $data->{wmin} . "]]></min_warning>\n"; } if (! (empty($data->{wmax})) ) { $xml_module .= "\t<max_warning><![CDATA[" . $data->{wmax} . "]]></max_warning>\n"; } if (! (empty ($data->{cmin})) ) { $xml_module .= "\t<min_critical><![CDATA[" . $data->{cmin} . "]]></min_critical>\n"; } if (! (empty ($data->{cmax})) ){ $xml_module .= "\t<max_critical><![CDATA[" . $data->{cmax} . "]]></max_critical>\n"; } if (! (empty ($data->{wstr}))) { $xml_module .= "\t<str_warning><![CDATA[" . $data->{wstr} . "]]></str_warning>\n"; } if (! (empty ($data->{cstr}))) { $xml_module .= "\t<str_critical><![CDATA[" . $data->{cstr} . "]]></str_critical>\n"; } if (! (empty ($data->{cinv}))) { $xml_module .= "\t<critical_inverse><![CDATA[" . $data->{cinv} . "]]></critical_inverse>\n"; } if (! (empty ($data->{winv}))) { $xml_module .= "\t<warning_inverse><![CDATA[" . $data->{winv} . "]]></warning_inverse>\n"; } if (! (empty ($data->{max}))) { $xml_module .= "\t<max><![CDATA[" . $data->{max} . "]]></max>\n"; } if (! (empty ($data->{min}))) { $xml_module .= "\t<min><![CDATA[" . $data->{min} . "]]></min>\n"; } if (! (empty ($data->{post_process}))) { $xml_module .= "\t<post_process><![CDATA[" . $data->{post_process} . "]]></post_process>\n"; } if (! (empty ($data->{disabled}))) { $xml_module .= "\t<disabled><![CDATA[" . $data->{disabled} . "]]></disabled>\n"; } if (! (empty ($data->{min_ff_event}))) { $xml_module .= "\t<min_ff_event><![CDATA[" . $data->{min_ff_event} . "]]></min_ff_event>\n"; } if (! (empty ($data->{status}))) { $xml_module .= "\t<status><![CDATA[" . $data->{status} . "]]></status>\n"; } if (! (empty ($data->{timestamp}))) { $xml_module .= "\t<timestamp><![CDATA[" . $data->{timestamp} . "]]></timestamp>\n"; } if (! (empty ($data->{custom_id}))) { $xml_module .= "\t<custom_id><![CDATA[" . $data->{custom_id} . "]]></custom_id>\n"; } if (! (empty ($data->{critical_instructions}))) { $xml_module .= "\t<critical_instructions><![CDATA[" . $data->{critical_instructions} . "]]></critical_instructions>\n"; } if (! (empty ($data->{warning_instructions}))) { $xml_module .= "\t<warning_instructions><![CDATA[" . $data->{warning_instructions} . "]]></warning_instructions>\n"; } if (! (empty ($data->{unknown_instructions}))) { $xml_module .= "\t<unknown_instructions><![CDATA[" . $data->{unknown_instructions} . "]]></unknown_instructions>\n"; } if (! (empty ($data->{quiet}))) { $xml_module .= "\t<quiet><![CDATA[" . $data->{quiet} . "]]></quiet>\n"; } if (! (empty ($data->{module_ff_interval}))) { $xml_module .= "\t<module_ff_interval><![CDATA[" . $data->{module_ff_interval} . "]]></module_ff_interval>\n"; } if (! (empty ($data->{crontab}))) { $xml_module .= "\t<crontab><![CDATA[" . $data->{crontab} . "]]></crontab>\n"; } if (! (empty ($data->{min_ff_event_normal}))) { $xml_module .= "\t<min_ff_event_normal><![CDATA[" . $data->{min_ff_event_normal} . "]]></min_ff_event_normal>\n"; } if (! (empty ($data->{min_ff_event_warning}))) { $xml_module .= "\t<min_ff_event_warning><![CDATA[" . $data->{min_ff_event_warning} . "]]></min_ff_event_warning>\n"; } if (! (empty ($data->{min_ff_event_critical}))) { $xml_module .= "\t<min_ff_event_critical><![CDATA[" . $data->{min_ff_event_critical} . "]]></min_ff_event_critical>\n"; } if (! (empty ($data->{ff_timeout}))) { $xml_module .= "\t<ff_timeout><![CDATA[" . $data->{ff_timeout} . "]]></ff_timeout>\n"; } if (! (empty ($data->{each_ff}))) { $xml_module .= "\t<each_ff><![CDATA[" . $data->{each_ff} . "]]></each_ff>\n"; } if (! (empty ($data->{parent_unlink}))) { $xml_module .= "\t<module_parent_unlink><![CDATA[" . $data->{parent_unlink} . "]]></module_parent_unlink>\n"; } if (! (empty ($data->{alerts}))) { foreach my $alert (@{$data->{alerts}}){ $xml_module .= "\t<alert_template><![CDATA[" . $alert . "]]></alert_template>\n"; } } if (defined ($config->{global_alerts})){ foreach my $alert (@{$config->{global_alerts}}){ $xml_module .= "\t<alert_template><![CDATA[" . $alert . "]]></alert_template>\n"; } } $xml_module .= "</module>\n"; if (empty ($not_print_flag)) { print $xml_module; } return $xml_module; } ################################################################################ # General arguments parser ################################################################################ sub parse_arguments($) { my $raw = shift; my @args; if (defined($raw)){ @args = @{$raw}; } else { return {}; } my %data; for (my $i = 0; $i < $#args; $i+=2) { my $key = trim($args[$i]); $key =~ s/^-//; $data{$key} = trim($args[$i+1]); } return \%data; } ################################################################################ # General configuration file parser # # log=/PATH/TO/LOG/FILE # ################################################################################ sub parse_configuration($;$$){ my $conf_file = shift; my $separator; $separator = shift or $separator = "="; my $custom_eval = shift; my $_CFILE; my $_config; if (empty($conf_file)) { return { error => "Configuration file not specified" }; } if( !open ($_CFILE,"<", "$conf_file")) { return { error => "Cannot open configuration file" }; } while (my $line = <$_CFILE>){ if (($line =~ /^ *\r*\n*$/) || ($line =~ /^#/ )){ # skip blank lines and comments next; } my @parsed = split /$separator/, $line, 2; if ($line =~ /^\s*global_alerts/){ push (@{$_config->{global_alerts}}, trim($parsed[1])); next; } if (ref ($custom_eval) eq "ARRAY") { my $f = 0; foreach my $item (@{$custom_eval}) { if ($line =~ /$item->{exp}/) { $f = 1; my $aux; eval { $aux = $item->{target}->($item->{exp},$line); }; if (empty($_config)) { $_config = $aux; } elsif (!empty($aux) && (ref ($aux) eq "HASH")) { $_config = merge_hashes($_config, $aux); } } } if (is_enabled($f)){ next; } } $_config->{trim($parsed[0])} = trim($parsed[1]); } close ($_CFILE); return $_config; } ################################################################################ # End of import ################################################################################ ########################################################################## # Show a message to STDERR ########################################################################## sub msg { my $msg = shift; print STDERR strftime ("%Y-%m-%d %H:%M:%S", localtime()) . ": $msg\n"; } sub get_next { my ($route, $step) = @_; return $route->{'next'}->{$step}; } ########################################################################## # Extract route steps & timming from mtr output ########################################################################## sub get_steps { my ($conf) = @_; my $target = $conf->{'t'}; return [] if empty($target); my $mtr_r = ""; my $ping_r = ""; my @route_raw; my @ping_raw; if ($^O =~ /win/i){ $ping_r = trim(`ping -r 9 $target -n 1 | tr "Routea:->-" " " | gawk "/^[0-9\. ]*\$/ {if (\$1 != \\"\\"){ print \$1\";\"0}}"`) unless is_enabled($conf->{'-no-ping'}); @ping_raw = split /\n/, $ping_r; if ($#ping_raw < 0) { $mtr_r = trim(`mtr -n -o A -c $conf->{'c'} -r $target 2>/NUL | gawk "{print \$2";"\$3}"`) unless is_enabled($conf->{'-no-mtr'}); } } else { $ping_r = trim(`for x in \$(ping -n -c 1 -R $target 2>/dev/null | tr -s "R:" " " | awk '/^[0-9\. \t]*\$/ {if (\$1 != ""){print \$1}}'); do echo -n \$x";"; ping -c $conf->{'c'} \$x 2>/dev/null | grep rtt |awk '{print \$4}'| cut -f2 -d"/"; done`) unless is_enabled($conf->{'-no-ping'}); @ping_raw = split /\n/, $ping_r; if ($#ping_raw < 0) { $mtr_r = trim(`mtr -n -c $conf->{'c'} -r $target -o A 2>/dev/null | awk '/^[0-9\\|\\-\\. \\t]*\$/ {print \$2\";\"\$3}'`) unless is_enabled($conf->{'-no-mtr'}); } } @route_raw = split /\n/, $mtr_r; my @modules; my @steps; my $route; if ($#ping_raw >= 0) { # PING mode my $rng = scalar @ping_raw; my $checked; my $j; if (is_enabled($conf->{'s'})) { # Symmetric routing if ($^O =~ /win/i){ $j = 1; } else { $j = 0; } for (my $i=0; $i< ($rng/2); $i++) { my ($step,$time) = split /;/, $ping_raw[$i]; my $_r; if (defined($checked->{$step})) { $j-=2; next; } $checked->{$step} = 1; $_r->{'step'} = $step; if ($^O =~ /win/i) { $_r->{'time'} = trim(`ping -n $conf->{'c'} $_r->{'step'} | grep -e "Av" -e "Me" | gawk "{print \$NF}" | tr -d "ms"`) unless ((!defined($_r->{'step'}) || ($_r->{'step'} eq ""))); } else { $_r->{'time'} = $time; } if ((!defined($_r->{'step'}) || ($_r->{'step'} eq ""))) { $_r->{'step'} = "???"; } $steps[$j] = $_r; $j+=2; } if ($^O =~ /win/i){ $j = 0; } else { $j = 1; } for (my $i=$rng-1; $i>= ($rng/2); $i--) { my ($step,$time) = split /;/, $ping_raw[$i]; my $_r; if (defined($checked->{$step})) { $j-=2 if $j>2; next; } $_r->{'step'} = $step; if ($^O =~ /win/i) { $_r->{'time'} = trim(`ping -n $conf->{'c'} $_r->{'step'} | grep -e "Av" -e "Me" | gawk "{print \$NF}" | tr -d "ms"`) unless ((!defined($_r->{'step'}) || ($_r->{'step'} eq ""))); } else { $_r->{'time'} = $time; } if ((!defined($_r->{'step'}) || ($_r->{'step'} eq ""))) { $_r->{'step'} = "???"; } $steps[$j] = $_r; $j+=2; } } else { # Asymmetric routing for (my $i=0; $i< $rng; $i++) { my ($step,$time) = split /;/, $ping_raw[$i]; my $_r; if (defined($checked->{$step})) { # target reached last; } $checked->{$step} = 1; $_r->{'step'} = $step; if ($^O =~ /win/i) { $_r->{'time'} = trim(`ping -n $conf->{'c'} $_r->{'step'} | grep -e "Av" -e "Me" | gawk "{print \$NF}" | tr -d "ms"`) unless ((!defined($_r->{'step'}) || ($_r->{'step'} eq ""))); } else { $_r->{'time'} = $time; } if ((!defined($_r->{'step'}) || ($_r->{'step'} eq ""))) { $_r->{'step'} = "???"; } $steps[$i] = $_r; } } my $__origin; if ($^O !~ /win/i){ $__origin = shift @steps; } my $gw; if ($^O =~ /win/i) { ($gw->{'step'},$__origin->{'step'}) = split /;/, trim(`route print -4 | gawk "BEGIN {min=10000} /^\\ *0.0.0.0/ {met=\$NF;if(met<min){min=met; gw=\$3\\\";\\\"\$4}} END {print gw}"`); $gw->{'time'} = trim(`ping -n $conf->{'c'} $gw->{'step'} 2>/NUL | grep ms | grep -v TTL | gawk "{print \$NF}" | tr -d "ms"`); $__origin->{'time'} = 0; } else { $gw->{'step'} = trim(`route -n | awk 'BEGIN {min=100000} /^0/ {met=\$5; if(min>met){gw=\$2;min=met} } END { print gw}'`); $gw->{'time'} = trim(`ping -c $conf->{'c'} $gw->{'step'} 2>/dev/null | grep rtt |awk '{print \$4}'| cut -f2 -d"/"`); } unshift (@steps,($__origin,$gw)); my $unknown_count = 0; my $previous = undef; for(my $i=0; $i <= $#steps; $i++) { my $host = $steps[$i]->{'step'}; my $time = to_number($steps[$i]->{'time'}); my $preffix = 'RouteStep_'; my $desc = ''; if (!defined($time)) { next; } if ($host eq "???") { $host = "Hidden_" . (++$unknown_count); } if (($i == $#steps) && in_array($conf->{'target_ip'},$host)) { $preffix = 'RouteStepTarget_'; } elsif($i == $#steps) { $desc = 'Step unreachable'; } push @modules, { name => $preffix . $host, type => "generic_data", value => $time, unit => 'ms', desc => $desc, module_parent => $previous, parent_unlink => (empty($previous)?'1':undef) }; $previous = $preffix . $host; } return \@modules; } else { # MTR mode if ($#route_raw < 0) { # Empty output msg("Failed to analyze [$target]"); return []; } for (my $i=0; $i <= $#route_raw; $i++) { my $line = $route_raw[$i]; if (trim($line) =~ /(.*?);(.*)/) { my $host = $1; my $time = to_number($2); my $preffix = 'RouteStep_'; my $desc = ''; my $item; my $_r; if (!defined($time)) { next; } $_r->{'step'} = $host; $_r->{'time'} = $time; push @steps, $_r; } } my $__origin; if ($^O !~ /win/i){ $__origin = shift @steps; } my $gw; if ($^O =~ /win/i) { ($gw->{'step'},$__origin->{'step'}) = split /;/, trim(`route print -4 | gawk "BEGIN {min=10000} /^\\ *0.0.0.0/ {met=\$NF;if(met<min){min=met; gw=\$3\\\";\\\"\$4}} END {print gw}"`); $gw->{'time'} = trim(`ping -n $conf->{'c'} $gw->{'step'} 2>/NUL | grep ms | grep -v TTL | gawk "{print \$NF}" | tr -d "ms"`); $__origin->{'time'} = 0; } else { $gw->{'step'} = trim(`route -n | awk 'BEGIN {min=100000} /^0/ {met=\$5; if(min>met){gw=\$2;min=met} } END { print gw}'`); $gw->{'time'} = trim(`ping -c $conf->{'c'} $gw->{'step'} 2>/dev/null | grep rtt |awk '{print \$4}'| cut -f2 -d"/"`); my $__xorigin = trim(`ip a show dev \`route -n | awk 'BEGIN {min=100000} /^0/ {met=\$5; if(min>met){iface=\$NF;min=met} } END { print iface}'\` | grep -w inet | awk '{print \$2}' | cut -d'/' -f1`); if ($__xorigin ne $__origin->{'step'}) { unshift(@steps, $__origin); $__origin = {}; $__origin->{'step'} = $__xorigin; $__origin->{'time'} = 0; } } unshift (@steps,($__origin,$gw)); my $unknown_count = 0; my $previous = undef; for(my $i=0; $i <= $#steps; $i++) { my $host = $steps[$i]->{'step'}; my $time = to_number($steps[$i]->{'time'}); my $preffix = 'RouteStep_'; my $desc = ''; if (!defined($time)) { next; } if ($host eq "???") { $host = "Hidden_" . (++$unknown_count); } if (($i == $#steps) && in_array($conf->{'target_ip'},$host)) { $preffix = 'RouteStepTarget_'; } elsif($i == $#steps) { $desc = 'Step unreachable'; } push @modules, { name => $preffix . $host, type => "generic_data", value => $time, unit => 'ms', desc => $desc, module_parent => $previous, parent_unlink => (empty($previous)?'1':undef) }; $previous = $preffix . $host; } } return \@modules; } ########################################################################## ########################################################################## # MAIN ########################################################################## ########################################################################## if ($#ARGV < 0) { print STDERR $HELP; exit 1; } my $conf; my $file_conf = {}; my $args_conf = {}; if (-e $ARGV[0]) { $file_conf = parse_configuration($ARGV[0]); shift @ARGV; } $args_conf = parse_arguments(\@ARGV); $conf = merge_hashes($file_conf,$args_conf); if (!defined $conf->{'t'}) { print STDERR $HELP; exit 1; } my @targets = gethostbyname($conf->{'t'}); @targets = map { inet_ntoa($_) } @targets[4 .. $#targets]; if (empty(\@targets)) { print STDERR "Cannot resolve $conf->{'t'} \n"; exit 2; } $conf->{'target_ip'} = \@targets; $conf->{'c'} = 4 unless looks_like_number($conf->{'c'}); $conf->{'s'} = 1 unless looks_like_number($conf->{'s'}); my $results = get_steps($conf); foreach (@{$results}) { print_module($conf, $_); }