1#!/usr/bin/perl 2 3use strict; 4use warnings; 5 6=head1 NAME 7 8exec-nagios.px 9 10=head1 DESCRIPTION 11 12This script allows you to use plugins that were written for Nagios with 13collectd's C<exec-plugin>. If the plugin checks some kind of threshold, please 14consider configuring the threshold using collectd's own facilities instead of 15using this transition layer. 16 17=cut 18 19use Sys::Hostname ('hostname'); 20use File::Basename ('basename'); 21use Config::General ('ParseConfig'); 22use Regexp::Common ('number'); 23 24our $ConfigFile = '/etc/exec-nagios.conf'; 25our $TypeMap = {}; 26our $NRPEMap = {}; 27our $Scripts = []; 28our $Interval = defined ($ENV{'COLLECTD_INTERVAL'}) ? (0 + $ENV{'COLLECTD_INTERVAL'}) : 300; 29our $Hostname = defined ($ENV{'COLLECTD_HOSTNAME'}) ? $ENV{'COLLECTD_HOSTNAME'} : ''; 30 31main (); 32exit (0); 33 34# Configuration 35# {{{ 36 37=head1 CONFIGURATION 38 39This script reads its configuration from F</etc/exec-nagios.conf>. The 40configuration is read using C<Config::General> which understands a Apache-like 41config syntax, so it's very similar to the F<collectd.conf> syntax, too. 42 43Here's a short sample config: 44 45 NRPEConfig "/etc/nrpe.cfg" 46 Interval 300 47 <Script /usr/lib/nagios/check_tcp> 48 Arguments -H alice -p 22 49 Type delay 50 </Script> 51 <Script /usr/lib/nagios/check_dns> 52 Arguments -H alice 53 Type delay 54 </Script> 55 56The options have the following semantic (i.E<nbsp>e. meaning): 57 58=over 4 59 60=item B<NRPEConfig> I<File> 61 62Read the NRPE config and add the command definitions to an alias table. After 63reading the file you can use the NRPE command name rather than the script's 64filename within B<Script> blocks (see below). If both, the NRPE config and the 65B<Script> block, define arguments they will be merged by concatenating the 66arguments together in the order "NRPE-args Script-args". 67 68Please note that this option is rather dumb. It does not support "command 69argument processing" (i.e. replacing C<$ARG1$> and friends), inclusion of other 70NRPE config files, include directories etc. 71 72=item B<Interval> I<Seconds> 73 74Sets the interval in which the plugins are executed. This doesn't need to match 75the interval setting of the collectd daemon. Usually, you want to execute the 76Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10 77seconds. 78 79=item E<lt>B<Script> I<File>E<gt> 80 81Adds a script to the list of scripts to be executed once per I<Interval> 82seconds. If the B<NRPEConfig> is given above the B<Script> block, you may use 83the NRPE command name rather than the script's filename. You can use the 84following optional arguments to specify the operation further: 85 86=over 4 87 88=item B<Arguments> I<Arguments> 89 90Pass the arguments I<Arguments> to the script. This is often needed with Nagios 91plugins, because much of the logic is implemented in the plugins, not in the 92daemon. If you need to specify a warning and/or critical range here, please 93consider using collectd's own threshold mechanism, which is by far the more 94elegant solution than this transition layer. 95 96=item B<Type> I<Type> 97 98If the plugin provides "performance data" the performance data is dispatched to 99collectd with this type. If no type is configured the data is ignored. Please 100note that this is limited to types that take exactly one value, such as the 101type C<delay> in the example above. If you need more complex performance data, 102rewrite the plugin as a collectd plugin (or at least port it do run directly 103with the C<exec-plugin>). 104 105=back 106 107=back 108 109=cut 110 111sub parse_nrpe_conf 112{ 113 my $file = shift; 114 my $fh; 115 my $status; 116 117 $status = open ($fh, '<', $file); 118 if (!$status) 119 { 120 print STDERR "Reading NRPE config from \"$file\" failed: $!\n"; 121 return; 122 } 123 124 while (<$fh>) 125 { 126 my $line = $_; 127 chomp ($line); 128 129 if ($line =~ m/^\s*command\[([^\]]+)\]\s*=\s*(.+)$/) 130 { 131 my $alias = $1; 132 my $script; 133 my $arguments; 134 135 ($script, $arguments) = split (' ', $2, 2); 136 137 if ($NRPEMap->{$alias}) 138 { 139 print STDERR "Warning: NRPE command \"$alias\" redefined.\n"; 140 } 141 142 $NRPEMap->{$alias} = { script => $script }; 143 if ($arguments) 144 { 145 $NRPEMap->{$alias}{'arguments'} = $arguments; 146 } 147 } 148 } # while (<$fh>) 149 150 close ($fh); 151} # parse_nrpe_conf 152 153sub handle_config_addtype 154{ 155 my $list = shift; 156 157 for (my $i = 0; $i < @$list; $i++) 158 { 159 my ($to, @from) = split (' ', $list->[$i]); 160 for (my $j = 0; $j < @from; $j++) 161 { 162 $TypeMap->{$from[$j]} = $to; 163 } 164 } 165} # handle_config_addtype 166 167# Update the script record. This function adds the name of the script / 168# executable to the hash and merges the configured and NRPE arguments if 169# required. 170sub update_script_opts 171{ 172 my $opts = shift; 173 my $script = shift; 174 my $nrpe_args = shift; 175 176 $opts->{'script'} = $script; 177 178 if ($nrpe_args) 179 { 180 if ($opts->{'arguments'}) 181 { 182 $opts->{'arguments'} = $nrpe_args . ' ' . $opts->{'arguments'}; 183 } 184 else 185 { 186 $opts->{'arguments'} = $nrpe_args; 187 } 188 } 189} # update_script_opts 190 191sub handle_config_script 192{ 193 my $scripts = shift; 194 195 for (keys %$scripts) 196 { 197 my $script = $_; 198 my $opts = $scripts->{$script}; 199 200 my $nrpe_args = ''; 201 202 # Check if the script exists in the NRPE map. If so, replace the alias name 203 # with the actual script name. 204 if ($NRPEMap->{$script}) 205 { 206 if ($NRPEMap->{$script}{'arguments'}) 207 { 208 $nrpe_args = $NRPEMap->{$script}{'arguments'}; 209 } 210 $script = $NRPEMap->{$script}{'script'}; 211 } 212 213 # Check if the script exists and is executable. 214 if (!-e $script) 215 { 216 print STDERR "Script `$script' doesn't exist.\n"; 217 } 218 elsif (!-x $script) 219 { 220 print STDERR "Script `$script' exists but is not executable.\n"; 221 } 222 else 223 { 224 # Add the script to the global @$Script array. 225 if (ref ($opts) eq 'ARRAY') 226 { 227 for (@$opts) 228 { 229 my $opt = $_; 230 update_script_opts ($opt, $script, $nrpe_args); 231 push (@$Scripts, $opt); 232 } 233 } 234 else 235 { 236 update_script_opts ($opts, $script, $nrpe_args); 237 push (@$Scripts, $opts); 238 } 239 } 240 } # for (keys %$scripts) 241} # handle_config_script 242 243sub handle_config 244{ 245 my $config = shift; 246 247 if (defined ($config->{'nrpeconfig'})) 248 { 249 if (ref ($config->{'nrpeconfig'}) eq 'ARRAY') 250 { 251 for (@{$config->{'nrpeconfig'}}) 252 { 253 parse_nrpe_conf ($_); 254 } 255 } 256 elsif (ref ($config->{'nrpeconfig'}) eq '') 257 { 258 parse_nrpe_conf ($config->{'nrpeconfig'}); 259 } 260 else 261 { 262 print STDERR "Cannot handle ref type '" 263 . ref ($config->{'nrpeconfig'}) . "' for option 'NRPEConfig'.\n"; 264 } 265 } 266 267 if (defined ($config->{'addtype'})) 268 { 269 if (ref ($config->{'addtype'}) eq 'ARRAY') 270 { 271 handle_config_addtype ($config->{'addtype'}); 272 } 273 elsif (ref ($config->{'addtype'}) eq '') 274 { 275 handle_config_addtype ([$config->{'addtype'}]); 276 } 277 else 278 { 279 print STDERR "Cannot handle ref type '" 280 . ref ($config->{'addtype'}) . "' for option 'AddType'.\n"; 281 } 282 } 283 284 if (defined ($config->{'script'})) 285 { 286 if (ref ($config->{'script'}) eq 'HASH') 287 { 288 handle_config_script ($config->{'script'}); 289 } 290 else 291 { 292 print STDERR "Cannot handle ref type '" 293 . ref ($config->{'script'}) . "' for option 'Script'.\n"; 294 } 295 } 296 297 if (defined ($config->{'interval'}) 298 && (ref ($config->{'interval'}) eq '')) 299 { 300 my $num = int ($config->{'interval'}); 301 if ($num > 0) 302 { 303 $Interval = $num; 304 } 305 } 306} # handle_config }}} 307 308sub scale_value 309{ 310 my $value = shift; 311 my $unit = shift; 312 313 if (!$unit) 314 { 315 return ($value); 316 } 317 318 if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M')) 319 { 320 return ($value * 1000000); 321 } 322 elsif ($unit =~ m/^k(b(yte)?)?$/i) 323 { 324 return ($value * 1000); 325 } 326 327 return ($value); 328} 329 330sub sanitize_instance 331{ 332 my $inst = shift; 333 334 if ($inst eq '/') 335 { 336 return ('root'); 337 } 338 339 $inst =~ s/[^A-Za-z_-]/_/g; 340 $inst =~ s/__+/_/g; 341 $inst =~ s/^_//; 342 $inst =~ s/_$//; 343 344 return ($inst); 345} 346 347sub handle_performance_data 348{ 349 my $host = shift; 350 my $plugin = shift; 351 my $pinst = shift; 352 my $type = shift; 353 my $time = shift; 354 my $line = shift; 355 my $ident = "$host/$plugin-$pinst/$type-$tinst"; 356 357 my $tinst; 358 my $value; 359 my $unit; 360 361 if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/) 362 { 363 $tinst = sanitize_instance ($1); 364 $value = scale_value ($2, $3); 365 } 366 else 367 { 368 return; 369 } 370 371 $ident =~ s/"/\\"/g; 372 373 print qq(PUTVAL "$ident" interval=$Interval ${time}:$value\n); 374} 375 376sub execute_script 377{ 378 my $fh; 379 my $pinst; 380 my $time = time (); 381 my $script = shift; 382 my @args = (); 383 my $host = $Hostname || hostname () || 'localhost'; 384 385 my $state = 0; 386 my $serviceoutput; 387 my @serviceperfdata; 388 my @longserviceoutput; 389 390 my $script_name = $script->{'script'}; 391 392 if ($script->{'arguments'}) 393 { 394 @args = split (' ', $script->{'arguments'}); 395 } 396 397 if (!open ($fh, '-|', $script_name, @args)) 398 { 399 print STDERR "Cannot execute $script_name: $!"; 400 return; 401 } 402 403 $pinst = sanitize_instance (basename ($script_name)); 404 405 # Parse the output of the plugin. The format is seriously fucked up, because 406 # it got extended way beyond what it could handle. 407 while (my $line = <$fh>) 408 { 409 chomp ($line); 410 411 if ($state == 0) 412 { 413 my $perfdata; 414 ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2); 415 416 if ($perfdata) 417 { 418 push (@serviceperfdata, split (' ', $perfdata)); 419 } 420 421 $state = 1; 422 } 423 elsif ($state == 1) 424 { 425 my $longoutput; 426 my $perfdata; 427 ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2); 428 429 push (@longserviceoutput, $longoutput); 430 431 if ($perfdata) 432 { 433 push (@serviceperfdata, split (' ', $perfdata)); 434 $state = 2; 435 } 436 } 437 else # ($state == 2) 438 { 439 push (@serviceperfdata, split (' ', $line)); 440 } 441 } 442 443 close ($fh); 444 # Save the exit status of the check in $state 445 $state = $? >> 8; 446 447 if ($state == 0) 448 { 449 $state = 'okay'; 450 } 451 elsif ($state == 1) 452 { 453 $state = 'warning'; 454 } 455 else 456 { 457 $state = 'failure'; 458 } 459 460 { 461 my $type = $script->{'type'} || 'nagios_check'; 462 463 print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios " 464 . "plugin_instance=$pinst type=$type message=$serviceoutput\n"; 465 } 466 467 if ($script->{'type'}) 468 { 469 for (@serviceperfdata) 470 { 471 handle_performance_data ($host, 'nagios', $pinst, $script->{'type'}, 472 $time, $_); 473 } 474 } 475} # execute_script 476 477sub main 478{ 479 my $last_run; 480 my $next_run; 481 482 my %config = ParseConfig (-ConfigFile => $ConfigFile, 483 -AutoTrue => 1, 484 -LowerCaseNames => 1); 485 handle_config (\%config); 486 487 while (42) 488 { 489 $last_run = time (); 490 $next_run = $last_run + $Interval; 491 492 for (@$Scripts) 493 { 494 execute_script ($_); 495 } 496 497 while ((my $timeleft = ($next_run - time ())) > 0) 498 { 499 sleep ($timeleft); 500 } 501 } 502} # main 503 504=head1 REQUIREMENTS 505 506This script requires the following Perl modules to be installed: 507 508=over 4 509 510=item C<Config::General> 511 512=item C<Regexp::Common> 513 514=back 515 516=head1 SEE ALSO 517 518L<http://www.nagios.org/>, 519L<http://nagiosplugins.org/>, 520L<http://collectd.org/>, 521L<collectd-exec(5)> 522 523=head1 AUTHOR 524 525Florian octo Forster E<lt>octo at verplant.orgE<gt> 526 527=cut 528 529# vim: set sw=2 sts=2 ts=8 fdm=marker : 530