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 = 300;
29 main ();
30 exit (0);
32 # Configuration
33 # {{{
35 =head1 CONFIGURATION
37 This script reads it's configuration from F</etc/exec-nagios.conf>. The
38 configuration is read using C<Config::General> which understands a Apache-like
39 config syntax, so it's very similar to the F<collectd.conf> syntax, too.
41 Here's a short sample config:
43 Interval 300
44 <Script /usr/lib/nagios/check_tcp>
45 Arguments -H alice -p 22
46 Type delay
47 </Script>
48 <Script /usr/lib/nagios/check_dns>
49 Arguments -H alice
50 Type delay
51 </Script>
53 The options have the following semantic (i.E<nbsp>e. meaning):
55 =over 4
57 =item B<Interval> I<Seconds>
59 Sets the interval in which the plugins are executed. This doesn't need to match
60 the interval setting of the collectd daemon. Usually, you want to execute the
61 Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
62 seconds.
64 =item E<lt>B<Script> I<File>E<gt>
66 Adds a script to the list of scripts to be executed once per I<Interval>
67 seconds. You can use the following optional arguments to specify the operation
68 further:
70 =over 4
72 =item B<Arguments> I<Arguments>
74 Pass the arguments I<Arguments> to the script. This is often needed with Nagios
75 plugins, because much of the logic is implemented in the plugins, not in the
76 daemon. If you need to specify a warning and/or critical range here, please
77 consider using collectd's own threshold mechanism, which is by far the more
78 elegant solution than this transition layer.
80 =item B<Type> I<Type>
82 If the plugin provides "performance data" the performance data is dispatched to
83 collectd with this type. If no type is configured the data is ignored. Please
84 note that this is limited to types that take exactly one value, such as the
85 type C<delay> in the example above. If you need more complex performance data,
86 rewrite the plugin as a collectd plugin (or at least port it do run directly
87 with the C<exec-plugin>).
89 =back
91 =cut
93 sub handle_config_addtype
94 {
95 my $list = shift;
97 for (my $i = 0; $i < @$list; $i++)
98 {
99 my ($to, @from) = split (' ', $list->[$i]);
100 for (my $j = 0; $j < @from; $j++)
101 {
102 $TypeMap->{$from[$j]} = $to;
103 }
104 }
105 } # handle_config_addtype
107 sub handle_config_script
108 {
109 my $scripts = shift;
111 for (keys %$scripts)
112 {
113 my $script = $_;
114 my $opts = $scripts->{$script};
116 if (!-e $script)
117 {
118 print STDERR "Script `$script' doesn't exist.\n";
119 }
120 elsif (!-x $script)
121 {
122 print STDERR "Script `$script' exists but is not executable.\n";
123 }
124 else
125 {
126 if (ref ($opts) eq 'ARRAY')
127 {
128 for (@$opts)
129 {
130 my $opt = $_;
131 $opt->{'script'} = $script;
132 push (@$Scripts, $opt);
133 }
134 }
135 else
136 {
137 $opts->{'script'} = $script;
138 push (@$Scripts, $opts);
139 }
140 }
141 } # for (keys %$scripts)
142 } # handle_config_script
144 sub handle_config
145 {
146 my $config = shift;
148 if (defined ($config->{'addtype'}))
149 {
150 if (ref ($config->{'addtype'}) eq 'ARRAY')
151 {
152 handle_config_addtype ($config->{'addtype'});
153 }
154 elsif (ref ($config->{'addtype'}) eq '')
155 {
156 handle_config_addtype ([$config->{'addtype'}]);
157 }
158 else
159 {
160 print STDERR "Cannot handle ref type '"
161 . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
162 }
163 }
165 if (defined ($config->{'script'}))
166 {
167 if (ref ($config->{'script'}) eq 'HASH')
168 {
169 handle_config_script ($config->{'script'});
170 }
171 else
172 {
173 print STDERR "Cannot handle ref type '"
174 . ref ($config->{'script'}) . "' for option 'Script'.\n";
175 }
176 }
178 if (defined ($config->{'interval'})
179 && (ref ($config->{'interval'}) eq ''))
180 {
181 my $num = int ($config->{'interval'});
182 if ($num > 0)
183 {
184 $Interval = $num;
185 }
186 }
187 } # handle_config }}}
189 sub scale_value
190 {
191 my $value = shift;
192 my $unit = shift;
194 if (!$unit)
195 {
196 return ($value);
197 }
199 if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
200 {
201 return ($value * 1000000);
202 }
203 elsif ($unit =~ m/^k(b(yte)?)?$/i)
204 {
205 return ($value * 1000);
206 }
208 return ($value);
209 }
211 sub sanitize_instance
212 {
213 my $inst = shift;
215 if ($inst eq '/')
216 {
217 return ('root');
218 }
220 $inst =~ s/[^A-Za-z_-]/_/g;
221 $inst =~ s/__+/_/g;
222 $inst =~ s/^_//;
223 $inst =~ s/_$//;
225 return ($inst);
226 }
228 sub handle_performance_data
229 {
230 my $host = shift;
231 my $plugin = shift;
232 my $pinst = shift;
233 my $type = shift;
234 my $time = shift;
235 my $line = shift;
237 my $tinst;
238 my $value;
239 my $unit;
241 if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
242 {
243 $tinst = sanitize_instance ($1);
244 $value = scale_value ($2, $3);
245 }
246 else
247 {
248 return;
249 }
251 print "PUTVAL $host/$plugin-$pinst/$type-$tinst interval=$Interval ${time}:$value\n";
252 }
254 sub execute_script
255 {
256 my $fh;
257 my $pinst;
258 my $time = time ();
259 my $script = shift;
260 my @args = ();
261 my $host = hostname () || 'localhost';
263 my $state = 0;
264 my $serviceoutput;
265 my @serviceperfdata;
266 my @longserviceoutput;
268 my $script_name = $script->{'script'};
270 if ($script->{'arguments'})
271 {
272 @args = split (' ', $script->{'arguments'});
273 }
275 if (!open ($fh, '-|', $script_name, @args))
276 {
277 print STDERR "Cannot execute $script_name: $!";
278 return;
279 }
281 $pinst = sanitize_instance (basename ($script_name));
283 # Parse the output of the plugin. The format is seriously fucked up, because
284 # it got extended way beyond what it could handle.
285 while (my $line = <$fh>)
286 {
287 chomp ($line);
289 if ($state == 0)
290 {
291 my $perfdata;
292 ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
294 if ($perfdata)
295 {
296 push (@serviceperfdata, split (' ', $perfdata));
297 }
299 $state = 1;
300 }
301 elsif ($state == 1)
302 {
303 my $longoutput;
304 my $perfdata;
305 ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
307 push (@longserviceoutput, $longoutput);
309 if ($perfdata)
310 {
311 push (@serviceperfdata, split (' ', $perfdata));
312 $state = 2;
313 }
314 }
315 else # ($state == 2)
316 {
317 push (@serviceperfdata, split (' ', $line));
318 }
319 }
321 close ($fh);
322 # Save the exit status of the check in $state
323 $state = $?;
325 if ($state == 0)
326 {
327 $state = 'okay';
328 }
329 elsif ($state == 1)
330 {
331 $state = 'warning';
332 }
333 else
334 {
335 $state = 'failure';
336 }
338 {
339 my $type = $script->{'type'} || 'nagios_check';
341 print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
342 . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
343 }
345 if ($script->{'type'})
346 {
347 for (@serviceperfdata)
348 {
349 handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
350 $time, $_);
351 }
352 }
353 } # execute_script
355 sub main
356 {
357 my $last_run;
358 my $next_run;
360 my %config = ParseConfig (-ConfigFile => $ConfigFile,
361 -AutoTrue => 1,
362 -LowerCaseNames => 1);
363 handle_config (\%config);
365 while (42)
366 {
367 $last_run = time ();
368 $next_run = $last_run + $Interval;
370 for (@$Scripts)
371 {
372 execute_script ($_);
373 }
375 while ((my $timeleft = ($next_run - time ())) > 0)
376 {
377 sleep ($timeleft);
378 }
379 }
380 } # main
382 =head1 REQUIREMENTS
384 This script requires the following Perl modules to be installed:
386 =over 4
388 =item C<Config::General>
390 =item C<Regexp::Common>
392 =back
394 =head1 SEE ALSO
396 L<http://www.nagios.org/>,
397 L<http://nagiosplugins.org/>,
398 L<http://collectd.org/>,
399 L<collectd-exec(5)>
401 =head1 AUTHOR
403 Florian octo Forster E<lt>octo at verplant.orgE<gt>
405 =cut
407 # vim: set sw=2 sts=2 ts=8 fdm=marker :