#!/usr/bin/perl -w
############################################################
# $Id: psmon-config,v 1.5 2005/05/06 12:50:20 nicolaw Exp $
# psmon.conf - Example psmon Configuration File
# Copyright: (c)2002,2003,2004,2005 Nicola Worthington. All rights reserved.
############################################################
# This file is part of psmon.
#
# psmon 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.
#
# psmon 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 psmon; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
############################################################

use strict;
use warnings;
use English;
use Term::ReadLine ();
use Proc::ProcessTable ();
use Config::General ();
use Text::Wrap qw(wrap);
use POSIX ();

use vars qw($VERSION $SELF);

$OUTPUT_AUTOFLUSH = 1;
($SELF = $PROGRAM_NAME) =~ s|^.*/||;
$VERSION = sprintf('%d.%02d', q$Revision: 1.5 $ =~ /(\d+)/g);
$Text::Wrap::columns = 77;

# Create Term::Realine object
my $term = new Term::ReadLine $SELF;


# Start header of config output
my $str = sprintf("# Generated by %s, by %s@%s\n", $SELF,
				(getpwuid($EFFECTIVE_USER_ID))[0], (POSIX::uname())[1]
			);
$str .= sprintf("# Created at %s\n\n",scalar localtime);

$str .= "# Please read through your configuration file before using it in production!\n";
$str .= "Disabled True\n\n";


# Print some information
print <<EOT;
psmon-config will ask you a series of questions to help you quickly generate a
valid configuration file. It is reccomended that you then review (and edit) the
resulting configuration file in a text editor of your choice before allowing it
to be used in a production environment.
EOT


# Get global directives
my $directives = get_directives();
for my $directive (sort keys(%{$directives})) {
	(my $help = $directives->{$directive}->[1]) =~ s/\s\s+/ /g;
	printf("\n%-13s%s\n",'Directive:',$directive);
	print wrap('Description: ','             ',"$help\n");
	printf("%-13s%s\n",'Defaults to:',$directives->{$directive}->[0]) if $directives->{$directive}->[0];

	my $prompt = "Specify $directive? [".($directives->{$directive}->[0] ? 'y/N' : 'Y/n')."]: ";
	$_ = $term->readline($prompt);
	redo unless /^\s*(y|n)?\s*$/i;

	#$str .= sprintf("# %-13s%s\n",'Directive:',$directive);
	#$str .= wrap('# Description: ','#              ',"$help\n");
	$str .= wrap('# ','# ',"$help\n");
	$str .= sprintf("# %-13s%s\n",'Defaults to:',$directives->{$directive}->[0]) if $directives->{$directive}->[0];

	if ($_ =~ /y/ || (!length($directives->{$directive}->[0]) && $_ !~ /n/)) {
		$str .= sprintf("%s %s\n\n",$directive,get_value('Value?: '));
	} else {
		$str .= "$directive $directives->{$directive}->[0]\n\n";
	}
}


# Print some information about process scope directives
print <<EOT;

psmon-config.pl will now scan your process table for background daemon
processes. You can then select which of these processes you wish to monitor,
in order to ensure they are always running and/or do not exceed specified
resource limits.

EOT


# Scan the process table
print "Scanning your process table ... ";
my $daemons = {};
my $p = new Proc::ProcessTable( 'cache_ttys' => 1 );
for my $process (@{$p->table}) {
	unless ($process->{ttynum} || $process->{ttydev}) {
		my ($executable) = $process->{cmndline} =~ /^\s*(\S+)/;
		if (-f $executable) {
			my $instances = 1;
			if (exists $daemons->{$process->{cmndline}}) {
				$instances += $daemons->{$process->{cmndline}}->{instances};
			}
			$daemons->{$process->{cmndline}} = $process;
			$daemons->{$process->{cmndline}}->{instances} = $instances;
		}
	}
}
print "done\n";


