1 #!/usr/bin/perl
3 use strict;
4 use warnings;
6 =head1 NAME
8 exec-nagios.px
10 =head1 DESCRIPTION
12 This script allows you to use plugins that were written for Nagios with
13 collectd's C<exec-plugin>. If the plugin checks some kind of threshold, please
14 consider configuring the threshold using collectd's own facilities instead of
15 using this transition layer.
17 =cut
19 use Sys::Hostname ('hostname');
20 use File::Basename ('basename');
21 use Config::General ('ParseConfig');
22 use Regexp::Common ('number');
24 our $ConfigFile = '/etc/exec-nagios.conf';
25 our $TypeMap = {};
26 our $Scripts = [];
27 our $Interval = defined ($ENV{'COLLECTD_INTERVAL'}) ? (0 + $ENV{'COLLECTD_INTERVAL'}) : 300;
28 our $Hostname = defined ($ENV{'COLLECTD_HOSTNAME'}) ? $ENV{'COLLECTD_HOSTNAME'} : '';
30 main ();
31 exit (0);
33 # Configuration
34 # {{{
36 =head1 CONFIGURATION
38 This script reads it's configuration from F</etc/exec-nagios.conf>. The
39 configuration is read using C<Config::General> which understands a Apache-like
40 config syntax, so it's very similar to the F<collectd.conf> syntax, too.
42 Here's a short sample config:
44 Interval 300
45 <Script /usr/lib/nagios/check_tcp>
46 Arguments -H alice -p 22
47 Type delay
48 </Script>
49 <Script /usr/lib/nagios/check_dns>
50 Arguments -H alice
51 Type delay
52 </Script>
54 The options have the following semantic (i.E<nbsp>e. meaning):
56 =over 4
58 =item B<Interval> I<Seconds>
60 Sets the interval in which the plugins are executed. This doesn't need to match
61 the interval setting of the collectd daemon. Usually, you want to execute the
62 Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
63 seconds.
65 =item E<lt>B<Script> I<File>E<gt>
67 Adds a script to the list of scripts to be executed once per I<Interval>
68 seconds. You can use the following optional arguments to specify the operation
69 further:
71 =over 4
73 =item B<Arguments> I<Arguments>
75 Pass the arguments I<Arguments> to the script. This is often needed with Nagios
76 plugins, because much of the logic is implemented in the plugins, not in the
77 daemon. If you need to specify a warning and/or critical range here, please
78 consider using collectd's own threshold mechanism, which is by far the more
79 elegant solution than this transition layer.
81 =item B<Type> I<Type>
83 If the plugin provides "performance data" the performance data is dispatched to
84 collectd with this type. If no type is configured the data is ignored. Please
85 note that this is limited to types that take exactly one value, such as the
86 type C<delay> in the example above. If you need more complex performance data,
87 rewrite the plugin as a collectd plugin (or at least port it do run directly
88 with the C<exec-plugin>).
90 =back
92 =back
94 =cut
96 sub handle_config_addtype
97 {
98 my $list = shift;
100 for (my $i = 0; $i < @$list; $i++)
101 {
102 my ($to, @from) = split (' ', $list->[$i]);
103 for (my $j = 0; $j < @from; $j++)
104 {
105 $TypeMap->{$from[$j]} = $to;
106 }
107 }
108 } # handle_config_addtype
110 sub handle_config_script
111 {
112 my $scripts = shift;
114 for (keys %$scripts)
115 {
116 my $script = $_;
117 my $opts = $scripts->{$script};
119 if (!-e $script)
120 {
121 print STDERR "Script `$script' doesn't exist.\n";
122 }
123 elsif (!-x $script)
124 {
125 print STDERR "Script `$script' exists but is not executable.\n";
126 }
127 else
128 {
129 if (ref ($opts) eq 'ARRAY')
130 {
131 for (@$opts)
132 {
133 my $opt = $_;
134 $opt->{'script'} = $script;
135 push (@$Scripts, $opt);
136 }
137 }
138 else
139 {
140 $opts->{'script'} = $script;
141 push (@$Scripts, $opts);
142 }
143 }
144 } # for (keys %$scripts)
145 } # handle_config_script
147 sub handle_config
148 {
149 my $config = shift;
151 if (defined ($config->{'addtype'}))
152 {
153 if (ref ($config->{'addtype'}) eq 'ARRAY')
154 {
155 handle_config_addtype ($config->{'addtype'});
156 }
157 elsif (ref ($config->{'addtype'}) eq '')
158 {
159 handle_config_addtype ([$config->{'addtype'}]);
160 }
161 else
162 {
163 print STDERR "Cannot handle ref type '"
164 . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
165 }
166 }
168 if (defined ($config->{'script'}))
169 {
170 if (ref ($config->{'script'}) eq 'HASH')
171 {
172 handle_config_script ($config->{'script'});
173 }
174 else
175 {
176 print STDERR "Cannot handle ref type '"
177 . ref ($config->{'script'}) . "' for option 'Script'.\n";
178 }
179 }
181 if (defined ($config->{'interval'})
182 && (ref ($config->{'interval'}) eq ''))
183 {
184 my $num = int ($config->{'interval'});
185 if ($num > 0)
186 {
187 $Interval = $num;
188 }
189 }
190 } # handle_config }}}
192 sub scale_value
193 {
194 my $value = shift;
195 my $unit = shift;
197 if (!$unit)
198 {
199 return ($value);
200 }
202 if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
203 {
204 return ($value * 1000000);
205 }
206 elsif ($unit =~ m/^k(b(yte)?)?$/i)
207 {
208 return ($value * 1000);
209 }
211 return ($value);
212 }
214 sub sanitize_instance
215 {
216 my $inst = shift;
218 if ($inst eq '/')
219 {
220 return ('root');
221 }
223 $inst =~ s/[^A-Za-z_-]/_/g;
224 $inst =~ s/__+/_/g;
225 $inst =~ s/^_//;
226 $inst =~ s/_$//;
228 return ($inst);
229 }
231 sub handle_performance_data
232 {
233 my $host = shift;
234 my $plugin = shift;
235 my $pinst = shift;
236 my $type = shift;
237 my $time = shift;
238 my $line = shift;
239 my $ident = "$host/$plugin-$pinst/$type-$tinst";
241 my $tinst;
242 my $value;
243 my $unit;
245 if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
246 {
247 $tinst = sanitize_instance ($1);
248 $value = scale_value ($2, $3);
249 }
250 else
251 {
252 return;
253 }
255 $ident =~ s/"/\\"/g;
257 print qq(PUTVAL "$ident" interval=$Interval ${time}:$value\n);
258 }
260 sub execute_script
261 {
262 my $fh;
263 my $pinst;
264 my $time = time ();
265 my $script = shift;
266 my @args = ();
267 my $host = $Hostname || hostname () || 'localhost';
269 my $state = 0;
270 my $serviceoutput;
271 my @serviceperfdata;
272 my @longserviceoutput;
274 my $script_name = $script->{'script'};
276 if ($script->{'arguments'})
277 {
278 @args = split (' ', $script->{'arguments'});
279 }
281 if (!open ($fh, '-|', $script_name, @args))
282 {
283 print STDERR "Cannot execute $script_name: $!";
284 return;
285 }
287 $pinst = sanitize_instance (basename ($script_name));
289 # Parse the output of the plugin. The format is seriously fucked up, because
290 # it got extended way beyond what it could handle.
291 while (my $line = <$fh>)
292 {
293 chomp ($line);
295 if ($state == 0)
296 {
297 my $perfdata;
298 ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
300 if ($perfdata)
301 {
302 push (@serviceperfdata, split (' ', $perfdata));
303 }
305 $state = 1;
306 }
307 elsif ($state == 1)
308 {
309 my $longoutput;
310 my $perfdata;
311 ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
313 push (@longserviceoutput, $longoutput);
315 if ($perfdata)
316 {
317 push (@serviceperfdata, split (' ', $perfdata));
318 $state = 2;
319 }
320 }
321 else # ($state == 2)
322 {
323 push (@serviceperfdata, split (' ', $line));
324 }
325 }
327 close ($fh);
328 # Save the exit status of the check in $state
329 $state = $?;
331 if ($state == 0)
332 {
333 $state = 'okay';
334 }
335 elsif ($state == 1)
336 {
337 $state = 'warning';
338 }
339 else
340 {
341 $state = 'failure';
342 }
344 {
345 my $type = $script->{'type'} || 'nagios_check';
347 print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
348 . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
349 }
351 if ($script->{'type'})
352 {
353 for (@serviceperfdata)
354 {
355 handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
356 $time, $_);
357 }
358 }
359 } # execute_script
361 sub main
362 {
363 my $last_run;
364 my $next_run;
366 my %config = ParseConfig (-ConfigFile => $ConfigFile,
367 -AutoTrue => 1,
368 -LowerCaseNames => 1);
369 handle_config (\%config);
371 while (42)
372 {
373 $last_run = time ();
374 $next_run = $last_run + $Interval;
376 for (@$Scripts)
377 {
378 execute_script ($_);
379 }
381 while ((my $timeleft = ($next_run - time ())) > 0)
382 {
383 sleep ($timeleft);
384 }
385 }
386 } # main
388 =head1 REQUIREMENTS
390 This script requires the following Perl modules to be installed:
392 =over 4
394 =item C<Config::General>
396 =item C<Regexp::Common>
398 =back
400 =head1 SEE ALSO
402 L<http://www.nagios.org/>,
403 L<http://nagiosplugins.org/>,
404 L<http://collectd.org/>,
405 L<collectd-exec(5)>
407 =head1 AUTHOR
409 Florian octo Forster E<lt>octo at verplant.orgE<gt>
411 =cut
413 # vim: set sw=2 sts=2 ts=8 fdm=marker :