#!/usr/bin/env perl # # Monitorix - A lightweight system monitoring tool. # # Copyright (C) 2005-2022 by Jordi Sanfeliu # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. # require 5.006; use strict; use warnings; use FindBin qw($Bin); use lib $Bin . "/lib", "/usr/lib/monitorix"; use Monitorix; use POSIX qw(WNOHANG LC_TIME setlocale uname pause setsid); use Config::General; use Getopt::Std; use Cwd qw(abs_path); use Fcntl qw(:flock); # Force a standard locale $ENV{LANG} = ""; setlocale(LC_TIME, "C"); $SIG{'INT' } = 'INT_handler'; $SIG{'ABRT'} = 'INT_handler'; $SIG{'QUIT'} = 'INT_handler'; $SIG{'TRAP'} = 'INT_handler'; $SIG{'STOP'} = 'INT_handler'; $SIG{'TERM'} = 'INT_handler'; $SIG{'HUP' } = 'HUP_handler'; use constant VERSION => "3.15.0"; use constant RELDATE => "07-Dec-2022"; my @suppsys = ("Linux", "FreeBSD", "OpenBSD", "NetBSD"); our %config; our %options; sub INT_handler { my ($signal) = @_; logger("SIG$signal caught."); flush_accounting_rules(\%config, $options{d}) unless $options{u}; if(lc($config{httpd_builtin}->{enabled} || "") eq "y") { kill(15, $config{httpd_pid}); } logger("Exiting."); exit(0); } sub HUP_handler { my ($signal) = @_; my $myself = (caller(0))[3]; return if $options{n}; # only continue if it's a daemon my (undef, undef, $uid) = getpwnam($config{httpd_builtin}->{user}); my (undef, undef, $gid) = getgrnam($config{httpd_builtin}->{group}); logger("SIG$signal caught."); # upon receiving SIGHUP a new logfile is opened if($config{log_file}) { close(STDOUT); close(STDERR); open(STDOUT, ">> $config{log_file}") || logger("Can't write to LOG: $!"); open(STDERR, ">> $config{log_file}") || logger("Can't write to LOG: $!"); chmod(0600, $config{log_file}); logger("$myself: opening a new log file."); } # restart HTTP built-in if(lc($config{httpd_builtin}->{enabled} || "") eq "y") { if(defined($config{httpd_pid})) { require HTTPServer; kill(15, $config{httpd_pid}); kill(9, $config{httpd_pid}); httpd_setup(\%config, $options{u}); logger("Restarted HTTP built-in server (pid $config{httpd_pid}).") if (defined($config{httpd_pid})); } } } sub daemonize { if($config{log_file}) { open(STDIN, "< /dev/null") || die "Can't read /dev/null: $!"; open(STDOUT, ">> $config{log_file}") || die "Can't write to LOG: $!"; } umask(022) || die "Unable to umask 022: $!"; exit if fork(); # parent exits (setsid() != -1) || die "Can't start a new session: $!"; if($config{log_file}) { open(STDERR, ">> $config{log_file}") || die "Can't write to LOG: $!"; } } sub usage { print(STDERR << "EOF"); Usage: monitorix -c configfile [-p pidfile] [-d none | graph[,graph] | all ] [-v] [-n] [-u] [-s splitpolicy] [-e report=,graphs=[,,...],to=] EOF exit(1); } sub create_index { my $myself = (caller(0))[3]; my $n; my $gname; my $piwik_code = ""; my ($piwik_url, $piwik_sid, $piwik_img); my $text_when_all; my $value_when_all; my $when_all_code = ""; # keep backwards compatibility for v3.2.1 and less if(ref($config{theme}) ne "HASH") { logger("$myself: WARNING: the option is not valid. Please consider upgrading your configuration file. Defaulting to white theme."); delete($config{theme}); $config{theme}->{white}->{main_bg} = "FFFFFF"; $config{theme}->{white}->{main_fg} = "000000"; $config{theme}->{white}->{title_bg} = "777777"; $config{theme}->{white}->{title_fg} = "CCCC00"; $config{theme}->{white}->{graph_bg} = "CCCCCC"; $config{theme_color} = "white"; } my $theme = $config{theme_color} || "white"; if(!$config{theme}->{$theme}) { logger("$myself: ERROR: invalid value in 'theme_color' option. Defaulting to white theme."); $theme = $config{theme_color} = "white"; } # Default option in 'Graph' list when 'All' is selected in 'Hostname' list. # # The following small JavaScript function is intended to avoid to select to view # accidentally (unless explicitly selected by the user) a huge amount of remote # images that will hang the browser for a while. $text_when_all = $config{multihost}->{default_option_when_all} || ""; $text_when_all = "System load" if !$text_when_all; my @match = grep { $config{graphs}{$_} eq $text_when_all } keys %{$config{graphs}}; $value_when_all = $match[0] || ""; if(!$value_when_all) { logger("$myself: ERROR: invalid value in 'default_option_when_all' option ('$text_when_all')."); $value_when_all = "_system1"; } $when_all_code = <<"EOF"; EOF # Piwik tracking code if(lc($config{piwik_tracking}->{enabled} || "") eq "y") { $piwik_url = $config{piwik_tracking}->{url} || ""; $piwik_sid = $config{piwik_tracking}->{sid} || ""; $piwik_img = $config{piwik_tracking}->{img} || ""; $piwik_code = <<"EOF"; EOF } # force to only one trailing slash (my $base_url = $config{base_url}) =~ s/\/*$/\//; (my $base_cgi = $config{base_cgi}) =~ s/\/*$/\//; if(!open(OUT, "> $config{base_dir}/index.html")) { die "Unable to create '${config{base_dir}}index.html': $!"; } my $css = sprintf("%scss/%s.css", $base_url, $theme); print(OUT < $config{title} $piwik_code $when_all_code


