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 $opts->{'script'} = $script;
127 push (@$Scripts, $opts);
128 }
129 } # for (keys %$scripts)
130 } # handle_config_script
132 sub handle_config
133 {
134 my $config = shift;
136 if (defined ($config->{'addtype'}))
137 {
138 if (ref ($config->{'addtype'}) eq 'ARRAY')
139 {
140 handle_config_addtype ($config->{'addtype'});
141 }
142 elsif (ref ($config->{'addtype'}) eq '')
143 {
144 handle_config_addtype ([$config->{'addtype'}]);
145 }
146 else
147 {
148 print STDERR "Cannot handle ref type '"
149 . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
150 }
151 }
153 if (defined ($config->{'script'}))
154 {
155 if (ref ($config->{'script'}) eq 'HASH')
156 {
157 handle_config_script ($config->{'script'});
158 }
159 else
160 {
161 print STDERR "Cannot handle ref type '"
162 . ref ($config->{'script'}) . "' for option 'Script'.\n";
163 }
164 }
166 if (defined ($config->{'interval'})
167 && (ref ($config->{'interval'}) eq ''))
168 {
169 my $num = int ($config->{'interval'});
170 if ($num > 0)
171 {
172 $Interval = $num;
173 }
174 }
175 } # handle_config }}}
177 sub scale_value
178 {
179 my $value = shift;
180 my $unit = shift;
182 if (!$unit)
183 {
184 return ($value);
185 }
187 if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
188 {
189 return ($value * 1000000);
190 }
191 elsif ($unit =~ m/^k(b(yte)?)?$/i)
192 {
193 return ($value * 1000);
194 }
196 return ($value);
197 }
199 sub sanitize_instance
200 {
201 my $inst = shift;
203 if ($inst eq '/')
204 {
205 return ('root');
206 }
208 $inst =~ s/[^A-Za-z_-]/_/g;
209 $inst =~ s/__+/_/g;
210 $inst =~ s/^_//;
211 $inst =~ s/_$//;
213 return ($inst);
214 }
216 sub handle_performance_data
217 {
218 my $host = shift;
219 my $plugin = shift;
220 my $pinst = shift;
221 my $type = shift;
222 my $time = shift;
223 my $line = shift;
225 my $tinst;
226 my $value;
227 my $unit;
229 if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
230 {
231 $tinst = sanitize_instance ($1);
232 $value = scale_value ($2, $3);
233 }
234 else
235 {
236 return;
237 }
239 print "PUTVAL $host/$plugin-$pinst/$type-$tinst interval=$Interval ${time}:$value\n";
240 }
242 sub execute_script
243 {
244 my $fh;
245 my $pinst;
246 my $time = time ();
247 my $script = shift;
248 my @args = ();
249 my $host = hostname () || 'localhost';
251 my $state = 0;
252 my $serviceoutput;
253 my @serviceperfdata;
254 my @longserviceoutput;
256 my $script_name = $script->{'script'};
258 if ($script->{'arguments'})
259 {
260 @args = split (' ', $script->{'arguments'});
261 }
263 if (!open ($fh, '-|', $script_name, @args))
264 {
265 print STDERR "Cannot execute $script_name: $!";
266 return;
267 }
269 $pinst = sanitize_instance (basename ($script_name));
271 # Parse the output of the plugin. The format is seriously fucked up, because
272 # it got extended way beyond what it could handle.
273 while (my $line = <$fh>)
274 {
275 chomp ($line);
277 if ($state == 0)
278 {
279 my $perfdata;
280 ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
282 if ($perfdata)
283 {
284 push (@serviceperfdata, split (' ', $perfdata));
285 }
287 $state = 1;
288 }
289 elsif ($state == 1)
290 {
291 my $longoutput;
292 my $perfdata;
293 ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
295 push (@longserviceoutput, $longoutput);
297 if ($perfdata)
298 {
299 push (@serviceperfdata, split (' ', $perfdata));
300 $state = 2;
301 }
302 }
303 else # ($state == 2)
304 {
305 push (@serviceperfdata, split (' ', $line));
306 }
307 }
309 close ($fh);
310 # Save the exit status of the check in $state
311 $state = $?;
313 if ($state == 0)
314 {
315 $state = 'okay';
316 }
317 elsif ($state == 1)
318 {
319 $state = 'warning';
320 }
321 else
322 {
323 $state = 'failure';
324 }
326 {
327 my $type = $script->{'type'} || 'nagios_check';
329 print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
330 . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
331 }
333 if ($script->{'type'})
334 {
335 for (@serviceperfdata)
336 {
337 handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
338 $time, $_);
339 }
340 }
341 } # execute_script
343 sub main
344 {
345 my $last_run;
346 my $next_run;
348 my %config = ParseConfig (-ConfigFile => $ConfigFile,
349 -AutoTrue => 1,
350 -LowerCaseNames => 1);
351 handle_config (\%config);
353 while (42)
354 {
355 $last_run = time ();
356 $next_run = $last_run + $Interval;
358 for (@$Scripts)
359 {
360 execute_script ($_);
361 }
363 while ((my $timeleft = ($next_run - time ())) > 0)
364 {
365 sleep ($timeleft);
366 }
367 }
368 } # main
370 =head1 REQUIREMENTS
372 This script requires the following Perl modules to be installed:
374 =over 4
376 =item C<Config::General>
378 =item C<Regexp::Common>
380 =back
382 =head1 SEE ALSO
384 L<http://www.nagios.org/>,
385 L<http://nagiosplugins.org/>,
386 L<http://collectd.org/>,
387 L<collectd-exec(5)>
389 =head1 AUTHOR
391 Florian octo Forster E<lt>octo at verplant.orgE<gt>
393 =cut
395 # vim: set sw=2 sts=2 ts=8 fdm=marker :