# Get process scope directive information
$directives = get_process_scope_directives();
for my $process (values %{$daemons}) {
	print "\n";
	print "Process name: $process->{fname}\n";
	print "Command line: $process->{cmndline}\n";
	print "Working UID : $process->{uid}\n";
	print "Instances   : $process->{instances}\n";

	$_ = $term->readline("Would you like to monitor '$process->{fname}'? [y/N]: ");
	redo unless /^\s*(y|n)?\s*$/i;
	next unless /y/i;

	$str .= "# Process $process->{fname} added by psmon-config\n";
	$str .= "<Process $process->{fname}>\n";

	for my $directive (sort keys(%{$directives})) {
		(my $help = $directives->{$directive}->[1]) =~ s/\s\s+/ /g;
		printf("\n%-13s%s\n",'Directive:',$directive);
		print wrap('Description: ','             ',"$help\n");
		printf("%-13s%s\n",'Defaults to:',$directives->{$directive}->[0]) if $directives->{$directive}->[0];

		my $prompt = "Specify $directive? [".($directives->{$directive}->[0] ? 'y/N' : 'Y/n')."]: ";
		$_ = $term->readline($prompt);
		redo unless /^\s*(y|n)?\s*$/i;

		if ($_ =~ /y/ || (!length($directives->{$directive}->[0]) && $_ !~ /n/)) {
			$str .= sprintf("\t%s %s\n",$directive,get_value('Value?: '));
		}
	}

	$str .= "</Process>\n\n";
}


# Add this again for those who just do not pay attention
$str .= "# You need to remove BOTH of these 'Disabled' directives before using this\n";
$str .= "# configuration file. Please make sure you have read and understood everything\n";
$str .= "# in this file before using it in a live production environment!\n";
$str .= "Disabled True\n";


# Print the configuration
open(FH,">psmon-config.conf") || die "Unable to open file handle FH for file 'psmon-config.conf': $!";
print FH <<__TEXT__;
##############################################################################
#
# Please read through this configuration file in detail. It will NOT function
# right out of the box without any modifications. This is for good reason,
# since I don't want to receive snotty emails from you or your system
# administrator, being accused of killing your server or workstation.
#
# There is further documentation supplied with the psmon software. I suggest
# that you read it thoroughly.
# 
#                       - The author, Nicola Worthington
#
##############################################################################

__TEXT__
print FH "$str\n";
print FH <<__TEXT__;


# The <Process *> scope is commented out by default. It should be used with
# *EXTREME* care. If you do decide to use it, may I suggest that you run psmon
# in 'DryRun' mode by adding the 'DryRun' directive in this configuration
# file. READ THE DOCUMENTATION THOROUGHLY BEFORE ENABLING THIS FEATURE!!!

#<Process *>
#	PctCpu		80
#	PctMem		50
#</Process>



# I have included a set of commonly required processes. They are all vital
# services which must be running on all of my workstations and servers. It's
# a pretty good guess you'll want them to always be running too.

# Secure Shell Daemon
#<Process sshd>
#	LogLevel	LOG_CRITICAL
#	SpawnCmd	/sbin/service sshd restart
#	PidFile		/var/run/sshd.pid
#	# Instances	30
#	# PctCPU	90
#</Process>

# Cron Daemon
#<Process crond>
#	spawncmd	/sbin/service crond restart
#	pidfile		/var/run/crond.pid
#</Process>

# System Logger Daemon
#<Process syslogd>
#	spawncmd	/sbin/service syslog restart
#	pidfile		/var/run/syslogd.pid
#</Process>

# Internet Super Daemon
#<Process xinetd>
#	spawncmd	/sbin/service xinetd restart
#	pidfile		/var/run/xinetd.pid
#</Process>

# Remote WHO Daemon
#<Process rwhod>
#	# rwhod is *EVIL*! There is almost never any real
#	# reason why you would ever want to run such pants!
#	killcmd		/sbin/service rwhod stop
#	ttl		1
#</Process>

# BIND DNS Daemon
#<Process named>
#	spawncmd	/sbin/service named start
#	pidfile		/var/run/named.pid
#</Process>

# Exim SMTP Mail Daemon
#<Process exim>
#	spawncmd	/sbin/service exim restart
#	pidfile		/var/run/exim.pid
#	# instamces	30
#	# pctcpu	90
#</Process>

# Sendmail SMTP Mail Daemon
#<Process sendmail>
#	spawncmd	/sbin/service sendmail start
#	pidfile		/var/run/sendmail.pid
#</Process>