v@{[VERSION]}

EOF print(OUT " \n"); print(OUT " \n"); print(OUT <
 Hostname   Graph 
\n"); print(OUT " \n"); print(OUT " \n"); print(OUT " \n"); print(OUT "

EOF if(lc($config{enable_hourly_view} || "") eq "y" ) { print(OUT < EOF } print(OUT < EOF if($config{max_historic_years} > 1) { print(OUT " \n"); } for($n = 2; $n <= $config{max_historic_years}; $n++) { if($n > 2 && (($n - 2) % 4) == 0) { print(OUT " \n"); print(OUT " \n"); } print(OUT < EOF } if($config{max_historic_years} > 1) { print(OUT " \n"); } print(OUT <

EOF close(OUT); } # Main # ---------------------------------------------------------------------------- getopts("c:p:d:vnus:e:", \%options) || usage(); if($options{v}) { print("Monitorix version " . VERSION . " (" . RELDATE . ")\n"); print("by Jordi Sanfeliu \n"); print("https://www.monitorix.org/\n\n"); exit(0); } if(!$options{c}) { usage(); exit(1); } $options{c} = (abs_path($options{c}) || $options{c}) unless $^V lt 5.6.2; if(!stat($options{c})) { die "Can't open file '$options{c}': $!"; } # load main configuration file $options{s} = "guess" if !defined($options{s}); my $conf = new Config::General( -ConfigFile => $options{c}, -SplitPolicy => $options{s}, ); %config = $conf->getall; $config{debug} = (); $config{func_update} = (); # get the current OS and kernel version and check its support my $release; ($config{os}, undef, $release) = uname(); if(!($release =~ m/^(\d+)\.(\d+)/)) { die "FATAL: unable to get the kernel version."; } $config{kernel} = "$1.$2"; if(!grep {$_ eq $config{os}} @suppsys) { die "FATAL: your operating system ($config{os}) is not supported."; } if(grep {$_ eq $config{os}} ("FreeBSD", "OpenBSD", "NetBSD")) { $SIG{'CHLD'} = 'DEFAULT'; } $0 = sprintf("%s %s%s%s%s%s%s", $^V lt 5.6.2 ? "monitorix" : abs_path($0), $options{c} ? "-c $options{c}" : "", $options{p} ? " -p $options{p}" : "", $options{d} ? " -d $options{d}" : "", $options{v} ? " -v" : "", $options{n} ? " -n" : "", $options{u} ? " -u" : ""); daemonize() unless $options{n}; logger("Starting Monitorix version " . VERSION . " (pid $$)."); logger("Loaded main configuration file '$options{c}'."); # save the pidfile if($options{p}) { $options{p} = abs_path($options{p}); open(OUT, "> $options{p}") || die "Could not open '$options{p}' for writing: $!"; print(OUT "$$"); close(OUT); } # change to a safety directory unless(chdir("/tmp")) { die "Can't chdir to /tmp: $!"; } if($options{d}) { if($options{d} ne "none" && $options{d} ne "all") { @{$config{debug}} = split(',', $options{d}); foreach my $t (@{$config{debug}}) { if(!grep {trim($_) eq $t} (split(',', $config{graph_name} . ", traffacct, emailreports"))) { die "Invalid debug key '$t'"; } } } logger("Entering in debug mode."); logger("Changed process name to '$0'."); } # load additional configuration files if($config{include_dir} && opendir(DIR, $config{include_dir})) { my @files = grep { !/^[.]/ } readdir(DIR); closedir(DIR); foreach my $c (sort @files) { next unless -f $config{include_dir} . "/$c"; next unless $c =~ m/\.conf$/; logger("Loading extra configuration file '$config{include_dir}/$c'."); my $conf_inc = new Config::General( -ConfigFile => $config{include_dir} . "/$c", -SplitPolicy => $options{s}, ); my %config_inc = $conf_inc->getall; while(my ($key, $val) = each(%config_inc)) { if(ref($val) eq "HASH") { # two level options (a subsection) while(my ($key2, $val2) = each(%{$val})) { # delete first this whole subsection delete($config{$key}->{$key2}); if(ref($val2) eq "HASH") { # three level options (a subsubsection) while(my ($key3, $val3) = each(%{$val2})) { $config{$key}->{$key2}->{$key3} = $val3; delete $config_inc{$key}->{$key2}->{$key3}; } next; } $config{$key}->{$key2} = $val2; delete $config_inc{$key}->{$key2}; } next; } # graph_name option is special #if($key eq "graph_name") { # $config{graph_name} .= ", $val"; # delete $config_inc{$key}; # next; #} if($key eq "additional_graph_name") { $config{graph_name} .= ", $val"; delete $config_inc{$key}; next; } # one level options $config{$key} = $val; delete $config_inc{$key}; } } } else { logger("Can't read directory '$config{include_dir}'. $!") if $config{include_dir}; } # make sure that some options are correctly defined die "ERROR: 'base_dir' option does not exist.\n" if(!defined($config{base_dir})); die "ERROR: 'base_lib' option does not exist.\n" if(!defined($config{base_lib})); die "ERROR: 'base_url' option does not exist.\n" if(!defined($config{base_url})); die "ERROR: 'base_cgi' option does not exist.\n" if(!defined($config{base_cgi})); if(!$config{max_historic_years}) { logger("WARNING: the 'max_historic_years' option doesn't exist. Please consider upgrading your configuration file."); $config{max_historic_years} = 1; } if(!$config{net}->{max}) { logger("WARNING: the 'max' option in 'net.pm' doesn't exist. Please consider upgrading your configuration file."); $config{net}->{max} = 10; } if(!$config{global_zoom}) { logger("WARNING: the 'global_zoom' option is not valid or doesn't exist. Please consider upgrading your configuration file."); # it's set in 'monitorix.cgi' } if(!$config{image_format}) { logger("WARNING: the 'image_format' option is not valid or doesn't exist. Please consider upgrading your configuration file."); # it's set in 'monitorix.cgi' } if(!defined($config{logo_top_url})) { logger("WARNING: the 'logo_top_url' option doesn't exist. Please consider upgrading your configuration file."); $config{logo_top_url} = "https://www.monitorix.org/"; } if(lc($config{httpd_builtin}->{enabled} || "") eq "y" && !$config{httpd_builtin}->{autocheck_responsiveness}) { logger("WARNING: the 'autocheck_responsiveness' option is not valid or doesn't exist. Please consider upgrading your configuration file."); $config{httpd_builtin}->{autocheck_responsiveness} = "y"; } if(lc($config{multihost}->{enabled} || "") eq "y" && !$config{multihost}->{default_option_when_all}) { logger("WARNING: the 'default_option_when_all' option is not valid or doesn't exist. Please consider upgrading your configuration file."); $config{multihost}->{default_option_when_all} = "System load"; } if(!$config{use_external_firewall}) { $config{use_external_firewall} = "n"; } # one-shot for the 'emailreports' module if($options{e}) { my $em_report; my $em_graphs; my $em_to; my $em_when; foreach (split(',', $options{e})) { my $em = trim($_); if($em =~ m/^report=(hourly|daily|weekly|monthly|yearly)$/) { $em_report=$1; } if($em =~ m/^graphs=(\S+)$/) { $em_graphs=$1; $em_graphs =~ s/\+/\,/g; } if($em =~ m/^to=(\S+)$/) { $em_to=$1; } } die "Invalid or not defined time frame in 'report='." unless $em_report; die "Unspecified graph names in 'graphs='." unless $em_graphs; die "Unspecified destination email address in 'to='." unless $em_to; my $emailreports = $config{emailreports}; $emailreports->{$em_report}->{graphs} = $em_graphs; $emailreports->{$em_report}->{to} = $em_to; my $d = "emailreports"; undef($d) if(!grep {trim($_) eq $d} (@{$config{debug}})); eval "use emailreports qw(emailreports_send)"; if($@) { die "WARNING: unable to load module 'emailreports'. $@"; } $em_when = "1hour" if $em_report eq "hourly"; $em_when = "1day" if $em_report eq "daily"; $em_when = "1week" if $em_report eq "weekly"; $em_when = "1month" if $em_report eq "monthly"; $em_when = "1year" if $em_report eq "yearly"; eval { emailreports::emailreports_send(\%config, $em_report, $em_when, $d); }; if($@) { logger("emailreports::emailreports_send(): $@"); } logger("Done."); exit(0); } # save the path of the configuration file if(open(OUT, "> " . $config{base_dir} . "/cgi/monitorix.conf.path")) { print(OUT "$options{c}\n"); print(OUT "$options{s}\n"); close(OUT); } else { logger("WARNING: unable to create the file '$config{base_dir}/cgi/monitorix.conf.path'. $!"); } if($config{os} eq "Linux" && !$options{u}) { # make sure that 'ip_default_table' option has a consistent value $config{ip_default_table} = "filter" if !$config{ip_default_table}; # check 'iptables' version to include the new wait lock option '--wait' my $ipv = `iptables --version`; chomp($ipv); $ipv =~ s/^iptables v//; $ipv = sprintf("%03s.%03s.%03s", split('\.', $ipv)); if($ipv gt "001.004.020") { # 1.4.20 $config{iptables_wait_lock} = " --wait "; } else { $config{iptables_wait_lock} = ""; } } # make sure that there aren't residual Monitorix iptables rules flush_accounting_rules(\%config, $options{d}) unless $options{u}; logger("Initializing graphs."); # check for inconsistencies between enabled graphs and defined graphs foreach my $g (sort keys %{$config{graph_enable}}) { if(lc($config{graph_enable}->{$g} || "") eq "y") { if(!grep {trim($_) eq $g} (split(',', $config{graph_name}))) { logger("WARNING: inconsitency between '' and 'graph_name' options; '$g' doesn't exist."); } } } # initialize all enabled graphs foreach (split(',', ($config{graph_name} || "") . ", traffacct")) { my $g = trim($_); my $e = "n"; if($g eq "traffacct") { $e = lc($config{$g}->{enabled} || ""); } else { $e = lc($config{graph_enable}->{$g} || ""); } if($e eq "y") { my $init = $g . "_init"; my $d = $g; undef($d) if(!grep {trim($_) eq $d} (@{$config{debug}})); if(defined($options{d}) && $options{d} eq "all") { $d = $g; } eval "use $g qw(" . $init . " " . $g . "_update)"; if($@) { logger("WARNING: unable to load module '$g'. $@"); next; } { no strict "refs"; eval { &$init($g, \%config, $d); }; } logger("WARNING: unexpected errors in function $init().") if($@); } } if(!scalar($config{func_update})) { logger("Nothing to do, exiting."); exit(0); } logger("Generating the 'index.html' file."); create_index(); # start the HTTP built-in (if enabled) if(lc($config{httpd_builtin}->{enabled} || "") eq "y") { if(!$options{u}) { logger("Setting owner/group and permission bits for the imgs/ directory.") if defined($options{d}); my (undef, undef, $uid) = getpwnam($config{httpd_builtin}->{user}); my (undef, undef, $gid) = getgrnam($config{httpd_builtin}->{group}); chown($uid, $gid, $config{base_dir} . "/" . $config{imgs_dir}); chmod(0755, $config{base_dir} . "/" . $config{imgs_dir}); } require HTTPServer; httpd_setup(\%config, $options{u}); logger("Started HTTP built-in server (pid $config{httpd_pid}).") if (defined($config{httpd_pid})); } setpriority(0, 0, $config{priority} || 0); logger("Ok, ready."); # main loop while(1) { select undef, undef, undef, 1.0; my ($sec, $min, $hour, $mday, $mon, undef, $wday) = localtime(time); # call to all enabled graphs on every minute if($sec == 0) { my $lockfile_handler = lockfile_handler(\%config); global_flock($lockfile_handler, LOCK_EX); foreach my $f (@{$config{func_update}}) { my $update = $f . "_update"; my $d = $f; logger("Calling $update()") unless !$options{d}; undef($d) if(!grep {trim($_) eq $d} (@{$config{debug}})); if(defined($options{d}) && $options{d} eq "all") { $d = $f; } { no strict "refs"; eval { &$update($f, \%config, $d); }; if($@) { logger("$update(): $@"); } } } global_flock($lockfile_handler, LOCK_UN); # TRAFFACCT graph daily reports if(lc($config{traffacct}->{enabled} || "") eq "y") { my $d = "traffacct"; undef($d) if(!grep {trim($_) eq $d} (@{$config{debug}})); # at 00:00h if($min == 0 && $hour == 0) { # collect traffic accounting every day eval { traffacct::traffacct_getcounters(\%config, $d); }; if($@) { logger("traffacct::traffacct_getcounters(): $@"); } # send reports every first day of a month if(lc($config{traffacct}->{reports}->{enabled} || "") eq "y") { if($mday == 1) { eval { traffacct::traffacct_sendreports(\%config, $d); }; if($@) { logger("traffacct::traffacct_sendreports(): $@"); } } } } } # Scheduled Email Reports if(lc($config{emailreports}->{enabled} || "") eq "y") { my $emailreports = $config{emailreports}; my $m = $emailreports->{minute} || 0; my $h = $emailreports->{hour} || 0; my $d = "emailreports"; undef($d) if(!grep {trim($_) eq $d} (@{$config{debug}})); if($min == $m && $hour == $h) { eval "use emailreports qw(emailreports_send)"; if($@) { logger("WARNING: unable to load module 'emailreports'. $@"); next; } # daily if(lc($emailreports->{daily}->{enabled} || "") eq "y") { eval { emailreports::emailreports_send(\%config, "daily", "1day", $d); }; if($@) { logger("emailreports::emailreports_send(): $@"); } } # weekly (send reports on every Monday) if($wday == 1) { if(lc($emailreports->{weekly}->{enabled} || "") eq "y") { eval { emailreports::emailreports_send(\%config, "weekly", "1week", $d); }; if($@) { logger("emailreports::emailreports_send(): $@"); } } } # monthly (send reports every first day of each month) if($mday == 1) { if(lc($emailreports->{monthly}->{enabled} || "") eq "y") { eval { emailreports::emailreports_send(\%config, "monthly", "1month", $d); }; if($@) { logger("emailreports::emailreports_send(): $@"); } } } # yearly (send reports every first day of each year) if($mon == 0 && $mday == 1) { if(lc($emailreports->{yearly}->{enabled} || "") eq "y") { eval { emailreports::emailreports_send(\%config, "yearly", "1year", $d); }; if($@) { logger("emailreports::emailreports_send(): $@"); } } } } } # check if the HTTP built-in server is responsive if(lc($config{httpd_builtin}->{enabled} || "") eq "y") { if(lc($config{httpd_builtin}->{autocheck_responsiveness} || "") eq "y") { use LWP::UserAgent; my $pid = fork(); if(!$pid) { my $host = $config{httpd_builtin}->{host} ? $config{httpd_builtin}->{host} : "localhost"; my $url = "http://" . $host . ":" . $config{httpd_builtin}->{port} . $config{base_url}; my $ua = LWP::UserAgent->new(timeout => 30); my $response = $ua->request(HTTP::Request->new('GET', $url)); if(!$response->is_success) { if($response->status_line ne "401 Access Denied") { logger("WARNING: HTTP built-in server not responding at '$url'."); exit(1); } } exit(0); } waitpid($pid, 0); # waitpid sets the child's status in $? if($? != 0) { # restart HTTP built-in server if(defined($config{httpd_pid})) { require HTTPServer; kill(15, $config{httpd_pid}); kill(9, $config{httpd_pid}); httpd_setup(\%config, $options{u}); logger("Restarted HTTP built-in server (pid $config{httpd_pid}).") if defined($config{httpd_pid}); } } } } } waitpid(-1, WNOHANG); }