#! /usr/bin/perl -T
# control.pl 1.2 for LMon, anders@bsdconsulting.no, 2005-05-19
# External requirements: Config::IniFiles.
#
# Copyright (c) 2005, Anders Nordby <anders@bsdconsulting.no>
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 
# * Neither the name of BSD Consulting nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package LMonControl;
use Config::IniFiles;
use FindBin qw($Bin);
use Getopt::Std;
use POSIX;
use strict;
use IPC::Open3;

$ENV{PATH} = "/sbin:/bin:/usr/sbin:/usr/bin";
$ENV{ENV} = "";

$LMonControl::gensect = "general";
$LMonControl::logvar = "log";
$LMonControl::rulevar = "rules";
$LMonControl::namevar = "name";
$LMonControl::modevar = "mode";
$LMonControl::sysnamevar = "sysname";
$LMonControl::buffervar = "buffer";
$LMonControl::fromvar = "from";
$LMonControl::tovar = "to";
$LMonControl::mailserversvar = "mailservers";
$LMonControl::cmdok = '([-\w\.\/\ \"\@\;\_]+)';
$LMonControl::fnok = '([-\w\.\_]+)';

$LMonControl::commname = "^perl";
$LMonControl::argcheck = "lmon\.pl";
$LMonControl::cfgfile = "$Bin/control.cfg";
$LMonControl::oscheck = '^(SunOS|Linux|FreeBSD)$';

die("ERROR: Unknown operating system") unless ((POSIX::uname)[0] =~ /$LMonControl::oscheck/);

use vars qw { $opt_i };

sub readconfig {
	if (! -f $LMonControl::cfgfile) {
		die("ERROR: Config file $LMonControl::cfgfile not found");
	} elsif (! -T $LMonControl::cfgfile) {
		die("ERROR: Config file $LMonControl::cfgfile is not a text file");
	} else {
		tie %LMonControl::cfg, 'Config::IniFiles', ( -file => $LMonControl::cfgfile );
	}
}

sub usage {
	print "Usage: control.pl [options] <keyword>\n\n";
	print "\tlist\t\t\tlist configured instances\n";
	print "\tstart\t\t\tstart all configured instances\n";
	print "\tstart -i <instance>\tstart given instances\n";
	print "\tstop\t\t\tstop all configured instances\n";
	print "\tstop -i <instance>\tstop given instances\n";
	print "\tstatus\t\t\tprint status for all configured instances\n";
	print "\tstatus -i <instance>\tprint status for given instance\n";
	exit(1);
}

sub pscmd {
	# $_[0]: cmd $_[1]: extra (optional)
	my $cmd = $_[0];
	my $extracmd = $_[1];
	if ($cmd =~ /^$LMonControl::cmdok$/) {
		$cmd = $1;
		$cmd .= " 2>/dev/null | tail +2";
		if ($extracmd) { $cmd .= " " . $extracmd; }
		return `$cmd`;
	} else {
		return "";
	}
}

sub instpid {
# $_[0]: inst
	my $inst = $_[0];
	my $pidfile = $Bin . "/" . $inst . ".pid";
	my $username = getpwuid(getuid);

	if (! -T $pidfile) {
		# PID file missing or not a text file
		return 0;
	} else {
		open(PID, $pidfile);
		my $regpid = <PID>;
		close(PID);
		chomp($regpid);
		if (!int($regpid)) {
			return 0;
		} else {
			my $psuser;
			my $pspid;
			my $pscomm;
			my $psargs;
			for ((POSIX::uname)[0]) {
				/^(FreeBSD|Linux)$/	and do {
					$psuser = pscmd("ps -p $regpid -oruser");
					$psuser =~ s@\s+$@@g;
					$pspid = pscmd("ps -p $regpid -opid");
					$pspid =~ s@\s+$@@g;
					$pscomm = pscmd("ps -c -p $regpid -ocommand");
					$pscomm =~ s@\s+$@@g;
					$psargs = pscmd("ps -ww -p $regpid -ocommand");
					$psargs =~ s@\s+$@@g;
					next;
				};
				/^SunOS$/	and do {
					$psuser = pscmd("ps -p $regpid -oruser", "| awk '{print \$1}'");
					$pspid = pscmd("ps -p $regpid -opid");
					$pscomm = pscmd("ps -p $regpid -ocomm", "| xargs -n 1 basename");
					$psargs = pscmd("ps -p $regpid -oargs");
					next;
				};
			}
			chomp($psuser);
			chomp($pspid);
			chomp($pscomm);
			chomp($psargs);

			if (int($pspid) && $regpid == $pspid && $psuser eq "$username" && $pscomm =~ /$LMonControl::commname/ && $psargs =~ /$LMonControl::argcheck/) {
				return($regpid);
			} else {
				return(0);
			}
		}
	}
}