# Samba SMB File Sharing Daemon
#<Process smbd>
#	spawncmd	/sbin/service smbd restart
#	pidfile		/var/run/samba/smbd.pid
#</Process>
#<Process nmbd>
#	spawncmd	/sbin/service smbd restart
#	pidfile		/var/run/samba/nmbd.pid
#</Process>

# Quallcomm QPopper POP3 Daemon
#<Process popper>
#	spawncmd	/sbin/service popper restart
#	pidfile		/var/run/popper.pid
#</Process>

# Apache Group HTTP Daemon
#<Process httpd>
#	spawncmd	/sbin/service httpd restart
#	pidfile		/var/run/httpd.pid
#	# instances	200
#	# pctcpu	80
#</Process>

# MySQL Database
#<Process mysqld>
#	spawncmd	/sbin/service mysqld restart
#	killcmd		/sbin/service mysqld stop
#	pidfile		/var/run/mysqld/mysqld.pid
#	# pctcpu	90
#	# pctmem	60
#</Process>

# NTP Time Daemon
#<Process ntpd>
#	spawncmd	/sbin/service ntpd restart
#</Process>

# SMNP Daemon
#<Process snmpd>
#
#</Process>

# ProFTPD FTP Daemon
#<Process proftpd>
#	spawncmd	/sbin/service proftpd restart
#	pidfile		/var/run/proftpd.pid
#</Processes>



# These are processes which run frequently on my machines, but I have had
# experience of either running for too long (for whatever readon), or spawning
# too many copies.
#
# The following is a quick table for your ease of reference:
#      Seconds     Minutes     Hours     Days     Weeks
#           60           1
#         3600          60         1
#        43200         720        12        0.5
#        86400        1440        24        1
#       604800       10080       168        7         1

# Kill excessive of slothenly rsync processes
#<Process rsync>
#	ttl		43200
#	instances	5
#</Process>

# Kill excessive of slothenly updatedb processes
#<Process updatedb>
#	ttl		43200
#	instances	2
#</Process>

# Kill excessive of slothenly find processes
#<Process find>
#	ttl		86400
#	instances	30
#</Process>



### END OF FILE


__TEXT__

close(FH) || warn "Unable to close file handle FH for file 'psmon-config.conf': $!";
print "\n\n";
print "Configuration file written to ./psmon-config.conf\n\n";


# Subroutines
sub get_value {
	my $prompt = shift || 'Value?: ';

	my $retval = '';
	until ($retval) {
		$_ = $term->readline($prompt);
		chomp;
		redo unless /\S+/;
		confirm: {
			print "You entered: $_\n";
			my $confirm = $term->readline('Is this correct? [Y/n]: ');
			redo confirm unless $confirm =~ /^\s*(y|n)?\s*$/i;
			$retval = $_ if $confirm !~ /n/i;
		}
	}

	return $retval;
}

sub read_config {
	my $config_file = shift;

	unless (-f $config_file && -r $config_file) {
		return undef;
	}

	my $conf = new Config::General(
			-ConfigFile				=> $config_file,
			-LowerCaseNames			=> 1,
			-UseApacheInclude		=> 1,
			-IncludeRelative		=> 1,
			-MergeDuplicateBlocks	=> 1,
			-AllowMultiOptions		=> 1,
			-MergeDuplicateOptions	=> 1,
			-AutoTrue				=> 1,
		);

	return $conf->getall;
}

