| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580 |
- #!/usr/bin/perl
- #
- # DESCRIPTION: Nagios plugin for checking the status of bonded network
- # interfaces (masters and slaves) on Linux servers.
- #
- # AUTHOR: Trond H. Amundsen <t.h.amundsen@usit.uio.no>
- #
- # Copyright (C) 2009-2014 Trond H. Amundsen
- #
- # 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 <http://www.gnu.org/licenses/>.
- #
- use strict;
- use warnings;
- use POSIX qw(isatty);
- use Getopt::Long qw(:config no_ignore_case);
- # Global (package) variables used throughout the code
- use vars qw( $NAME $VERSION $AUTHOR $CONTACT $E_OK $E_WARNING $E_CRITICAL
- $E_UNKNOWN $USAGE $HELP $LICENSE $linebreak $counter $exit_code
- %opt %reverse_exitcode %text2exit %bonding %nagios_level_count
- @perl_warnings @reports @blacklist @ok_reports
- );
- #---------------------------------------------------------------------
- # Initialization and global variables
- #---------------------------------------------------------------------
- # Collect perl warnings in an array
- $SIG{__WARN__} = sub { push @perl_warnings, [@_]; };
- # Version and similar info
- $NAME = 'check_linux_bonding';
- $VERSION = '1.4';
- $AUTHOR = 'Trond H. Amundsen';
- $CONTACT = 't.h.amundsen@usit.uio.no';
- # Exit codes
- $E_OK = 0;
- $E_WARNING = 1;
- $E_CRITICAL = 2;
- $E_UNKNOWN = 3;
- # Nagios error levels reversed
- %reverse_exitcode
- = (
- 0 => 'OK',
- 1 => 'WARNING',
- 2 => 'CRITICAL',
- 3 => 'UNKNOWN',
- );
- # Usage text
- $USAGE = <<"END_USAGE";
- Usage: $NAME [OPTION]...
- END_USAGE
- # Help text
- $HELP = <<'END_HELP';
- OPTIONS:
- -t, --timeout Plugin timeout in seconds [5]
- -s, --state Prefix alerts with alert state
- -S, --short-state Prefix alerts with alert state abbreviated
- -n, --no-bonding Alert level if no bonding interfaces found [ok]
- --slave-down Alert level if a slave is down [warning]
- --disable-sysfs Don't use sysfs (default), use procfs
- --ignore-num-ad (IEEE 802.3ad) Don't warn if num_ad_ports != num_slaves
- -b, --blacklist Blacklist failed interfaces
- -v, --verbose Debug/Verbose output, reports everything
- -h, --help Display this help text
- -V, --version Display version info
- For more information and advanced options, see the manual page or URL:
- http://folk.uio.no/trondham/software/check_linux_bonding.html
- END_HELP
- # Version and license text
- $LICENSE = <<"END_LICENSE";
- $NAME $VERSION
- Copyright (C) 2009-2014 $AUTHOR
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law.
- Written by $AUTHOR <$CONTACT>
- END_LICENSE
- # Options with default values
- %opt
- = ( 'timeout' => 5, # default timeout is 5 seconds
- 'help' => 0,
- 'version' => 0,
- 'blacklist' => [],
- 'no_bonding' => 'ok',
- 'state' => 0,
- 'shortstate' => 0,
- 'linebreak' => undef,
- 'verbose' => 0,
- 'disable_sysfs' => 0,
- 'slave_down' => 'warning',
- 'ignore_num_ad' => 0,
- );
- # Get options
- GetOptions('t|timeout=i' => \$opt{timeout},
- 'h|help' => \$opt{help},
- 'V|version' => \$opt{version},
- 'b|blacklist=s' => \@{ $opt{blacklist} },
- 'n|no-bonding=s' => \$opt{no_bonding},
- 's|state' => \$opt{state},
- 'S|short-state' => \$opt{shortstate},
- 'linebreak=s' => \$opt{linebreak},
- 'v|verbose' => \$opt{verbose},
- 'disable-sysfs' => \$opt{disable_sysfs},
- 'slave-down=s' => \$opt{slave_down},
- 'ignore-num-ad' => \$opt{ignore_num_ad},
- ) or do { print $USAGE; exit $E_UNKNOWN };
- # If user requested help
- if ($opt{'help'}) {
- print $USAGE, $HELP;
- exit $E_OK;
- }
- # If user requested version info
- if ($opt{'version'}) {
- print $LICENSE;
- exit $E_OK;
- }
- # Reports (messages) are gathered in this array
- @reports = ();
- # Setting timeout
- $SIG{ALRM} = sub {
- print "PLUGIN TIMEOUT: $NAME timed out after $opt{timeout} seconds\n";
- exit $E_UNKNOWN;
- };
- alarm $opt{timeout};
- # Default line break
- $linebreak = isatty(*STDOUT) ? "\n" : '<br/>';
- # Line break from option
- if (defined $opt{linebreak}) {
- if ($opt{linebreak} eq 'REG') {
- $linebreak = "\n";
- }
- elsif ($opt{linebreak} eq 'HTML') {
- $linebreak = '<br/>';
- }
- else {
- $linebreak = $opt{linebreak};
- }
- }
- # Blacklisted interfaces
- @blacklist = defined $opt{blacklist} ? @{ get_blacklist() } : ();
- # Translate text exit codes to values
- %text2exit
- = ( 'ok' => $E_OK,
- 'warning' => $E_WARNING,
- 'critical' => $E_CRITICAL,
- 'unknown' => $E_UNKNOWN,
- );
- # Check syntax of '--no-bonding' option
- if (!exists $text2exit{$opt{no_bonding}}) {
- unknown_error("Wrong usage of '--no-bonding' option: '"
- . $opt{no_bonding}
- . "' is not a recognized keyword");
- }
- # Check syntax of '--slave-down' option
- if (!exists $text2exit{$opt{slave_down}}) {
- unknown_error("Wrong usage of '--slave-down' option: '"
- . $opt{slave_down}
- . "' is not a recognized keyword");
- }
- #---------------------------------------------------------------------
- # Functions
- #---------------------------------------------------------------------
- #
- # Store a message in the message array
- #
- sub report {
- my ($msg, $exval) = @_;
- return push @reports, [ $msg, $exval ];
- }
- #
- # Give an error and exit with unknown state
- #
- sub unknown_error {
- my $msg = shift;
- print "ERROR: $msg\n";
- exit $E_UNKNOWN;
- }
- #
- # Read the blacklist option and return a hash containing the
- # blacklisted components
- #
- sub get_blacklist {
- my @bl = ();
- my @blacklist = ();
- if (scalar @{ $opt{blacklist} } >= 0) {
- foreach my $black (@{ $opt{blacklist} }) {
- my $tmp = q{};
- if (-f $black) {
- open my $BL, '<', $black
- or do { report('other', "Couldn't open blacklist file $black: $!", $E_UNKNOWN)
- and return {} };
- chomp($tmp = <$BL>);
- close $BL;
- }
- else {
- $tmp = $black;
- }
- push @bl, $tmp;
- }
- }
- return [] if $#bl < 0;
- # Parse blacklist string, put in hash
- foreach my $black (@bl) {
- push @blacklist, split m{,}xms, $black;
- }
- return \@blacklist;
- }
- #
- # Find bonding interfaces using sysfs
- #
- sub find_bonding_sysfs {
- my $sysdir = '/sys/class/net';
- my $masters_file = "$sysdir/bonding_masters";
- my @bonds = ();
- my %bonding = ();
- if (! -f $masters_file) {
- return {};
- }
- # get bonding masters
- open my $MASTER, '<', $masters_file
- or unknown_error("Couldn't open $masters_file: $!");
- @bonds = split m{\s+}xms, <$MASTER>;
- close $MASTER;
- foreach my $bond (@bonds) {
- # get bonding mode
- open my $MODE, '<', "$sysdir/$bond/bonding/mode"
- or unknown_error("ERROR: Couldn't open $sysdir/$bond/bonding/mode: $!");
- my ($mode, $nr) = split m/\s+/xms, <$MODE>;
- close $MODE;
- $bonding{$bond}{mode} = "mode=$nr ($mode)";
- # get 802.3ad number of ports
- if ($bonding{$bond}{mode} eq 'mode=4 (802.3ad)') {
- open my $AD_NUM, '<', "$sysdir/$bond/bonding/ad_num_ports"
- or unknown_error("ERROR: Couldn't open $sysdir/$bond/bonding/ad_num_ports: $!");
- my $ad_num = <$AD_NUM>;
- close $AD_NUM;
- $bonding{$bond}{ad_num} = $ad_num;
- }
- # get slaves
- my @slaves = ();
- open my $SLAVES, '<', "$sysdir/$bond/bonding/slaves"
- or unknown_error("Couldn't open $sysdir/$bond/bonding/slaves: $!");
- @slaves = split m/\s+/xms, <$SLAVES>;
- close $SLAVES;
- # get active slave
- open my $ACTIVE, '<', "$sysdir/$bond/bonding/active_slave"
- or unknown_error("Couldn't open $sysdir/$bond/bonding/active_slave: $!");
- $bonding{$bond}{active} = <$ACTIVE>;
- close $ACTIVE;
- if (defined $bonding{$bond}{active}) {
- chop $bonding{$bond}{active};
- }
- # get primary slave
- open my $PRIMARY, '<', "$sysdir/$bond/bonding/primary"
- or unknown_error("Couldn't open $sysdir/$bond/bonding/primary: $!");
- $bonding{$bond}{primary} = <$PRIMARY>;
- close $PRIMARY;
- if (defined $bonding{$bond}{primary}) {
- chop $bonding{$bond}{primary};
- }
- # get slave status
- foreach my $slave (@slaves) {
- my $statefile = -e "$sysdir/$bond/slave_$slave/operstate"
- ? "$sysdir/$bond/slave_$slave/operstate"
- : "$sysdir/$bond/lower_$slave/operstate";
- open my $STATE, '<', "$statefile"
- or unknown_error("Couldn't open $statefile: $!");
- chop($bonding{$bond}{slave}{$slave} = <$STATE>);
- close $STATE;
- }
- # get bond state
- open my $BSTATE, '<', "$sysdir/$bond/operstate"
- or unknown_error("Couldn't open $sysdir/$bond/operstate: $!");
- chop($bonding{$bond}{status} = <$BSTATE>);
- close $BSTATE;
- }
- return \%bonding;
- }
- #
- # Find bonding interfaces using procfs (fallback, deprecated)
- #
- sub find_bonding_procfs {
- my $procdir = '/proc/net/bonding';
- my @bonds = ();
- my %bonding = ();
- opendir(my $DIR, $procdir);
- @bonds = grep { m{\A bond\d+ \z}xms && -f "$procdir/$_" } readdir $DIR;
- closedir $DIR;
- if ($#bonds == -1) {
- return {};
- }
- foreach my $b (@bonds) {
- my $slave = undef;
- open my $BOND, '<', "$procdir/$b"
- or unknown_error("Couldn't open $procdir/$b: $!");
- while (<$BOND>) {
- # get bonding mode
- if (m{\A Bonding \s Mode: \s (.+) \z}xms) {
- chop($bonding{$b}{mode} = $1);
- }
- # get 802.3ad number of ports
- elsif (defined $bonding{$b}{mode} and $bonding{$b}{mode} =~ m{802\.3ad}xms
- and m{\A\s+ Number \s of \s ports: \s (\d+) .*\z}xms) {
- chomp($bonding{$b}{ad_num} = $1);
- }
- # get slave
- elsif (m{\A Slave \s Interface: \s (.+) \z}xms) {
- chop($slave = $1);
- }
- # get slave and bonding status
- elsif (m{\A MII \s Status: \s (.+) \z}xms) {
- if (defined $slave) {
- chop($bonding{$b}{slave}{$slave} = $1);
- }
- else {
- chop($bonding{$b}{status} = $1);
- }
- }
- # get primary slave
- elsif (m{\A Primary \s Slave: \s (\S+) .* \z}xms) {
- chomp($bonding{$b}{primary} = $1);
- }
- # get active slave
- elsif (m{\A Currently \s Active \s Slave: \s (.+) \z}xms) {
- chop($bonding{$b}{active} = $1);
- }
- }
- }
- return \%bonding;
- }
- #
- # Find bonding interfaces
- #
- sub find_bonding {
- my $bonding = undef;
- if ($opt{disable_sysfs}) {
- $bonding = find_bonding_procfs();
- }
- else {
- # first try sysfs
- $bonding = find_bonding_sysfs();
- # second try procfs
- if (scalar keys %{ $bonding } == 0) {
- $bonding = find_bonding_procfs();
- }
- }
- # if no bonding interfaces found, exit
- if (scalar keys %{ $bonding } == 0) {
- print $reverse_exitcode{$text2exit{$opt{no_bonding}}}
- . ": No bonding interfaces found\n";
- exit $text2exit{$opt{no_bonding}};
- }
- return $bonding;
- }
- #
- # Returns true if an interface is blacklisted
- #
- sub blacklisted {
- return 0 if !defined $opt{blacklist};
- my $if = shift;
- foreach $b (@blacklist) {
- if ($if eq $b) {
- return 1;
- }
- }
- return 0;
- }
- #=====================================================================
- # Main program
- #=====================================================================
- %bonding = %{ find_bonding() };
- MASTER:
- foreach my $b (sort keys %bonding) {
- # If the master interface is blacklisted
- if (blacklisted($b)) {
- my $msg = sprintf 'Bonding interface %s [%s] is %s, but IGNORED',
- $b, $bonding{$b}{mode}, $bonding{$b}{status};
- report($msg, $E_OK);
- next MASTER;
- }
- if ($bonding{$b}{status} ne 'up') {
- my $msg = sprintf 'Bonding interface %s [%s] is %s',
- $b, $bonding{$b}{mode}, $bonding{$b}{status};
- report($msg, $E_CRITICAL);
- }
- else {
- my $slaves_are_up = 1; # flag
- SLAVE:
- foreach my $i (sort keys %{ $bonding{$b}{slave} }) {
- # If the slave interface is blacklisted
- if (blacklisted($i)) {
- my $msg = sprintf 'Slave interface %s [member of %s] is %s, but IGNORED',
- $i, $b, $bonding{$b}{slave}{$i};
- report($msg, $E_OK);
- next SLAVE;
- }
- if ($bonding{$b}{slave}{$i} ne 'up') {
- $slaves_are_up = 0; # not all slaves are up
- my $msg = sprintf 'Bonding interface %s [%s]: Slave %s is %s',
- $b, $bonding{$b}{mode}, $i, $bonding{$b}{slave}{$i};
- report($msg, $text2exit{$opt{slave_down}});
- }
- }
- if ($slaves_are_up) {
- my %slave = map { $_ => q{} } keys %{ $bonding{$b}{slave} };
- foreach my $s (keys %slave) {
- if (defined $bonding{$b}{primary} and $bonding{$b}{primary} eq $s) {
- $slave{$s} .= '*';
- }
- if (defined $bonding{$b}{active} and $bonding{$b}{active} eq $s) {
- $slave{$s} .= '!';
- }
- }
- if (scalar keys %slave == 1) {
- my @slaves = keys %slave;
- my $msg = sprintf 'Bonding interface %s [%s] has only one slave (%s)',
- $b, $bonding{$b}{mode}, $slaves[0];
- report($msg, $E_WARNING);
- }
- elsif (scalar keys %slave == 0) { # FIXME: does this ever happen?
- my $msg = sprintf 'Bonding interface %s [%s] has zero slaves!',
- $b, $bonding{$b}{mode};
- report($msg, $E_CRITICAL);
- }
- elsif (defined $bonding{$b}{ad_num} and $bonding{$b}{ad_num} != scalar keys %slave
- and $opt{ignore_num_ad} == 0) {
- my $msg = sprintf 'Bonding interface %s [%s]: Number of AD ports (%d) does not equal the number of slaves (%d)',
- $b, $bonding{$b}{mode}, $bonding{$b}{ad_num}, scalar keys %slave;
- report($msg, $E_WARNING);
- }
- else {
- my @slaves = map { $_ . $slave{$_} } sort keys %slave;
- my $msg = sprintf 'Interface %s is %s: %s, %d slaves: %s',
- $b, $bonding{$b}{status}, $bonding{$b}{mode},
- scalar @slaves, join q{, }, @slaves;
- report($msg, $E_OK);
- }
- }
- }
- }
- # Counter variable
- %nagios_level_count
- = (
- 'OK' => 0,
- 'WARNING' => 0,
- 'CRITICAL' => 0,
- 'UNKNOWN' => 0,
- );
- # holds only ok messages
- @ok_reports = ();
- # Reset the WARN signal
- $SIG{__WARN__} = 'DEFAULT';
- # Print any perl warnings that have occured
- if (@perl_warnings) {
- foreach (@perl_warnings) {
- chop @$_;
- report("INTERNAL ERROR: @$_", $E_UNKNOWN);
- }
- }
- $counter = 0;
- ALERT:
- foreach (sort {$a->[1] < $b->[1]} @reports) {
- my ($msg, $level) = @{ $_ };
- $nagios_level_count{$reverse_exitcode{$level}}++;
- if ($level == $E_OK && !$opt{verbose}) {
- push @ok_reports, $msg;
- next ALERT;
- }
- # Prefix with nagios level if specified with option '--state'
- $msg = $reverse_exitcode{$level} . ": $msg" if $opt{state};
- # Prefix with one-letter nagios level if specified with option '--short-state'
- $msg = (substr $reverse_exitcode{$level}, 0, 1) . ": $msg" if $opt{shortstate};
- ($counter++ == 0) ? print $msg : print $linebreak, $msg;
- }
- # Determine our exit code
- $exit_code = $E_OK;
- if ($nagios_level_count{UNKNOWN} > 0) { $exit_code = $E_UNKNOWN; }
- if ($nagios_level_count{WARNING} > 0) { $exit_code = $E_WARNING; }
- if ($nagios_level_count{CRITICAL} > 0) { $exit_code = $E_CRITICAL; }
- # Print OK messages
- $counter = 0;
- if ($exit_code == $E_OK && !$opt{verbose}) {
- foreach my $msg (@ok_reports) {
- # Prefix with nagios level if specified with option '--state'
- $msg = "OK: $msg" if $opt{state};
- # Prefix with one-letter nagios level if specified with option '--short-state'
- $msg = "O: $msg" if $opt{shortstate};
- ($counter++ == 0) ? print $msg : print $linebreak, $msg;
- }
- }
- print "\n";
- # Exit with proper exit code
- exit $exit_code;
|