sub statusinst {
# $_[0]: instance name
	my $inst = $_[0];
	my $pid = instpid($inst);
	if ($pid) {
		print "$inst up, PID $pid.\n";
	} else {
		print "$inst down.\n";
	}
}

sub startinst {
# $_[0]: instance name
	my $inst = $_[0];

	print "Start lmon.pl instance $_[0]: ";
	if (!instpid($inst)) {
		my $cmd = "cd $Bin; ./lmon.pl -r \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"} . "\" -f \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::logvar"} . "\" -p \"" . $inst . ".pid\" -d";

		if (exists $LMonControl::cfg{"$inst"}{"$LMonControl::modevar"}) {
			if ($LMonControl::cfg{"$inst"}{"$LMonControl::modevar"} eq "include") {
				$cmd .= " -i";
			}
		} elsif (exists $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::modevar"} && $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::modevar"} eq "include") {
				$cmd .= " -i";
		}

		if (exists $LMonControl::cfg{"$inst"}{"$LMonControl::sysnamevar"}) {
				$cmd .= " -s \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::sysnamevar"} . "\"";
		} elsif (exists $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::sysnamevar"}) {
				$cmd .= " -s \"" . $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::sysnamevar"} . "\"";
		}

		if (exists $LMonControl::cfg{"$inst"}{"$LMonControl::fromvar"}) {
				$cmd .= " -F \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::fromvar"} . "\"";
		} elsif (exists $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::fromvar"}) {
				$cmd .= " -F \"" . $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::fromvar"} . "\"";
		}

		if (exists $LMonControl::cfg{"$inst"}{"$LMonControl::tovar"}) {
				$cmd .= " -t \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::tovar"} . "\"";
		} elsif (exists $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::tovar"}) {
				$cmd .= " -t \"" . $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::tovar"} . "\"";
		}

		if (exists $LMonControl::cfg{"$inst"}{"$LMonControl::mailserversvar"}) {
				$cmd .= " -m \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::mailserversvar"} . "\"";
		} elsif (exists $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::mailserversvar"}) {
				$cmd .= " -m \"" . $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::mailserversvar"} . "\"";
		}

		if (exists $LMonControl::cfg{"$inst"}{"$LMonControl::buffervar"}) {
				$cmd .= " -b \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::buffervar"} . "\"";
		} elsif (exists $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::buffervar"}) {
				$cmd .= " -b \"" . $LMonControl::cfg{"$LMonControl::gensect"}{"$LMonControl::buffervar"} . "\"";
		}
		if (exists $LMonControl::cfg{"$inst"}{"$LMonControl::namevar"}) {
				$cmd .= " -n \"" . $LMonControl::cfg{"$inst"}{"$LMonControl::namevar"} . "\"";
		}

		if (!exists $LMonControl::cfg{"$inst"}{"$LMonControl::logvar"}) {
			print "FAIL, instance $inst needs configuration parameter " . $LMonControl::logvar . ".\n";
		} elsif (! -f $LMonControl::cfg{"$inst"}{"$LMonControl::logvar"}) {
			print "FAIL, log file " . $LMonControl::cfg{"$inst"}{"$LMonControl::logvar"} . " does not exist.\n";
		} elsif (! -f $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"}) {
			print "FAIL, rule file " . $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"} . " does not exist.\n";
		} elsif (! -T $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"}) {
			print "FAIL, rule file " . $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"} . " is not a text file.\n";
		} else {
#			print "TRY: $cmd\n";

# untaint $cmd if characters are OK, sort-of
			if ($cmd =~ /^$LMonControl::cmdok$/) {
				$cmd = $1;

				local(*IN, *OUT, *ERR);
				my $mypid = open3(*IN, *OUT, *ERR, $cmd);
				close(IN);

				my $errdata;
				while (<ERR>) {
					$errdata .= $_;
				}
				close(ERR);
				close(OUT);
				waitpid($mypid, 0);
				my $ret = $?;

				if ($ret == 0) {
					print "OK.\n";
				} else {
					print "FAIL, startup problems:\n\n$errdata\n";
				}
			} else {
				my $badargs = $cmd;
				$badargs =~ s@$LMonControl::cmdok@@g;
				print "FAIL, invalid argument characters\nin command for instance $inst (see cmdok setting or disable\n-T option for Perl): $badargs.\n";
			}
		}
	} else {
		print "FAIL, already running.\n";
	}
}