sub get_process_scope_directives {
	my $directives = {
		SpawnCmd		=> [ ('',
							'Defines the full command line to be executed in order to respawn a dead process.') ],

		KillCmd			=> [ ('*Undefined*',
							'Defines the full command line to be executed in order
							to gracefully shutdown or kill a rogue process. If the command
							returns a boolean true exit status then it is assumed that the
							command failed to execute sucessfully. If no KillCmd is specified
							or the command fails, the process will be killed by sending a
							SIGKILL signal with the standard kill() function.') ],

		AdminEmail		=> [ ('*Undefined*',
							'Defines the email address where notification emails should be sent to for
							this process scope. An AdminEmail entry in the process scope will take priority
							over the global AdminEmail declaration. All AdminEmail values in the
							configuration file will be ignored and overridden if AdminEmail is specified as
							a command line option.') ],

		PIDFile			=> [ ('',
							'Defines the full path and filename of a file created by
							a process which contain it\'s main parent process ID.') ],

		TTL				=> [ ('*Undefined*',
							'Defines a maximum time to live (in seconds) of a process.
							The process will be killed once it has been running longer than
							this value, and it\'s process ID isn\'t contained in the defined pidfile.') ],

		PctCpu			=> [ ('*Undefined*',
							'Defines a maximum allowable percentage of CPU time a
							process may use. The process will be killed once it\'s CPU
							usage exceeds this threshold and it\'s process ID isn\'t
							contained in the defined pidfile.') ],

		PctMem			=> [ ('*Undefined*',
							'Defines a maximum allowable percentage of total system
							memory a process may use. The process will be killed once it\'s
							memory usage exceeds this threshold and it\'s process ID isn\'t
							contained in the defined pidfile.') ],

		Instances		=> [ ('*Undefined*',
							'Defines a maximum number of instances of a process which
							may run. The process will be killed once there are more than
							this number of occurances running, and it\'s process ID isn\'t
							contained in the defined pid file.') ],

		NoEmailOnKill	=> [ ('False',
							'Accepts a boolean value of True or False. Surpresses
							process killing notification emails for this process scope.') ],

		NoEmailOnSpawn	=> [ ('False',
							'Accepts a boolean value of True or False. Surpresses
							process spawning notification emails for this process scope.') ],

		NoEmail			=> [ ('False',
							'Accepts a boolean value of True or False. Surpresses
							all notification emails for this process scope.') ],
		};
	return $directives;
}

sub get_directives {
	my $directives = {
		Facility		=> [ ('LOG_DAEMON',
							'Defines which syslog facility to log to. Refer
							to your syslogd and/or operating system documentation for a
							list of valid facilities.') ],

		LogLevel		=> [ ('LOG_NOTICE',
							'Defines the loglevel priority that notifications
							to syslog will be marked as. Refer to your operating system\'s
							kernel.h documentation for a list of valid priorities.') ],

		AdminEmail		=> [ ('root@localhost',
							'Defines the email address where notification emails should be sent to.
							This may be also be used in a Process scope which will take priority over 
							a global declaration. All AdminEmail entries in the configuration file
							will be overridden if it is specified on the command line as an option.') ],

		NotifyEmailFrom	=> [ (sprintf('%s@%s',(getpwuid($EFFECTIVE_USER_ID))[0],(POSIX::uname())[1]),
							'Defines the email address that notification email should be
							addresses from.') ],

		SMTPHost		=> [ ('localhost',
							'Defines the IP address or hostname of the SMTP
							server to used to send email notifications.') ],

		DefaultEmailMethod => [ ('sendmail',
							'Defines which method should be used by default to try and send notification emails.
							Legal values are "SMTP" or "sendmail".') ],

		SMTPTimeout		=> [ ('20',
							'Defines the timeout in seconds to be used during SMTP
							connections.') ],

		SendmailCMD		=> [ ('',
							'Defines the sendmail command to use to send notification emails if
							there is a failure with the SMTP connection to the host defined by SMTPHost.
							PSMon will attempt to locate the sendmail command for you by looking in
							common locations.') ],

		Frequency		=> [ ('60',
							'Defines the frequency (in seconds) of process table queries.') ],

		LastSafePID		=> [ ('100',
							'When defined, psmon will never attempt to kill a
							process ID which is numerically less than or equal to the value
							defined by lastsafepid. It should be noted that psmon will never
							attempt to kill itself, or a process ID less than or equal to 1.') ],

		NeverKillPID	=> [ ('1',
							'Accepts a space delimited list of PIDs which will never
							be killed.') ],

		NeverKillProcessName	=> [ ('kswapd kupdated mdrecoveryd pageout sched init',
							'Accepts a space
							delimited list of process names which will never be killed. ') ],

		ProtectSafePIDsQuietly	=> [ ('Off',
							'Accepts a boolean value of On or Off.
							Surpresses all notifications of preserved process IDs when
							used in conjunction with the lastsafepid directive.') ],
		};
	return $directives;
}

