Current File : //usr/share/webmin/status/status-lib.pl |
# status-lib.pl
# Functions for getting the status of services
BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
%access = &get_module_acl();
use Socket;
# Wrapper command for cron job
$cron_cmd = "$module_config_directory/monitor.pl";
$full_cron_cmd = $cron_cmd." >/dev/null 2>&1";
# Config directory for monitors
$services_dir = "$module_config_directory/services";
# File storing last status of each monitor
$oldstatus_file = "$module_config_directory/oldstatus";
if (!-r $oldstatus_file) {
$oldstatus_file = "$module_var_directory/oldstatus";
}
# Failure count file for each monitor
$fails_file = "$module_config_directory/fails";
if (!-r $fails_file) {
$fails_file = "$module_var_directory/fails";
}
# Last email sent for each monitor
$lastsent_file = "$module_config_directory/lastsent";
if (!-r $lastsent_file) {
$lastsent_file = "$module_var_directory/lastsent";
}
# Directory of historic results for each monitor
$history_dir = "$module_config_directory/history";
if (!-d $history_dir) {
$history_dir = "$module_var_directory/history";
}
$templates_dir = "$module_config_directory/templates";
%monitor_os_support = ( 'traffic' => { 'os_support' => '*-linux freebsd' },
);
%monitor_deprecated = ( 'dnsadmin' => 1 );
@monitor_statuses = ( 'up', 'down', 'un', 'webmin', 'timed', 'isdown' );
$mysql_variant = "mysql";
if (&foreign_check("mysql")) {
# Fix MySQL / MariaDB
&foreign_require("mysql");
if ($mysql::mysql_version =~ /mariadb/i) {
$mysql_variant = "mariadb";
}
}
if ($mysql_variant eq "mariadb") {
$text{'type_mysql'} =~ s/MySQL/MariaDB/g;
}
# list_services()
# Returns a list of all services this module knows how to get status on.
# If this is the first time the function is called a default set of services
# will be setup.
sub list_services
{
my (%mod, @rv);
if (!-d $services_dir) {
# setup initial services
mkdir($module_config_directory, 0700);
mkdir($services_dir, 0700);
system("cp $module_root_directory/services/* $services_dir");
}
map { $mod{$_}++ } &list_modules();
opendir(DIR, $services_dir);
while($f = readdir(DIR)) {
next if ($f !~ /^(.*)\.serv$/);
my $serv = &get_service($1);
next if (!$serv || !$serv->{'type'} || !$serv->{'id'});
if ($serv->{'default'} && $serv->{'id'} eq 'mysql' &&
$mysql_variant eq "mariadb") {
$serv->{'desc'} =~ s/MySQL/MariaDB/g;
}
if ($serv->{'depends'}) {
my $d;
map { $d++ if (!$mod{$_}) } split(/\s+/, $serv->{'depends'});
push(@rv, $serv) if (!$d);
}
else {
push(@rv, $serv);
}
}
closedir(DIR);
return @rv;
}
# get_service(id)
sub get_service
{
my %serv;
read_file("$services_dir/$_[0].serv", \%serv) || return undef;
$serv{'fails'} = 1 if (!defined($serv{'fails'}));
$serv{'_file'} = "$services_dir/$_[0].serv";
if (!defined($serv{'notify'})) {
$serv{'notify'} = 'email pager snmp';
}
$serv{'remote'} = "*" if (!$serv{'remote'} && !$serv{'groups'});
return $_[0] ne $serv{'id'} ? undef : \%serv;
}
# save_service(&serv)
sub save_service
{
my ($serv) = @_;
mkdir($services_dir, 0755) if (!-d $services_dir);
&lock_file("$services_dir/$serv->{'id'}.serv");
&write_file("$services_dir/$serv->{'id'}.serv", $serv);
&unlock_file("$services_dir/$serv->{'id'}.serv");
}
# delete_service(&serv)
sub delete_service
{
my ($serv) = @_;
&unlink_logged("$services_dir/$serv->{'id'}.serv");
&unlink_logged("$history_dir/$serv->{'id'}");
}
# expand_remotes(&service)
# Given a service with direct and group remote hosts, returns a list of the
# names of all actual hosts (* means local)
sub expand_remotes
{
my @remote;
push(@remote, split(/\s+/, $_[0]->{'remote'}));
my @groupnames = split(/\s+/, $_[0]->{'groups'});
if (@groupnames) {
&foreign_require("servers");
my @groups = &servers::list_all_groups();
foreach my $g (@groupnames) {
my ($group) = grep { $_->{'name'} eq $g } @groups;
if ($group) {
push(@remote, @{$group->{'members'}});
}
}
}
return &unique(@remote);
}
# service_status(&service, [from-cgi])
# Gets the status of a service, possibly on another server. If called in
# an array content, the status of all hosts for this monitor are returned.
sub service_status
{
my ($serv, $fromcgi) = @_;
my $t = $serv->{'type'};
my @rv;
foreach $r (&expand_remotes($serv)) {
my $rv;
local $main::error_must_die = 1;
eval {
local $SIG{'ALRM'} = sub { die "status alarm\n" };
alarm(60); # wait at most 60 secs for a result
if ($r ne "*") {
# Make a remote call to another webmin server
&remote_error_setup(\&remote_error);
$remote_error_msg = undef;
&remote_foreign_require($r, 'status', 'status-lib.pl')
if (!$done_remote_status{$r}++);
my $webmindown = $s->{'type'} eq 'alive' ? 0 : -2;
if ($remote_error_msg) {
$rv = { 'up' => $webmindown,
'desc' => "$text{'mon_webmin'} : $remote_error_msg" };
}
else {
my %s = %{$serv};
$s{'remote'} = '*';
$s{'groups'} = undef;
($rv) = &remote_foreign_call($r, 'status',
'service_status', \%s, $fromcgi);
if ($remote_error_msg) {
$rv = { 'up' => $webmindown, 'desc' =>
"$text{'mon_webmin'} : $remote_error_msg" };
}
}
}
elsif ($t =~ /^(\S+)::(\S+)$/) {
# Call to another module
my ($mod, $mtype) = ($1, $2);
$mod = $serv->{'clone'} if ($serv->{'clone'});
&foreign_require($mod, "status_monitor.pl");
$rv = &foreign_call($mod, "status_monitor_status",
$mtype, $serv, $fromcgi);
}
else {
# Just include and use the local monitor library
do "${t}-monitor.pl" if (!$done_monitor{$t}++);
my $func = "get_${t}_status";
$rv = &$func($serv,
$serv->{'clone'} ? $serv->{'clone'} : $t,
$fromcgi);
}
alarm(0);
};
if ($@ =~ /status alarm/) {
push(@rv, { 'up' => -3,
'remote' => $r });
}
elsif ($@) {
# A real error happened
die $@;
}
else {
$rv->{'remote'} = $r;
push(@rv, $rv);
}
}
return wantarray ? @rv : $rv[0];
}
sub remote_error
{
$remote_error_msg = join("", @_);
}
# list_modules()
# Returns a list of all modules available on this system
sub list_modules
{
return map { $_->{'dir'} } grep { &check_os_support($_) }
&get_all_module_infos();
}
# list_handlers()
# Returns a list of the module's monitor type handlers, and those
# defined in other modules. Each is a array ref with ID, description
# and deprecated fields.
sub list_handlers
{
my ($f, @rv);
opendir(DIR, ".");
while($f = readdir(DIR)) {
if ($f =~ /^(\S+)-monitor\.pl$/) {
my $m = $1;
my $oss = $monitor_os_support{$m};
next if ($oss && !&check_os_support($oss));
my $dep = $monitor_deprecated{$m} ? 1 : 0;
push(@rv, [ $m, $text{"type_$m"}, $dep ]);
}
}
closedir(DIR);
foreach my $m (&get_all_module_infos()) {
my $mdir = &module_root_directory($m->{'dir'});
if (-r "$mdir/status_monitor.pl" &&
&check_os_support($m) &&
!$m->{'cloneof'}) {
&foreign_require($m->{'dir'}, "status_monitor.pl");
my @mms = &foreign_call($m->{'dir'}, "status_monitor_list");
push(@rv, map { [ $m->{'dir'}."::".$_->[0], $_->[1], $_->[2] ] } @mms);
}
}
return @rv;
}
# depends_check(&service, [module]+)
sub depends_check
{
my ($serv, @mods) = @_;
return if ($serv->{'id'}); # only check for new services
if ($serv->{'remote'}) {
# Check on the remote server
foreach my $m (@mods) {
&remote_foreign_check($serv->{'remote'}, $m, 1) ||
&error(&text('depends_remote', "<tt>$m</tt>",
"<tt>$serv->{'remote'}</tt>"));
}
}
else {
# Check on this server
foreach my $m (@mods) {
my %minfo = &get_module_info($m);
%minfo || &error(&text('depends_mod', "<tt>$m</tt>"));
&check_os_support(\%minfo, undef, undef, 1) ||
&error(&text('depends_os', "<tt>$minfo{'desc'}</tt>"));
}
$_[0]->{'depends'} = join(" ", @mods);
}
}
# find_named_process(regexp)
sub find_named_process
{
foreach my $p (&proc::list_processes()) {
$p->{'args'} =~ s/\s.*$//; $p->{'args'} =~ s/[\[\]]//g;
if ($p->{'args'} =~ /$_[0]/) {
return $p;
}
}
return undef;
}
# smtp_command(handle, command)
sub smtp_command
{
my ($m, $c) = @_;
print $m $c;
my $r = <$m>;
if ($r !~ /^[23]\d+/) {
&error(&text('sched_esmtpcmd', "<tt>$c</tt>", "<tt>$r</tt>"));
}
}
# setup_cron_job()
# Create a cron job based on the module's configuration
sub setup_cron_job
{
&lock_file($cron_cmd);
&foreign_require("cron");
my $job;
foreach my $j (&cron::list_cron_jobs()) {
$job = $j if ($j->{'user'} eq 'root' &&
($j->{'command'} eq $cron_cmd ||
$j->{'command'} eq $full_cron_cmd));
}
if ($job) {
&lock_file(&cron::cron_file($job));
&cron::delete_cron_job($job);
&unlock_file(&cron::cron_file($job));
unlink($cron_cmd);
}
if ($config{'sched_mode'}) {
# Create the program that cron calls
&cron::create_wrapper($cron_cmd, $module_name, "monitor.pl");
# Setup the actual cron job
my $njob;
$njob = { 'user' => 'root', 'active' => 1,
'hours' => '*', 'days' => '*',
'months' => '*', 'weekdays' => '*',
'command' => $full_cron_cmd };
if ($config{'sched_period'} == 0) {
$njob->{'mins'} = &make_interval(60);
}
elsif ($config{'sched_period'} == 1) {
$njob->{'hours'} = &make_interval(24);
$njob->{'mins'} = 0;
}
elsif ($config{'sched_period'} == 2) {
$njob->{'days'} = &make_interval(31, 1);
$njob->{'hours'} = $njob->{'mins'} = 0;
}
elsif ($config{'sched_period'} == 3) {
$njob->{'months'} = &make_interval(12, 1);
$njob->{'days'} = 1;
$njob->{'hours'} = $njob->{'mins'} = 0;
}
&lock_file(&cron::cron_file($njob));
&cron::create_cron_job($njob);
&unlock_file(&cron::cron_file($njob));
}
&unlock_file($cron_cmd);
}
# make_interval(length, offset2)
sub make_interval
{
my (@rv, $i);
for($i=$config{'sched_offset'}+$_[1]; $i<$_[0]; $i+=$config{'sched_int'}) {
push(@rv,$i);
}
return join(",", @rv);
}
# expand_oldstatus(oldstatus, &serv)
# Converts an old-status string like *=1 foo.com=2 into a hash. If the string
# contains just one number, it is assumed to be for just the first remote host
sub expand_oldstatus
{
my ($o, $serv) = @_;
my @remotes = split(/\s+/, $serv->{'remote'});
if ($o =~ /^\-?(\d+)$/) {
return { $remotes[0] => $o };
}
else {
my %rv;
foreach my $hs (split(/\s+/, $o)) {
my ($h, $s) = split(/=/, $hs);
$rv{$h} = $s;
}
return \%rv;
}
}
# nice_remotes(&monitor)
sub nice_remotes
{
my ($s) = @_;
my @remotes = map { $_ eq "*" ? $text{'index_local'}
: &html_escape($_) }
split(/\s+/, $s->{'remote'});
foreach my $g (split(/\s+/, $s->{'groups'})) {
push(@remotes, &text('index_group', $g));
}
return join("<br>", @remotes);
}
sub group_desc
{
my ($group) = @_;
my $mems = scalar(@{$group->{'members'}});
return $group->{'name'}." (".
&text($mems == 0 ? 'mon_empty' :
$mems == 1 ? 'mon_onemem' : 'mon_members', $mems).")";
}
# list_notification_modes()
# Returns a list of available notification modes (like email, sms, etc..)
sub list_notification_modes
{
my @rv = ( "email" );
if ($config{'pager_cmd'} && $config{'sched_pager'}) {
push(@rv, "pager");
}
if ($config{'snmp_server'}) {
my $gotmod = 0;
eval "use Net::SNMP";
$gotmod++ if (!$@);
eval "use SNMP_Session";
$gotmod++ if (!$@);
push(@rv, "snmp") if ($gotmod);
}
if ($config{'sched_carrier'} && $config{'sched_sms'}) {
push(@rv, "sms");
}
if ($config{'sched_webhook'}) {
push(@rv, "webhook");
}
return @rv;
}
# list_sms_carriers()
# Returns a list of information about carriers to whom we can send SMS
sub list_sms_carriers
{
return ( { 'id' => 'tmobile',
'desc' => 'T-Mobile',
'domain' => 'tmomail.net' },
{ 'id' => 'cingular',
'desc' => 'AT&T SMS',
'domain' => 'txt.att.net' },
{ 'id' => 'attmms',
'desc' => 'AT&T MMS',
'domain' => 'mms.att.net' },
{ 'id' => 'oldcingular',
'desc' => 'Cingular',
'domain' => 'cingularme.com',
'alpha' => 1 },
{ 'id' => 'verizon',
'desc' => 'Verizon',
'domain' => 'vtext.com' },
{ 'id' => 'sprint',
'desc' => 'Sprint PCS',
'domain' => 'messaging.sprintpcs.com' },
{ 'id' => 'nextel',
'desc' => 'Nextel',
'domain' => 'messaging.nextel.com' },
{ 'id' => 'alltel',
'desc' => 'Alltel',
'domain' => 'message.alltel.com' },
{ 'id' => 'boost',
'desc' => 'Boost Mobile',
'domain' => 'myboostmobile.com' },
{ 'id' => 'virgin',
'desc' => 'Virgin Mobile',
'domain' => 'vmobl.com' },
{ 'id' => 'cbell',
'desc' => 'Cincinnati Bell',
'domain' => 'gocbw.com' },
{ 'id' => 'tcom',
'desc' => 'T-COM',
'domain' => 'sms.t-online.de' },
{ 'id' => 'vodafone',
'desc' => 'Vodafone UK',
'domain' => 'vodafone.net' },
{ 'id' => 'vodafonejapan',
'desc' => 'Vodafone Japan',
'domain' => 't.vodafone.ne.jp' },
{ 'id' => 'bellcanada',
'desc' => 'Bell Canada',
'domain' => 'txt.bellmobility.ca' },
{ 'id' => 'bellsouth',
'desc' => 'Bell South',
'domain' => 'sms.bellsouth.com' },
{ 'id' => 'cellularone',
'desc' => 'Cellular One',
'domain' => 'mobile.celloneusa.com' },
{ 'id' => 'o2',
'desc' => 'O2',
'domain' => 'mmail.co.uk' },
{ 'id' => 'rogers',
'desc' => 'Rogers Canada',
'domain' => 'pcs.rogers.com' },
{ 'id' => 'skytel',
'desc' => 'Skytel',
'domain' => 'skytel.com' },
{ 'id' => 'telus',
'desc' => 'Telus Canada',
'domain' => 'msg.telus.com' },
{ 'id' => 'cricket',
'desc' => 'Cricket',
'domain' => 'sms.mycricket.com' },
{ 'id' => 'googlefi',
'desc' => 'Google Fi',
'domain' => 'msg.fi.google.com' },
);
}
# list_templates()
# Returns a list of hash refs, one for each email template
sub list_templates
{
opendir(DIR, $templates_dir) || return ( );
my @rv;
foreach my $f (readdir(DIR)) {
if ($f =~ /^\d+$/) {
push(@rv, &get_template($f));
}
}
closedir(DIR);
return @rv;
}
# get_template(id)
# Returns the hash ref for a specific template, by ID
sub get_template
{
my ($id) = @_;
my %tmpl;
&read_file("$templates_dir/$id", \%tmpl) || return undef;
$tmpl{'id'} = $id;
$tmpl{'file'} = "$templates_dir/$id";
$tmpl{'email'} =~ s/\\n/\n/g;
$tmpl{'email'} =~ s/\\\\/\\/g;
return \%tmpl;
}
# save_template(&template)
# Creates or saves an email template. Also does locking.
sub save_template
{
my ($tmpl) = @_;
$tmpl->{'id'} ||= time().$$;
$tmpl->{'file'} = "$templates_dir/$tmpl->{'id'}";
my %write = %$tmpl;
$write{'email'} =~ s/\\/\\\\/g;
$write{'email'} =~ s/\n/\\n/g;
if (!-d $templates_dir) {
&make_dir($templates_dir, 0755);
}
&lock_file($tmpl->{'file'});
&write_file($tmpl->{'file'}, \%write);
&unlock_file($tmpl->{'file'});
}
# delete_template(&template)
# Removes an existing template. Also does locking.
sub delete_template
{
my ($tmpl) = @_;
&unlink_logged($tmpl->{'file'});
}
# set_monitor_environment(&serv)
# Sets environment variables based on some monitor
sub set_monitor_environment
{
my ($serv) = @_;
foreach my $k (keys %$serv) {
if (!ref($serv->{$k})) {
$ENV{'STATUS_'.uc($k)} = $serv->{$k};
}
}
}
# reset_monitor_environment(&serv)
# Undoes the call to set_monitor_environment
sub reset_monitor_environment
{
my ($serv) = @_;
foreach my $k (keys %$serv) {
if (!ref($serv->{$k})) {
delete($ENV{'STATUS_'.uc($k)});
}
}
}
# list_history(&serv, [max-tail-lines], [max-head-lines])
# Lists history entries for some service. Each is a hash ref with keys
# old - Previous status, in host=status format
# new - New status, in host=status format
# time - Time at which history was logged
# value - Optional value at that time (such as memory used)
# by - Can be 'web' for update from web UI, or 'cron' for background
sub list_history
{
my ($serv, $maxtail, $maxhead) = @_;
my $hfile = "$history_dir/$serv->{'id'}";
return ( ) if (!-r $hfile);
if ($maxtail) {
open(HFILE, "tail -".quotemeta($maxtail)." ".quotemeta($hfile)." |");
}
else {
open(HFILE, "<".$hfile);
}
my @rv;
while(my $line = <HFILE>) {
$line =~ s/\r|\n//g;
my %h = map { split(/=/, $_, 2) } split(/\t+/, $line);
if ($h{'time'}) {
push(@rv, \%h);
}
last if ($maxhead && scalar(@rv) >= $maxhead);
}
close(HFILE);
return @rv;
}
# add_history(&serv, &history)
# Adds a history entry for some service
sub add_history
{
my ($serv, $h) = @_;
if (!-d $history_dir) {
&make_dir($history_dir, '0700');
}
my $hfile = "$history_dir/$serv->{'id'}";
&lock_file($hfile, 1);
my ($first) = &list_history($serv, undef, 1);
my $cutoff = time() - $config{'history_purge'}*24*60*60;
if ($first && $first->{'time'} < $cutoff-(24*60*60)) {
# First entry is more than a day older than the cutoff .. remove all
# entries older than the custoff
my @oldh = &list_history($serv);
&open_tempfile(HFILE, ">$hfile", 0, 1);
foreach my $oh (@oldh) {
if ($oh->{'time'} > $cutoff) {
&print_tempfile(HFILE,
join("\t", map { $_."=".$oh->{$_} } keys %$oh)."\n");
}
}
&close_tempfile(HFILE);
}
&open_tempfile(HFILE, ">>$hfile", 0, 1);
&print_tempfile(HFILE, join("\t", map { $_."=".$h->{$_} } keys %$h)."\n");
&close_tempfile(HFILE);
&unlock_file($hfile, 1);
}
# get_status_icon(up)
# Given a status code, return the image path to it
sub get_status_icon
{
my ($up) = @_;
return &get_webprefix().
"/".$module_name."/images/".($up == 1 ? "up.gif" :
$up == -1 ? "not.gif" :
$up == -2 ? "webmin.gif" :
$up == -3 ? "timed.gif" :
$up == -4 ? "skip.gif" :
"down.gif");
}
sub status_to_string
{
my ($up) = @_;
return $up == 1 ? $text{'mon_up'} :
$up == -1 ? $text{'mon_not'} :
$up == -2 ? $text{'mon_webmin'} :
$up == -3 ? $text{'mon_timeout'} :
$up == -4 ? $text{'mon_skip'} :
"<font color=#ff0000>$text{'mon_down'}</font>";
}
sub get_services_current_status
{
my (@serv) = @_;
my %monitors;
foreach my $s (@serv) {
my $title = &html_escape($s->{'desc'});
my %monitor;
$monitor{'title'} = $title;
my @stats = &service_status($s, 1);
if ($s->{'depend'}) {
my $ds = &get_service($s->{'depend'});
if ($ds) {
my @dstats = &service_status($ds, 1);
if ($dstats[0]->{'up'} != 1) {
@stats = map {{ 'up' => -4 }} @stats;
}
}
}
my @ups = map {$_->{'up'}} @stats;
my @remotes = map {$_->{'remote'}} @stats;
for (my $i = 0; $i < @ups; $i++) {
my $up = $ups[$i];
my $h = $remotes[$i];
$h = $text{'index_local'} if ($h eq '*');
$monitor{'status'} = $up;
$monitor{'status_text'} = &status_to_string($h);
$monitor{'icon'} = &get_status_icon($up);
$monitor{'icon_title'} = &html_escape($h);
}
$monitors{$s->{'id'}} = \%monitor;
}
return %monitors;
}
1;