#!/usr/bin/perl # -*- coding: None -*- # vim: sw=4 sts=4: # zabbix_multichecker: Execute and parse the output of a command and return # some selected values as Zabbix items to the Zabbix server. A configuration # file is used to determind the commands to execute and the items to return # for each command. # # Date: 2007/05/25 # Version: 2.1 # # Copyright (C) 2007 Farzad FARID # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # TODO: # - Support multiple items per regular expression. For this we need to correctly parse # number of parentheses in the regex (or count the number of item names on the same # line in the configuration file) and provide the corresponding $1, $2, $3... use warnings; use strict; use Data::Dumper; use Getopt::Long; use Pod::Usage; # Default Zabbix Agentd and Check configuration files my $default_agentd_conf = "/etc/zabbix/zabbix_agentd.conf"; my $default_server_conf = "/etc/zabbix/zabbix_server.conf"; my $default_multichecker_conf = "/etc/zabbix/zbx_multicheck.conf"; # These default values are taken from the default zabbix configuration files # or are reasonnable defaults my $default_server = "127.0.0.1"; my $default_sport = "10051"; my $sender_bin = "/usr/local/bin/zabbix_sender"; #---------------------------------------------- # Do not change anything below this line # --------------------------------------------- my $VERSION="2.0"; my $agentd_conf = $default_agentd_conf; my $server_conf = $default_server_conf; my $multichecker_conf = $default_multichecker_conf; my $man = 0; my $help = 0; my $debug = 0; my $show_version = 0; my $hostname = undef; # First parameter of the command line arguments my $zbx_config; # Print debug messages, only if $debug > 0 sub debug { return if !$debug; my $msg = $_[0]; # Get some info on the calling procedure. A call from main is a special # case. my @info = caller(1); my $subroutine_name = $info[3] || "main"; foreach my $line (split(/\n/, $msg)) { chomp($line); print STDERR "DEBUG: ($subroutine_name) $line\n"; } } # This function is called if this script is running on the Zabbix Agent. # - Parse the Zabbix Agentd configuration file to get the local hostname, # the server IP or name and the server port. This way, we don't have to # redefine them. # - Return value: # A array { hostname, servername, serverport } sub parse_agentd_conf { my $conffile = $_[0]; # Some parameters can have a default value my ($server, $sport, $hostname) = ($default_server, $default_sport, undef); debug("Parsing main Zabbix Agentd configuration file '$conffile'"); open (my $fh, "<$conffile") or die "ERROR: Can't open Zabbix agentd's configuration file '$conffile'"; while (<$fh>) { $server = $1 if (m/^\s*Server\s*=\s*([^\s,]*)/); $sport = $1 if (m/^\s*ServerPort\s*=\s*(.*)/); $hostname = $1 if (m/^\s*Hostname\s*=\s*(.*)/); } close($fh); # Hostname must be defined, we can't guess a default value if (!defined($hostname)) { die("ERROR: Hostname must be defined in the Zabbix Agentd configuration file"); } my $result = {"hostname" => $hostname, "servername" => $server, "serverport" => $sport}; debug("Parse result for Zabbix Agentd = " . Dumper($result)); return $result; } # This function is called if this script is running on the Zabbix Server as # an External Script, on behalf of a distant Zabbix Agent. # - Parse the Zabbix Server configuration file to get the local # server IP, and the server port. This way, we don't have to # redefine them. Note that we need to further add "hostname" to the array # returned by this function. This hostname was provided on the command line. # - Return value: # A array { servername, serverport } sub parse_server_conf { my $conffile = $_[0]; # Some parameters can have a default value my ($server, $sport, $hostname) = ($default_server, $default_sport, undef); debug("Parsing main Zabbix Server configuration file '$conffile'"); open (my $fh, "<$conffile") or die "ERROR: Can't open Zabbix server's configuration file '$conffile'"; while (<$fh>) { $server = $1 if (m/^\s*ListenIP\s*=\s*([^\s,]*)/); $sport = $1 if (m/^\s*ListenPort\s*=\s*(.*)/); } close($fh); # Note that hostname is still be defined and must be completed later. my $result = {"hostname" => $hostname, "servername" => $server, "serverport" => $sport}; debug("Parse result for Zabbix Server = " . Dumper($result)); return $result; } # Parse the Zabbix Multichecker configuration file. This file contains: # - the command to execute. It will be run in a shell and its ouput will # then be parsed. # - A list of regular expressions and their corresponding items. # Return value: # Return a hash of commands, each of which contains a hash of regexp -> items # # For example: # $commands = { # 'sudo mysqladmin status' => { # 'Queries per second avg: (\\d+\\.?\\d*)' => 'mysql.qps', # 'Threads: (\\d+)' => 'mysql.threads', # 'Slow queries: (\\d+)' => 'mysql.slowqueries', # 'Questions: (\\d+)' => 'mysql.questions', # 'Uptime: (\\d+)' => 'mysql.uptime' # }, # 'sudo mysqladmin version' => { # '^Server version\\s*(.*)\\s*$' => 'mysql.server-version', # '^Protocol version\\s*(.*)\\s*$' => 'mysql.protocol-version' # } # }; # # TODO: # - Does not accept the [] syntax in the item name sub parse_multichecker_conf { my $conffile = $_[0]; my $commands = {}; my $command = undef; debug("Parsing Multichecker configuration file '$conffile'"); open (my $fh, "<$conffile") or die "ERROR: Can't open Zabbix Multichecker's configuration file '$conffile'"; while (<$fh>) { chomp; if (m/^\s*(#|$)/) { # Comment or blank line next; } elsif (m/^\s*command\s*=\s*(.*)\s*$/) { # Parse command line, which must only be defined once $command = $1; if (exists($commands->{$command})) { die("ERROR: Command line '$command' redefined in multichecker configuration file '$conffile'"); } $commands->{$command} = {}; } elsif (m/^\s*item\s*=\s*\/(.*)\/\s*,\s*([[:alnum:]._-]+)\s*$/) { # Parse regexp/item line if (!defined($command)) { die("ERROR: Item '$_' found before command definition in multichecker configuration file '$conffile'"); } $commands->{$command}->{$1} = $2; } else { # Unparseable line die "ERROR: Syntax error in multichecker configuration file '$conffile': \"$_\""; } } close($fh); if (!scalar($commands)) { die "ERROR: Missing command and/or item lines in multichecker configuration file '$conffile'"; } debug("Parse result = " . Dumper($commands)); return $commands; } # Parse the command line # The first argument must be "localhost" (we're running on a zabbix agent) or # a machine name (we're running on the zabbix server) as an External Script. $hostname = shift(@ARGV) if (scalar(@ARGV) and $ARGV[0] !~ m/^-/); # Parse the remaining arguments normally Getopt::Long::Configure(qw(gnu_getopt no_ignore_case)); GetOptions('help|h|?' => \$help, man => \$man, 'debug|d' => \$debug, 'version|v' => \$show_version, 'agent-conf|A=s' => \$agentd_conf, 'server-conf|C=s' => \$server_conf, 'multichecker-conf|M=s' => \$multichecker_conf, 'sender|S=s' => \$sender_bin) or pod2usage(2); pod2usage(1) if $help; pod2usage(-exitstatus => 0, -verbose => 2) if $man; if ($show_version) { print < This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA EOFHELP exit 1; } # Hostname must be provided if (! defined($hostname)) { pod2usage(-message => "ERROR: HOSTNAME must be the first argument", -exitval => 3); } # zabbix_sender must be available if (! -r $sender_bin) { pod2usage(-message => "ERROR: Invalid 'zabbix_sender' path '$sender_bin'", -exitval => 3); } # Parse the two configuration files if ($hostname eq 'localhost') { debug("We're running on a Zabbix Agent"); $zbx_config = &parse_agentd_conf($agentd_conf); } else { debug("We're running on the Zabbix Server"); $zbx_config = &parse_server_conf($server_conf); debug("Adding hostname '$hostname' to the configuration variables"); $zbx_config->{hostname} = $hostname; } my $multiconfig = &parse_multichecker_conf($multichecker_conf); # Execute the commands while (my ($command, $items) = each(%$multiconfig)) { debug("#### Command to execute before hostname substitution: '$command' ####"); # Remplace @HOSTNAME@ by the name of the server, if needed $command =~ s/\@HOSTNAME\@/$zbx_config->{hostname}/ge; debug("#### Executing '$command' ####"); my $result = `$command`; debug("==== Command result ===="); debug($result); debug("==== End command result ===="); # And now parse the output and compare to regular expressions debug("==== Parsing output and generating items ===="); while (my ($reg,$item) = each(%$items)) { debug(" === Matching against '$reg' ==="); if ($result =~ m/$reg/m) { debug(" == Found a match! Setting the item '$item' to '$1' =="); # Now send the result back to the Zabbix Server my $send_cmd = "$sender_bin --zabbix-server $zbx_config->{servername} --port $zbx_config->{serverport} --host $zbx_config->{hostname} --key '$item' --value '$1'"; debug(" = Send command is : $send_cmd"); `$send_cmd` if (!$debug); } } debug("==== End output parsing ===="); } print "OK\n"; exit 0; __END__ =head1 NAME zabbix_multichecker - A tool used to execute a serie of Unix commands and then parse the command's output to create a number of Zabbix C. It then uses C to send the items back to the Zabbix server. =head1 SYNOPSIS zbx_multicheck HOSTNAME [options] B must be 'localhost' if the script is running as a UserParameter on a Zabbix Agent. If running on the Zabbix Server, B must be the name of the agent on behalf of which we're running this script (as an External Script). Options: --agent-conf=FILE, -A FILE Path to the Zabbix Agentd configuration file --server-conf=FILE, -C FILE Path to the Zabbix Server configuration file --multichecker-conf=FILE, -M FILE Path to zabbix_multichecker's configuration file --sender=FILE, -S FILE Path of the zabbix_sender executable --debug, -d Print more information (do not use in production!) --help|-h Brief help message --version|-v Print version and license --man Full documentation =head1 OPTIONS =over 5 =item B<--agent-conf=I|-A I> Path of the Zabbix Agentd configuration file. This file is parsed to get the serveur name and port, and the local hostname. This parameter is only used if HOSTNAME is equal to 'localhost'. Defaults to 'F'. =item B<--server-conf=I|-C I> Path of the Zabbix Server configuration file. This file is parsed to get the serveur name and port. This parameter is only used if HOSTNAME is not equal to 'localhost'. Defaults to 'F'. =item B<--multichecker-conf=I|-M I> Path of zabbix_multichecker's configuration file. The file contains the commands to execute and the list of regexp/items for each command. Defaults to 'F'. =item B<--sender=I|-S I> Path of the C executable. Defaults to 'F'. =item B<--debug|-d> Print more information on the execution of the script. Do not use in production as it disturb the output processing! =item B<--help|-h> Prints a brief help message and exits. =item B<--man> Prints the manual page and exists. =item B<--version|-v> Print program version and license. =back =head1 DESCRIPTION B parses a specific configuration file and executes in a shell the defined command. A list of regular expression is then executed on the output, and each corresponding match generates an item and its corresponding value. The items are then returned back to the server with the B program. This script can either run as a B on the agent (in which case the first parameter must explicitely be 'F'), or as an B on the server (in which case the first parameter is automatically set to the agent we're monitoring). =head1 SAMPLE CONFIGURATION Every occurrence of B<@HOSTNAME@> in the command line will be replaced by the name of the computer for which we are calculating items. Sample F: # First command to execute and whose output we are going to parse command = sudo mysqladmin status # Each line defines a regular expression and an item corresponding to # the first group of parentheses in the regexp. item = /Uptime: (\d+)/, mysql.uptime item = /Threads: (\d+)/, mysql.threads item = /Questions: (\d+)/, mysql.questions item = /Slow queries: (\d+)/, mysql.slowqueries item = /Queries per second avg: (\d+\.?\d*)/, mysql.qps # Second command and its set of items command = sudo mysqladmin version item = /^Server version\s*(.*)\s*$/, mysql.server-version item = /^Protocol version\s*(.*)\s*$/, mysql.protocol-version =head1 AUTHOR Author: Farzad FARID For more information or support check http://www.pragmatic-source.com/ . =cut