#!/usr/bin/perl -w
############################################################
#
# $Id: rrd-client.pl 1092 2008-01-23 14:23:51Z nicolaw $
# rrd-client.pl - Data gathering script for RRD::Simple
#
# Copyright 2006,2007,2008 Nicola Worthington
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
############################################################
# vim:ts=4:sw=4:tw=78
############################################################
# User defined constants
use constant DB_MYSQL_DSN => $ENV{DB_MYSQL_DSN} || 'DBI:mysql:mysql:localhost';
use constant DB_MYSQL_USER => $ENV{DB_MYSQL_USER} || undef;
use constant DB_MYSQL_PASS => $ENV{DB_MYSQL_PASS} || undef;
use constant NET_PING_HOSTS => $ENV{NET_PING_HOSTS} ?
(split(/[\s,:]+/,$ENV{NET_PING_HOSTS})) : qw();
#
# YOU SHOULD NOT NEED TO EDIT ANYTHING BEYOND THIS POINT
#
############################################################
use 5.004;
use strict;
#use warnings; # comment out for release
use vars qw($VERSION);
$VERSION = '1.42' || sprintf('%d', q$Revision: 1092 $ =~ /(\d+)/g);
$ENV{PATH} = '/bin:/usr/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
# Default list of probes
my %probes = (
hw_irq_interrupts => 'Hardward IRQ Interrupts',
cpu_utilisation => 'CPU Utilisation',
cpu_loadavg => 'Load Average',
cpu_temp => 'CPU Temperature',
cpu_interrupts => 'CPU Interrupts',
hdd_io => 'Hard Disk I/O',
hdd_temp => 'Hard Disk Temperature',
hdd_capacity => 'Disk Capacity',
mem_usage => 'Memory Usage & Swap Usage',
mem_swap_activity => 'Swap Activity',
mem_proc_largest => 'Largest Process',
proc_threads => 'Threads',
proc_state => 'Processes',
proc_filehandles => 'File Handles',
apache_status => 'Apache Scoreboard & Apache Activity',
apache_logs => 'Apache Log Activity',
misc_uptime => 'Server Uptime',
misc_users => 'Users Logged In',
misc_ipmi_temp => 'IPMI Temperature Probes',
misc_entropy => 'Available Entropy',
db_mysql_activity => 'MySQL Database Activity',
db_mysql_replication => 'MySQL Database Replication',
mail_exim_queue => 'Exim Mail Queue',
mail_postfix_queue => 'Postfix Mail Queue',
mail_sendmail_queue => 'Sendmail Mail Queue',
net_traffic => 'Network Traffic',
net_connections => 'Network Connections',
net_ping_host => 'Ping',
# net_connections_ports => 'Service Connections',
);
# Get command line options
my %opt = ();
eval "require Getopt::Std";
$Getopt::Std::STANDARD_HELP_VERSION = 1;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
Getopt::Std::getopts('p:i:x:s:c:V:hvqlP:?', \%opt) unless $@;
(HELP_MESSAGE() && exit) if defined $opt{h} || defined $opt{'?'};
(VERSION_MESSAGE() && exit) if defined $opt{v};
# Display a list of available probe names
if ($opt{l}) {
print "Available probes:\n";
printf " %-24s %s\n",'PROBE','DESCRIPTION';
for (sort keys %probes) {
my $str = sprintf(" %-24s %s\n", $_, $probes{$_});
$str =~ s/(\S+) (\s+) /$_ = "$1 ". '.' x length($2) ." ";/e;
print "$str";
}
exit;
}
# Check to see if we are capable of SNMP queries
my $snmpClient;
if ($opt{s}) {
eval {
require Net::SNMP;
$snmpClient = 'Net::SNMP';
};
if ($@) {
my $c = 'snmpwalk'; # snmpget
my $cmd = select_cmd("/usr/bin/$c","/usr/local/bin/$c");
die "Error: unable to query via SNMP. Please install Net::SNMP or $c.\n" unless $cmd;
$snmpClient = $cmd;
}
$opt{c} = 'public' unless defined($opt{c}) && $opt{c} =~ /\S+/;
$opt{V} = '2c' unless defined($opt{V}) && $opt{V} =~ /^(1|2c)$/;
$opt{P} = 161 unless defined($opt{P}) && $opt{P} =~ /^[0-9]+$/;
}
# Filter on probe include list
my @probes = sort keys %probes;
if (defined $opt{i}) {
my $inc = join('|',split(/\s*,\s*/,$opt{i}));
@probes = grep(/(^|_)($inc)(_|$)/,@probes);
}
# Filter on probe exclude list
if (defined $opt{x}) {
my $exc = join('|',split(/\s*,\s*/,$opt{x}));
@probes = grep(!/(^|_)($exc)(_|$)/,@probes);
}
# Run the probes one by one
die "Error: nothing to probe!\n" unless @probes;
my $post = '';
my %update_cache;
for my $probe (@probes) {
eval {
local $SIG{ALRM} = sub { die "Timeout!\n"; };
alarm 15;
my $str = report($probe,eval "$probe();");
if (defined $opt{p}) {
$post .= $str;
} else {
print $str;
}
warn "Warning [$probe]: $@" if !$opt{q} && $@;
alarm 0;
};
warn "Warning [$probe]: $@" if !$opt{q} && $@;
}
# HTTP POST the data if asked to
print scalar(basic_http('POST',$opt{p},30,$post))."\n" if $opt{p};
exit;
# Report the data
sub report {
(my $probe = shift) =~ s/[_-]/\./g;
my %data = @_ % 2 ? (@_,undef) : @_;
my $str = '';
for my $k (sort keys %data) {
#$data{$k} = 0 unless defined($data{$k});
next unless defined($data{$k}) && $data{$k} =~ /^[0-9\.]*$/;
$str .= sprintf("%s.%s.%s %s\n", time(), $probe, $k, $data{$k});
}
return $str;
}
# Display help
sub HELP_MESSAGE {
print qq{Syntax: rrd-client.pl [-i probe1,probe2,..|-x probe1,probe2,..]
[-s host] [-c community] [-P port] [-V 1|2c] [-p URL] [-h|-v]
-i <probes> Include a list of comma seperated probes
-x <probes> Exclude a list of comma seperated probes
-s <host> Specify hostname to probe via SNMP
-c <community> Specify SNMP community name (defaults to public)
-V <version> Specify SNMP version to use (1 or 2c, defaults to 2c)
-P <port> Specify SNMP port to use
-p <URL> HTTP POST data to the specified URL
-q Suppress all warning messages
-l Display a list of available probe names
-v Display version information
-h Display this help
Examples:
rrd-client.pl -x apache_status -q -p http://rrd.me.uk/cgi-bin/rrd-server.cgi
rrd-client.pl -s localhost -p http://rrd.me.uk/cgi-bin/rrd-server.cgi
rrd-client.pl -s server1.company.com | rrd-server.pl -u server1.company.com
\n};
}
# Display version
sub VERSION { &VERSION_MESSAGE; }
sub VERSION_MESSAGE {
print "$0 version $VERSION ".'($Id: rrd-client.pl 1092 2008-01-23 14:23:51Z nicolaw $)'."\n";
}
# Basic HTTP client if LWP is unavailable
sub basic_http {
my ($method,$url,$timeout,$data) = @_;
$method ||= 'GET';
$url ||= 'http://localhost/';
$timeout ||= 5;
my ($scheme,$host,$port,$path) = $url =~ m,^(https?://)([\w\d\.\-]+)(?::(\d+))?(.*),i;
$scheme ||= 'http://';
$host ||= 'localhost';
$path ||= '/';
$port ||= 80;
my $str = '';
eval "use Socket";
return $str if $@;
eval {
local $SIG{ALRM} = sub { die "TIMEOUT\n" };
alarm $timeout;
my $iaddr = inet_aton($host) || die;
my $paddr = sockaddr_in($port, $iaddr);
my $proto = getprotobyname('tcp');
socket(SOCK, AF_INET(), SOCK_STREAM(), $proto) || die "socket: $!";
connect(SOCK, $paddr) || die "connect: $!";
select(SOCK); $| = 1;
select(STDOUT);
# Send the HTTP request
print SOCK "$method $path HTTP/1.1\n";
print SOCK "Host: $host". ("$port" ne "80" ? ":$port" : '') ."\n";
print SOCK "User-Agent: $0 version $VERSION ".'($Id: rrd-client.pl 1092 2008-01-23 14:23:51Z nicolaw $)'."\n";
if ($data && $method eq 'POST') {
print SOCK "Content-Length: ". length($data) ."\n";
print SOCK "Content-Type: application/x-www-form-urlencoded\n";
}
print SOCK "\n";
print SOCK $data if $data && $method eq 'POST';
my $body = 0;
while (local $_ = <SOCK>) {
s/[\n\n]+//g;
$str .= $_ if $_ && $body;
$body = 1 if /^\s*$/;
}
close(SOCK);
alarm 0;
};
warn "Warning [basic_http]: $@" if !$opt{q} && $@ && $data;
return wantarray ? split(/\n/,$str) : "$str";
}
# Return the most appropriate binary command
sub select_cmd {
foreach (@_) {
if (-f $_ && -x $_ && /(\S+)/) {
return $1;
}
}
return '';
}
#
# Probes
#
sub _snmp {
my $oid = [@_];
my $result = {};
# Net::SNMP
if ($snmpClient eq 'Net::SNMP') {
my ($session, $error) = Net::SNMP->session(
-hostname => $opt{s},
-community => $opt{c},
-version => $opt{V},
-port => $opt{P},
-translate => [ -timeticks => 0x0 ],
);
die $error if !defined($session);
$result = $session->get_request(-varbindlist => $oid);
$session->close;
die $session->error if !defined($result);
# snmpget / snmpwalk
} else {
my $oidStr = join(' ', @{$oid});
my $cmd = "$snmpClient -O n -O t -v $opt{V} -c $opt{c} $opt{s} $oidStr";
#my $cmd = "$snmpClient -O t -v $opt{V} -c $opt{c} $opt{s} $oidStr";
open(PH,'-|',"$cmd 2>&1") || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
s/[\r\n]+//g; s/^(?:\s+|\s+)$//g;
if (/^(\.[\.0-9]+|[A-Za-z:\-\.0-9]+)\s*=\s*(?:([A-Za-z0-9]+):\s*)?["']?(\S*)["']?/) {
my ($oid,$type,$value) = ($1,$2,$3);
$oid = ".$oid" if $oid =~ /^[0-9][0-9\.]+$/;
$result->{$oid} = $value;
} else {
warn "Warning [_snmp]: $_\n" unless $opt{q};
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
}
return $result->{(keys(%{$result}))[0]} if !wantarray && keys(%{$result}) == 1;
return $result;
}
sub net_ping_host {
return if $opt{s};
return unless defined NET_PING_HOSTS() && scalar NET_PING_HOSTS() > 0;
my $cmd = select_cmd(qw(/bin/ping /usr/bin/ping /sbin/ping /usr/sbin/ping));
return unless -f $cmd;
my %update = ();
my $count = 3;
for my $str (NET_PING_HOSTS()) {
my ($host) = $str =~ /^([\w\d_\-\.]+)$/i;
next unless $host;
my $cmd2 = "$cmd -c $count $host 2>&1";
open(PH,'-|',$cmd2) || die "Unable to open file handle PH for command '$cmd2': $!\n";
while (local $_ = <PH>) {
if (/\s+(\d+)%\s+packet\s+loss[\s,]/i) {
$update{"$host.PacketLoss"} = $1 || 0;
} elsif (my ($min,$avg,$max,$mdev) = $_ =~
/\s+([\d\.]+)\/([\d\.]+)\/([\d\.]+)\/([\d\.]+)\s+/) {
$update{"$host.AvgRTT"} = $avg || 0;
$update{"$host.MinRTT"} = $min || 0;
$update{"$host.MaxRTT"} = $max || 0;
$update{"$host.MDevRTT"} = $mdev || 0;
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd2': $!\n";
}
return %update;
}
sub mem_proc_largest {
return if $opt{s};
my $cmd = select_cmd(qw(/bin/ps /usr/bin/ps));
return unless -f $cmd;
$cmd .= ' -eo vsize';
my %update = ();
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
if (/(\d+)/) {
my $kb = $1;
$update{LargestProc} = $kb if !defined $update{LargestProc} ||
(defined $update{LargestProc} && $kb > $update{LargestProc});
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
$update{LargestProc} *= 1024 if defined $update{LargestProc};
return %update;
}
sub proc_threads {
if ($opt{s}) {
my $procs = _snmp('.1.3.6.1.2.1.25.1.6.0'); # hrSystemProcesses
return unless defined($procs) && $procs =~ /^[0-9]+$/;
return ('Processes' => $procs, 'Threads' => 0, 'MultiThreadProcs' => 0);
}
return if $opt{s};
return unless ($^O eq 'linux' && `/bin/uname -r 2>&1` =~ /^2\.6\./) ||
($^O eq 'solaris' && `/bin/uname -r 2>&1` =~ /^5\.9/);
my %update = ();
my $cmd = '/bin/ps -eo pid,nlwp';
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!";
while (local $_ = <PH>) {
if (my ($pid,$nlwp) = $_ =~ /^\s*(\d+)\s+(\d+)\s*$/) {
$update{Processes}++;
$update{Threads} += $nlwp;
$update{MultiThreadProcs}++ if $nlwp > 1;
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!";
return %update;
}
sub mail_exim_queue {
return if $opt{s};
my $spooldir = '/var/spool/exim/input';
return unless -d $spooldir && -x $spooldir && -r $spooldir;
local %mail::exim::queue::update = (Messages => 0);
require File::Find;
File::Find::find({wanted => sub {
my ($dev,$ino,$mode,$nlink,$uid,$gid);
(($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
-f _ &&
/^.*-D\z/s &&
$mail::exim::queue::update{Messages}++;
}, no_chdir => 1}, $spooldir);
return %mail::exim::queue::update;
}
sub mail_sendmail_queue {
return if $opt{s};
my $spooldir = '/var/spool/mqueue';
return unless -d $spooldir && -x $spooldir && -r $spooldir;
local %mail::sendmail::queue::update = (Messages => 0);
require File::Find;
File::Find::find({wanted => sub {
my ($dev,$ino,$mode,$nlink,$uid,$gid);
(($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
-f _ &&
/^Qf[a-zA-Z0-9]{14}\z/s &&
$mail::sendmail::queue::update{Messages}++;
}, no_chdir => 1}, $spooldir);
return %mail::sendmail::queue::update;
}
sub mail_postfix_queue {
return if $opt{s};
my @spooldirs = qw(
/var/spool/postfix/incoming
/var/spool/postfix/active
/var/spool/postfix/defer
/var/spool/postfix/deferred
);
for my $spooldir (@spooldirs) {
return unless -d $spooldir && -x $spooldir && -r $spooldir;
}
local %mail::postfix::queue::update = (Messages => 0);
require File::Find;
File::Find::find({wanted => sub {
my ($dev,$ino,$mode,$nlink,$uid,$gid);
(($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
-f _ &&
$mail::postfix::queue::update{Messages}++;
}, no_chdir => 1}, @spooldirs);
return %mail::postfix::queue::update;
}
# DO NOT ENABLE THIS ONE YET
sub mail_queue {
return if $opt{s};
my $cmd = select_cmd(qw(/usr/bin/mailq /usr/sbin/mailq /usr/local/bin/mailq
/usr/local/sbin/mailq /bin/mailq /sbin/mailq
/usr/local/exim/bin/mailq /home/system/exim/bin/mailq));
return unless -f $cmd;
my %update = ();
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
# This needs to match a single message id = currently only exim friendly
if (/^\s*\S+\s+\S+\s+[a-z0-9]{6}-[a-z0-9]{6}-[a-z0-9]{2} </i) {
$update{Messages}++;
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
$update{Messages} = 0 if !defined($update{Messages});
return %update;
}
sub db_mysql_activity {
return if $opt{s};
my %update = ();
return %update unless (defined DB_MYSQL_DSN && defined DB_MYSQL_USER);
my @cols = qw(Com_select Com_insert Com_delete Com_replace Com_update Questions);
# my @GAUGE = qw(Key_blocks_not_flushed Key_blocks_unused Key_blocks_used
# Open_files Open_streams Open_tables Qcache_free_blocks Qcache_free_memory
# Qcache_queries_in_cache Qcache_total_blocks Slave_open_temp_tables
# Threads_cached Threads_connected Threads_running Uptime);
# my @DERIVE = qw(Aborted_clients Aborted_connects Binlog_cache_disk_use
# Binlog_cache_use Bytes_received Bytes_sent Com_admin_commands Com_alter_db
# Com_alter_table Com_analyze Com_backup_table Com_begin Com_change_db
# Com_change_master Com_check Com_checksum Com_commit Com_create_db
# Com_create_function Com_create_index Com_create_table Com_dealloc_sql
# Com_delete Com_delete_multi Com_do Com_drop_db Com_drop_function
# Com_drop_index Com_drop_table Com_drop_user Com_execute_sql Com_flush
# Com_grant Com_ha_close Com_ha_open Com_ha_read Com_help Com_insert
# Com_insert_select Com_kill Com_load Com_load_master_data Com_load_master_table
# Com_lock_tables Com_optimize Com_preload_keys Com_prepare_sql Com_purge
# Com_purge_before_date Com_rename_table Com_repair Com_replace Com_replace_select
# Com_reset Com_restore_table Com_revoke Com_revoke_all Com_rollback
# Com_savepoint Com_select Com_set_option Com_show_binlog_events
# Com_show_binlogs Com_show_charsets Com_show_collations Com_show_column_types
# Com_show_create_db Com_show_create_table Com_show_databases
# Com_show_errors Com_show_fields Com_show_grants Com_show_innodb_status
# Com_show_keys Com_show_logs Com_show_master_status Com_show_ndb_status
# Com_show_new_master Com_show_open_tables Com_show_privileges Com_show_processlist
# Com_show_slave_hosts Com_show_slave_status Com_show_status Com_show_storage_engines
# Com_show_tables Com_show_variables Com_show_warnings Com_slave_start
# Com_slave_stop Com_stmt_close Com_stmt_execute Com_stmt_prepare Com_stmt_reset
# Com_stmt_send_long_data Com_truncate Com_unlock_tables Com_update
# Com_update_multi Connections Created_tmp_disk_tables Created_tmp_files
# Created_tmp_tables Delayed_errors Delayed_insert_threads Delayed_writes
# Flush_commands Handler_commit Handler_delete Handler_discover Handler_read_first
# Handler_read_key Handler_read_next Handler_read_prev Handler_read_rnd
# Handler_read_rnd_next Handler_rollback Handler_update Handler_write
# Key_blocks_not_flushed Key_blocks_unused Key_blocks_used Key_read_requests
# Key_reads Key_write_requests Key_writes Max_used_connections Not_flushed_delayed_rows
# Open_files Open_streams Open_tables Opened_tables Qcache_free_blocks
# Qcache_free_memory Qcache_hits Qcache_inserts Qcache_lowmem_prunes Qcache_not_cached
# Qcache_queries_in_cache Qcache_total_blocks Questions Select_full_join
# Select_full_range_join Select_range Select_range_check Select_scan Slave_open_temp_tables
# Slave_retried_transactions Slow_launch_threads Slow_queries Sort_merge_passes
# Sort_range Sort_rows Sort_scan Ssl_accept_renegotiates Ssl_accepts
# Ssl_callback_cache_hits Ssl_client_connects Ssl_connect_renegotiates Ssl_ctx_verify_depth
# Ssl_ctx_verify_mode Ssl_default_timeout Ssl_finished_accepts Ssl_finished_connects
# Ssl_session_cache_hits Ssl_session_cache_misses Ssl_session_cache_overflows
# Ssl_session_cache_size Ssl_session_cache_timeouts Ssl_sessions_reused
# Ssl_used_session_cache_entries Ssl_verify_depth Ssl_verify_mode
# Table_locks_immediate Table_locks_waited Threads_cached Threads_connected
# Threads_created Threads_running);
eval {
require DBI;
my $dbh = DBI->connect(DB_MYSQL_DSN,DB_MYSQL_USER,DB_MYSQL_PASS);
my $sth = $dbh->prepare('SHOW GLOBAL STATUS');
$sth->execute();
while (my @ary = $sth->fetchrow_array()) {
if (grep { $ary[0] eq $_ && /^Com_/ } @cols) {
$update{"com.$ary[0]"} = $ary[1];
} elsif (grep { $ary[0] eq $_ } @cols) {
$update{$ary[0]} = $ary[1];
}
}
$sth->finish();
$dbh->disconnect();
};
return %update;
}
sub db_mysql_replication {
return if $opt{s};
my %update = ();
return %update unless (defined DB_MYSQL_DSN && defined DB_MYSQL_USER);
eval {
require DBI;
my $dbh = DBI->connect(DB_MYSQL_DSN,DB_MYSQL_USER,DB_MYSQL_PASS);
my $sth = $dbh->prepare('SHOW SLAVE STATUS');
$sth->execute();
my $row = $sth->fetchrow_hashref;
$sth->finish();
$dbh->disconnect();
$update{SecondsBehind} = $row->{Seconds_Behind_Master} || 0;
};
return %update;
}
sub misc_users {
if ($opt{s}) {
my $users = _snmp('.1.3.6.1.2.1.25.1.5.0'); # hrSystemNumUsers
return unless defined($users) && $users =~ /^[0-9]+$/;
return ('Users' => $users, 'Unique' => 0);
}
return if $opt{s};
my $cmd = select_cmd(qw(/usr/bin/who /bin/who /usr/bin/w /bin/w));
return unless -f $cmd;
my %update = ();
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
my %users = ();
while (local $_ = <PH>) {
next if /^\s*USERS\s*TTY/;
$users{(split(/\s+/,$_))[0]}++;
$update{Users}++;
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
$update{Unique} = keys %users if keys %users;
unless (keys %update) {
$cmd = -f '/usr/bin/uptime' ? '/usr/bin/uptime' : '/bin/uptime';
if (my ($users) = `$cmd` =~ /,\s*(\d+)\s*users?\s*,/i) {
$update{Users} = $1;
}
}
$update{Users} ||= 0;
$update{Unique} ||= 0;
return %update;
}
sub misc_uptime {
if ($opt{s}) {
my $ticks = _snmp('.1.3.6.1.2.1.25.1.1.0'); # hrSystemUptime
return unless defined($ticks) && $ticks =~ /^[0-9]+$/;
return ('DaysUp' => $ticks/100/60/60/24);
}
my $cmd = select_cmd(qw(/usr/bin/uptime /bin/uptime));
return unless -f $cmd;
my %update = ();
if (my ($str) = `$cmd` =~ /\s*up\s*(.+?)\s*,\s*\d+\s*users?/) {
my $days = 0;
if (my ($nuke,$num) = $str =~ /(\s*(\d+)\s*days?,?\s*)/) {
$str =~ s/$nuke//;
$days += $num;
}
if (my ($nuke,$mins) = $str =~ /(\s*(\d+)\s*mins?,?\s*)/) {
$str =~ s/$nuke//;
$days += ($mins / (60*24));
}
if (my ($nuke,$hours) = $str =~ /(\s*(\d+)\s*(hour|hr)s?,?\s*)/) {
$str =~ s/$nuke//;
$days += ($hours / 24);
}
if (my ($hours,$mins) = $str =~ /\s*(\d+):(\d+)\s*,?/) {
$days += ($mins / (60*24));
$days += ($hours / 24);
}
$update{DaysUp} = $days;
}
return %update;
}
sub cpu_temp {
return if $opt{s};
my $cmd = '/usr/bin/sensors';
return unless -f $cmd;
my %update = ();
open(PH,'-|',"$cmd 2>&1") || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
if (my ($k,$v) = $_ =~ /^([^:]*\b(?:CPU|temp)\d*\b.*?):\s*\S*?([\d\.]+)\S*\s*/i) {
$k =~ s/\W//g; $k =~ s/Temp$//i;
$update{$k} = $v;
} elsif (/(no sensors found|kernel driver|sensors-detect|error|warning)/i
&& !$opt{q}) {
warn "Warning [cpu_temp]: $_";
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
return %update;
}
sub apache_logs {
return if $opt{s};
my $dir = '/var/log/httpd';
return unless -d $dir;
my %update = ();
if (-d $dir) {
opendir(DH,$dir) || die "Unable to open file handle for directory '$dir': $!\n";
my @files = grep(!/^\./,readdir(DH));
closedir(DH) || die "Unable to close file handle for directory '$dir': $!\n";
for (@files) {
next if /\.(\d+|gz|bz2|Z|zip|old|bak|pid|backup)$/i || /[_\.\-]pid$/;
my $file = "$dir/$_";
next unless -f $file;
s/[\.\-]/_/g;
$update{$_} = (stat($file))[7];
}
}
return %update;
}
sub apache_status {
return if $opt{s};
my @data = ();
my %update = ();
my $timeout = 5;
my $url = 'http://localhost/server-status?auto';
my %keys = (W => 'Write', G => 'GraceClose', D => 'DNS', S => 'Starting',
L => 'Logging', R => 'Read', K => 'Keepalive', C => 'Closing',
I => 'Idle', '_' => 'Waiting');
eval "use LWP::UserAgent";
unless ($@) {
eval {
my $ua = LWP::UserAgent->new(
agent => "$0 version $VERSION ".'($Id)',
timeout => $timeout);
$ua->env_proxy;
$ua->max_size(1024*250);
my $response = $ua->get($url);
if ($response->is_success) {
@data = split(/\n+|\r+/,$response->content);
} elsif (!$opt{q}) {
warn "Warning [apache_status]: failed to get $url; ". $response->status_line ."\n";
}
};
}
if ($@) {
@data = basic_http('GET',$url,$timeout);
}
for (@data) {
my ($k,$v) = $_ =~ /^\s*(.+?):\s+(.+?)\s*$/;
$k = '' unless defined $k;
$v = '' unless defined $v;
$k =~ s/\s+//g; #$k = lc($k);
next unless $k;
if ($k eq 'Scoreboard') {
my %x; $x{$_}++ for split(//,$v);
for (keys %keys) {
$update{"scoreboard.$keys{$_}"} =
defined $x{$_} ? $x{$_} : 0;
}
} else {
$update{$k} = $v;
}
}
$update{ReqPerSec} = int($update{TotalAccesses})
if defined $update{TotalAccesses};
$update{BytesPerSec} = int($update{TotalkBytes} * 1024)
if defined $update{TotalkBytes};
return %update;
}
sub _darwin_cpu_utilisation {
my $output = qx{/usr/bin/sar 4 1};
my %rv = ();
if ($output =~ m/Average:\s+(\d+)\s+(\d+)\s+(\d+)/) {
%rv = (
User => $1,
System => $2,
Idle => $3,
IO_Wait => 0, # at the time of writing, sar doesn't provide this metric
);
}
return %rv;
}
sub cpu_utilisation {
my %update = ();
if ($opt{s}) {
$update{'User'} = _snmp('.1.3.6.1.4.1.2021.11.9.0');
$update{'System'} = _snmp('.1.3.6.1.4.1.2021.11.10.0');
$update{'Idle'} = _snmp('.1.3.6.1.4.1.2021.11.11.0');
#$update{'IO_Wait'} = 100 - $update{'User'} - $update{'System'} - $update{'Idle'};
$update{'IO_Wait'} = 0;
# Try querying the Windows thingie instead
unless (grep(/^[0-9\.]{1,3}$/, values(%update)) == 4) {
# hrProcessorLoad
# .1.3.6.1.2.1.25.3.3.1.2.1 - CPU 1
# .1.3.6.1.2.1.25.3.3.1.2.2 - CPU 2 ...
my $total; my $cpu;
for ($cpu = 1; $cpu <= 16; $cpu++) {
my $load = _snmp(".1.3.6.1.2.1.25.3.3.1.2.$cpu");
if (defined($load) && !ref($load) && $load =~ /^[0-9\.]{1,3}$/) {
$total += $load;
} else {
last;
}
}
return unless $total && $cpu-1;
%update = ('User' => int($total / $cpu-1), 'System' => 0, 'Idle' => 0, 'IO_Wait' => 0);
}
return %update;
}
if ($^O eq 'darwin') {
return _darwin_cpu_utilisation();
}
my $cmd = '/usr/bin/vmstat';
return unless -f $cmd;
%update = _parse_vmstat("$cmd 1 2");
my %labels = (wa => 'IO_Wait', id => 'Idle', sy => 'System', us => 'User');
$update{$_} ||= 0 for keys %labels;
return ( map {( $labels{$_} || $_ => $update{$_} )} keys %labels );
}
sub hw_irq_interrupts {
return if $opt{s};
my @update;
if (open(FH,'<','/proc/interrupts')) {
local $_ = <FH>;
return unless /^\s+(CPU[0-9]+.*)/;
my @cpus = split(/\s+/,$1);
$_ = lc($_) for @cpus;
my %data;
while (local $_ = <FH>) {
if (/^\s*([0-9]{1,2}):\s+([\s0-9]+)\s+(\S+)\s+(.+?)\s*$/) {
my ($irq,$ints,$path,$src) = ($1,$2,$3,$4);
my @ints = split(/\s+/,$ints);
for (my $i = 0; $i <= @cpus; $i++) {
my $cpu = $cpus[$i];
my $int = $ints[$i];
next unless defined $cpu && defined $int;
$data{$cpu}->{"$cpu.irq$irq"} = $int;
}
}
}
for my $cpu (keys %data) {
if (grep(/[1-9]/,values %{$data{$cpu}})) {
push @update, %{$data{$cpu}};
}
}
close(FH) || warn "Unable to close file handle FH for file '/proc/interrupts': $!";
}
return @update;
}
sub cpu_interrupts {
return if $opt{s};
my $cmd = '/usr/bin/vmstat';
return unless -f $cmd;
my %update = _parse_vmstat("$cmd 1 2");
my %labels = (in => 'Interrupts');
return unless defined $update{in};
$update{$_} ||= 0 for keys %labels;
return ( map {( $labels{$_} || $_ => $update{$_} )} keys %labels );
}
sub mem_swap_activity {
return if $opt{s};
my $cmd = '/usr/bin/vmstat';
return unless -f $cmd;
my %update = _parse_vmstat("$cmd 1 2");
my %labels = (si => 'Swap_In', so => 'Swap_Out');
return unless defined $update{si} && defined $update{so};
$update{$_} ||= 0 for keys %labels;
return ( map {( $labels{$_} || $_ => $update{$_} )} keys %labels );
}
sub _parse_vmstat {
my $cmd = shift;
my %update;
my @keys;
if (exists $update_cache{vmstat}) {
%update = %{$update_cache{vmstat}};
} else {
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
s/^\s+|\s+$//g;
if (/\s+\d+\s+\d+\s+\d+\s+/ && @keys) {
@update{@keys} = split(/\s+/,$_);
} else { @keys = split(/\s+/,$_); }
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
$update_cache{vmstat} = \%update;
}
return %update;
}
sub _parse_ipmitool_sensor {
my $cmd = shift;
my %update;
my @keys;
if (exists $update_cache{ipmitool_sensor}) {
%update = %{$update_cache{ipmitool_sensor}};
} else {
if ((-e '/dev/ipmi0' || -e '/dev/ipmi/0') && open(PH,'-|',$cmd)) {
while (local $_ = <PH>) {
chomp; s/(^\s+|\s+$)//g;
my ($key,@ary) = split(/\s*\|\s*/,$_);
$key =~ s/[^a-zA-Z0-9_]//g;
$update{$key} = \@ary;
}
close(PH);
$update_cache{ipmitool_sensor} = \%update;
}
}
return %update;
}
sub misc_ipmi_temp {
return if $opt{s};
my $cmd = select_cmd(qw(/usr/bin/ipmitool));
return unless -f $cmd;
my %update = ();
my %data = _parse_ipmitool_sensor("$cmd sensor");
for (grep(/temp/i,keys %data)) {
$update{$_} = $data{$_}->[0]
if $data{$_}->[0] =~ /^[0-9\.]+$/;
}
return unless keys %update;
return %update;;
}
sub hdd_io {
return if $opt{s};
my $cmd = select_cmd(qw(/usr/bin/iostat /usr/sbin/iostat));
return unless -f $cmd;
return unless $^O eq 'linux';
$cmd .= ' -k';
my %update = ();
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
if (my ($dev,$r,$w) = $_ =~ /^([\w\d]+)\s+\S+\s+\S+\s+\S+\s+(\d+)\s+(\d+)$/) {
$update{"$dev.Read"} = $r*1024;
$update{"$dev.Write"} = $w*1024;
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
return %update;
}
sub mem_usage {
return if $opt{s};
my %update = ();
my $cmd = select_cmd(qw(/usr/bin/free /bin/free));
my @keys = ();
if ($^O eq 'linux' && -f $cmd && -x $cmd) {
$cmd .= ' -b';
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
if (@keys && /^Mem:\s*(\d+.+)\s*$/i) {
my @values = split(/\s+/,$1);
for (my $i = 0; $i < @values; $i++) {
$update{ucfirst($keys[$i])} = $values[$i];
}
$update{Used} = $update{Used} - $update{Buffers} - $update{Cached};
} elsif (@keys && /^Swap:\s*(\d+.+)\s*$/i) {
my @values = split(/\s+/,$1);
for (my $i = 0; $i < @values; $i++) {
$update{"swap.".ucfirst($keys[$i])} = $values[$i];
}
} elsif (!@keys && /^\s*([\w\s]+)\s*$/) {
@keys = split(/\s+/,$1);
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
} elsif ($^O eq 'darwin' && -x '/usr/sbin/sysctl') {
my $swap = qx{/usr/sbin/sysctl vm.swapusage};
if ($swap =~ m/total = (.+)M used = (.+)M free = (.+)M/) {
$update{"swap.Total"} = $1*1024*1024;
$update{"swap.Used"} = $2*1024*1024;
$update{"swap.Free"} = $3*1024*1024;
}
} else {
eval "use Sys::MemInfo qw(totalmem freemem)";
die "Please install Sys::MemInfo so that I can get memory information.\n" if $@;
@update{qw(Total Free)} = (totalmem(),freemem());
}
return %update;
}
sub hdd_temp {
return if $opt{s};
my $cmd = select_cmd(qw(/usr/sbin/hddtemp /usr/bin/hddtemp));
return unless -f $cmd;
my @devs = ();
for my $dev (glob('/dev/hd?'),glob('/dev/sd?')) {
if ($dev =~ /^(\/dev\/\w{3})$/i) {
push @devs, $1;
}
}
$cmd .= " -q @devs 2>&1";
my %update = ();
return %update unless @devs;
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
if (my ($dev,$temp) = $_ =~ m,^/dev/([a-z]+):\s+.+?:\s+(\d+)..?C,) {
$update{$dev} = $temp;
} elsif (!/^\s*$/ && !$opt{q}) {
warn "Warning [hdd_temp]: $_";
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
return %update;
}
sub hdd_capacity {
if ($opt{s}) {
return;
my $snmp = _snmp('.1.3.6.1.4.1.2021.9.1'); # dskTable
return unless defined($snmp) && ref($snmp) eq 'HASH';
my %disks;
#.1.3.6.1.4.1.2021.9.1.1.1 = INTEGER: 1
#.1.3.6.1.4.1.2021.9.1.2.1 = STRING: /
#.1.3.6.1.4.1.2021.9.1.3.1 = STRING: /dev/sda1
#.1.3.6.1.4.1.2021.9.1.4.1 = INTEGER: 100000
#.1.3.6.1.4.1.2021.9.1.5.1 = INTEGER: -1
#.1.3.6.1.4.1.2021.9.1.6.1 = INTEGER: 7850996
#.1.3.6.1.4.1.2021.9.1.7.1 = INTEGER: 3153808
#.1.3.6.1.4.1.2021.9.1.8.1 = INTEGER: 4298376
#.1.3.6.1.4.1.2021.9.1.9.1 = INTEGER: 58
#.1.3.6.1.4.1.2021.9.1.10.1 = INTEGER: 16
#.1.3.6.1.4.1.2021.9.1.100.1 = INTEGER: 0
#.1.3.6.1.4.1.2021.9.1.101.1 = STRING:
#'UCD-SNMP-MIB::dskMinimum.1' => '100000',
#'UCD-SNMP-MIB::dskErrorMsg.1' => '',
#'UCD-SNMP-MIB::dskIndex.1' => '1',
#'UCD-SNMP-MIB::dskPath.1' => '/',
#'UCD-SNMP-MIB::dskPercentNode.1' => '16',
#'UCD-SNMP-MIB::dskErrorFlag.1' => '0',
#'UCD-SNMP-MIB::dskAvail.1' => '3153784',
#'#UCD-SNMP-MIB::dskPercent.1' => '58',
#'UCD-SNMP-MIB::dskMinPercent.1' => '-1',
#'UCD-SNMP-MIB::dskDevice.1' => '/dev/sda1',
#'UCD-SNMP-MIB::dskUsed.1' => '4298400',
#'UCD-SNMP-MIB::dskTotal.1' => '7850996'
#use Data::Dumper;
#warn Dumper($snmp);
return;
}
my $cmd = select_cmd(qw(/bin/df /usr/bin/df));
return unless -f $cmd;
if ($^O eq 'linux') { $cmd .= ' -P -x iso9660 -x nfs -x smbfs'; }
elsif ($^O eq 'solaris') { $cmd .= ' -lk -F ufs'; }
elsif ($^O eq 'darwin') { $cmd .= ' -P -T hfs,ufs'; }
else { $cmd .= ' -P'; }
my %update = ();
my %variants = (
'' => '',
'inodes.' => ' -i ',
);
for my $variant (keys %variants) {
my $variant_cmd = "$cmd $variants{$variant}";
my @data = split(/\n/, `$variant_cmd`);
shift @data;
my @cols = qw(fs blocks used avail capacity mount unknown);
for (@data) {
my %data = ();
@data{@cols} = split(/\s+/,$_);
if ($^O eq 'darwin' || defined $data{unknown}) {
@data{@cols} = $_ =~ /^(.+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%?\s+(.+)\s*$/;
}
next if ($data{fs} eq 'none' || $data{mount} =~ m#^/dev/#);
$data{capacity} =~ s/\%//;
(my $ds = $data{mount}) =~ s/[^a-z0-9]/_/ig; $ds =~ s/__+/_/g;
# McAfee SCM 4.2 bodge-o-rama fix work around
next if $ds =~ /^_var_jails_d_spam_/;
$update{"${variant}$ds"} = $data{capacity};
}
}
return %update;
}
sub misc_entropy {
return if $opt{s};
my $file = '/proc/sys/kernel/random/entropy_avail';
return unless -f $file;
my %update = ();
open(FH,'<',$file) || die "Unable to open '$file': $!\n";
chomp($update{entropy_avail} = <FH>);
close(FH) || die "Unable to close '$file': $!\n";
return %update;
}
sub net_traffic {
return if $opt{s};
return unless -f '/proc/net/dev';
my @keys = ();
my %update = ();
open(FH,'<','/proc/net/dev') || die "Unable to open '/proc/net/dev': $!\n";
while (local $_ = <FH>) {
s/^\s+|\s+$//g;
if ((my ($dev,$data) = $_ =~ /^(.+?):\s*(\d+.+)\s*$/) && @keys) {
my @values = split(/\s+/,$data);
for (my $i = 0; $i < @keys; $i++) {
if ($keys[$i] eq 'TXbytes') {
$update{"$dev.Transmit"} = $values[$i];
} elsif ($keys[$i] eq 'RXbytes') {
$update{"$dev.Receive"} = $values[$i];
}
#$update{"$dev.$keys[$i]"} = $values[$i];
}
} else {
my ($rx,$tx) = (split(/\s*\|\s*/,$_))[1,2];
@keys = (map({"RX$_"} split(/\s+/,$rx)), map{"TX$_"} split(/\s+/,$tx));
}
}
close(FH) || die "Unable to close '/proc/net/dev': $!\n";
return %update;
}
sub proc_state {
return if $opt{s};
my $cmd = select_cmd(qw(/bin/ps /usr/bin/ps));
my %update = ();
my %keys = ();
if (-f $cmd && -x $cmd) {
if ($^O eq 'freebsd' || $^O eq 'darwin') {
$cmd .= ' axo pid,state';
# %keys = (D => 'IO_Wait', R => 'Run', S => 'Sleep', T => 'Stopped',
# I => 'Idle', L => 'Lock_Wait', Z => 'Zombie', W => 'Idle_Thread');
%keys = (D => 'IO_Wait', R => 'Run', S => 'Sleep', T => 'Stopped',
W => 'Paging', Z => 'Zombie', I => 'Sleep');
} else {#} elsif ($^O =~ /^(linux|solaris)$/)
$cmd .= ' -eo pid,s';
%keys = (D => 'IO_Wait', R => 'Run', S => 'Sleep', T => 'Stopped',
W => 'Paging', X => 'Dead', Z => 'Zombie');
}
my $known_keys = join('',keys %keys);
open(PH,'-|',$cmd) || die "Unable to open file handle PH for command '$cmd': $!\n";
while (local $_ = <PH>) {
if (my ($pid,$state) = $_ =~ /^\s*(\d+)\s+(\S+)\s*$/) {
$state =~ s/[^$known_keys]//g;
$update{$keys{$state}||$state}++ if $state;
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
$update{$_} ||= 0 for values %keys;
} else {
eval "use Proc::ProcessTable";
die "Please install /bin/ps or Proc::ProcessTable\n" if $@;
my $p = new Proc::ProcessTable("cache_ttys" => 1 );
for (@{$p->table}) {
$update{$_->{state}}++;
}
}
return %update;
}
sub cpu_loadavg {
my %update = ();
if ($opt{s}) {
$update{'1min'} = _snmp('.1.3.6.1.4.1.2021.10.1.3.1');
$update{'5min'} = _snmp('.1.3.6.1.4.1.2021.10.1.3.2');
$update{'15min'} = _snmp('.1.3.6.1.4.1.2021.10.1.3.3');
return %update;
}
my @data = ();
if (-f '/proc/loadavg') {
open(FH,'<','/proc/loadavg') || die "Unable to open file handle FH for file '/proc/loadavg': $!\n";
my $str = <FH>;
close(FH) || die "Unable to close file handle FH for file '/proc/loadavg': $!\n";
@data = split(/\s+/,$str);
} else {
my $cmd = -f '/usr/bin/uptime' ? '/usr/bin/uptime' : '/bin/uptime';
@data = `$cmd` =~ /[\s:]+([\d\.]+)[,\s]+([\d\.]+)[,\s]+([\d\.]+)\s*$/;
}
%update = (
"1min" => $data[0],
"5min" => $data[1],
"15min" => $data[2],
);
return %update;
}
sub _parse_netstat {
my $cmd = shift;
my $update;
my @keys = qw(local_ip local_port remote_ip remote_port);
if (exists $update_cache{netstat}) {
$update = $update_cache{netstat};
} else {
open(PH,'-|',$cmd) || die "Unable to open file handle for command '$cmd': $!\n";
while (local $_ = <PH>) {
my %line;
if (@line{qw(proto data state)} = $_ =~ /^(tcp[46]?|udp[46]?|raw)\s+(.+)\s+([A-Z_]+)\s*$/) {
@line{@keys} = $line{data} =~ /(?:^|[\s\b])([:abcdef0-9\.]+):(\d{1,5})(?:[\s\b]|$)/g;
push @{$update}, \%line;
}
}
close(PH) || die "Unable to close file handle PH for command '$cmd': $!\n";
$update_cache{netstat} = $update;
}
return $update;
}
sub net_connections_ports {
return if $opt{s};
my $cmd = select_cmd(qw(/bin/netstat /usr/bin/netstat /usr/sbin/netstat));
return unless -f $cmd;
$cmd .= ' -na 2>&1';
my %update = ();
my %listening_ports;
for (@{_parse_netstat($cmd)}) {
if ($_->{state} =~ /listen/i && defined $_->{local_port}) {
$listening_ports{"$_->{proto}:$_->{local_port}"} = 1;
$update{"$_->{proto}_$_->{local_port}"} = 0;
}
}
for (@{_parse_netstat($cmd)}) {
next if !defined $_->{state} || !defined $_->{remote_port};
$update{"$_->{proto}_$_->{remote_port}"}++ if exists $listening_ports{"$_->{proto}:$_->{remote_port}"};
}
return %update;
}
sub net_connections {
return if $opt{s};
my $cmd = select_cmd(qw(/bin/netstat /usr/bin/netstat /usr/sbin/netstat));
return unless -f $cmd;
$cmd .= ' -na 2>&1';
my %update = ();
for (@{_parse_netstat($cmd)}) {
$update{$_->{state}}++ if defined $_->{state};
}
return %update;
}
sub proc_filehandles {
return if $opt{s};
return unless -f '/proc/sys/fs/file-nr';
my %update = ();
open(FH,'<','/proc/sys/fs/file-nr') || die "Unable to open file handle FH for file '/proc/sys/fs/file-nr': $!\n";
my $str = <FH>;
close(FH) || die "Unable to close file handle FH for file '/proc/sys/fs/file-nr': $!\n";
@update{qw(Allocated Free Maximum)} = split(/\s+/,$str);
$update{Used} = $update{Allocated} - $update{Free};
return %update;
}