sub stopinst {
# $_[0]: instance name
	my $inst = $_[0];

	print "Stop lmon.pl instance $_[0]: ";
	my $pid = instpid($inst);

	if (int($pid)) {
		if ($pid =~ /^(\d+)$/) {
			# untaint PID
			$pid = $1;

			kill 'TERM', $pid;
			my $pidfile = $inst . ".pid";
			if ($pidfile =~ /^$LMonControl::fnok$/) {
				# Untaint pidfile filename
				$pidfile = $1;
				if (-e $pidfile) { unlink($pidfile) };
			}
			print "DONE\n";
		} else {
			print "no valid PID number found.\n";
		}
	} else {
		print "not running.\n";
	}
}

sub fixconfig {
	my $inst;
	foreach $inst (keys %LMonControl::cfg) {
		next if ($inst =~ /^$LMonControl::gensect$/);

		# Fix the rule file filename/path
		if (!exists $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"}) {
			# No rule file specified, use default: <instance>.rules in $Bin
			$LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"} = $Bin . "/" . $inst . ".rules";
		} elsif (! -f $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"}) {
			# Rule file not found, add $Bin before filename
			$LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"} = $Bin . "/" . $LMonControl::cfg{"$inst"}{"$LMonControl::rulevar"};
		}
	}
}

sub ckpids {
	opendir(DIR, $Bin);
	my @dirfiles = readdir DIR;
	closedir(DIR);
	my $dirfile;
	foreach $dirfile (@dirfiles) {
		next if ($dirfile !~ /\.pid$/);
		my $inst = $dirfile;
		$inst =~ s@\.pid$@@;
		my $pidfile = $Bin . "/" . $dirfile;

		if (! -T $pidfile) {
			print "WARNING: PID file $dirfile found in program dir. Not even a valid text file!\n";
		} elsif (!exists $LMonControl::cfg{"$inst"}) {
			print "WARNING: PID file $dirfile found in program dir, but $inst is not in config.\n";
		} else {
			my $dirpid = $Bin . "/" . $dirfile;
			open(PID, $dirpid);
			my $regpid = <PID>;
			close(PID);
			if (!int($regpid)) {
				print "WARNING: PID file $dirfile found in program dir, but has no number.\n";
			} else {
				my $ckpid = instpid($inst);
				if ($ckpid && $ckpid != $regpid) {
					print "WARNING: PID file $dirfile found in program dir, but has unexpected PID.\n";
				}
			}
		}
	}
}

if ($ARGV[0]) {
	if ($ARGV[0] =~ /^(start|stop|status)$/) {

		my $keyword = $ARGV[0];
		@ARGV = grep(!/^(start|stop|status)/, @ARGV);
		push(@ARGV, $keyword);
	}
} else {
	usage;
}

getopts('i:');
my $inst;

for ($ARGV[0]) {
	/^start$/	and do {
		readconfig; fixconfig;
		if ($opt_i) {
			if ($opt_i eq $LMonControl::gensect) {
				print "Could not start invalid instance $opt_i.\n";
			} elsif (exists $LMonControl::cfg{"$opt_i"}) {
				startinst($opt_i);
			} else {
				print "Could not start instance $opt_i, does not exist in configuration.\n";
			}
		} else {
			foreach $inst (keys %LMonControl::cfg) {
				next if ($inst =~ /^$LMonControl::gensect$/);
				startinst($inst);
			}
		}
		next;
	};
	/^stop$/	and do {
		readconfig;
		if ($opt_i) {
			if (exists $LMonControl::cfg{"$opt_i"}) {
				stopinst($opt_i);
			} else {
				print "Could not stop instance $opt_i, does not exist in configuration.\n";
			}
		} else {
			foreach $inst (keys %LMonControl::cfg) {
				next if ($inst =~ /^$LMonControl::gensect$/);
				stopinst($inst);
			}
		}
		next;
	};
	/^list$/	and do {
		readconfig;
		print "Instances:\n\n";
		foreach $inst (keys %LMonControl::cfg) {
			next if ($inst =~ /^$LMonControl::gensect$/);
			print "$inst\n";
		}
		next;
	};
	/^status$/	and do {
		readconfig;
		if ($opt_i) {
			if (exists $LMonControl::cfg{"$opt_i"}) {
				statusinst($opt_i);
			} else {
				print "Will not check status for instance $opt_i,\ndoes not exist in configuration.\n";
			}
		} else {
			foreach $inst (keys %LMonControl::cfg) {
				next if ($inst =~ /^$LMonControl::gensect$/);
				statusinst($inst);
			}
			ckpids;
		}
		ckpids;
		next;
	};
	usage;
}


syntax highlighted by Code2HTML, v. 0.9.1