package common;

# common.pm - these are common system summary routines
#
# ==============================================================
# 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.
# 
# Original source:
#	Copyright (c) 1999, 2000 SAIC (Bruce W. Mohler)
# 
# Contributed source:
#	Copyright (c) 2000 Martin Andrews
#	Copyright (c) 1999, 2000 Frank Crawford
#	Copyright (c) 1999, 2000 Paul Farrall
#	Copyright (c) 1999, 2000 Jim Holthaus
#	Copyright (c) 1999, 2000 Alek O. Komarnitsky
#	Copyright (c) 1999, 2000 Keong Lim
#	Copyright (c) 1999, 2000 Wanderley O. Mendes
# 
# ==============================================================
# 
# $Header: /export/home/mohlerb/syssumm/remote/RCS/common.pm,v 1.49 2000/05/22 00:20:51 mohlerb Exp mohlerb $
# 
# $Revision: 1.49 $
#
# $Log: common.pm,v $
# Revision 1.49  2000/05/22 00:20:51  mohlerb
# changes from Paul Farrall including:
# Added check on argument to sub IPtoFQDN to make sure it has
# valid format for "dotted decimal" IP addresses.  Added return
# string "unknown" if arg does not have valid format.
#
# Revision 1.48  2000/05/08 21:52:58  mohlerb
# changes from Bruce Mohler including:
# I had to modify this routine for one more Compaq
# Tru64 anomaly where they use fixed columns in the
# output of netstat.  If the FQDN of the default
# router is too long, it just gets truncated which
# tended to break FQDNtoIP
#
# Revision 1.47  2000/05/04 23:38:03  mohlerb
# changes from Bruce Mohler including:
# ended up remove "word boundary" markers from the SysV Lp
# pattern too as it would not match /usr/lib/lpsched under
# Solaris 2.7
#
# Revision 1.46  2000/05/04 18:14:08  mohlerb
# changes from Bruce Mohler including:
# removed the word boundary markers from the $BSDLpPattern
# variable in order to get the pattern to recognize the lpd
# process under RH 6.0; see comments just before the beginning
# of the ReturnRunningBSDLpr function for more information
#
# Revision 1.45  2000/04/27 17:42:29  mohlerb
# changes from Ken Monville including:
# During some invocations of this script (mainly on IRIX),
# syssumm.pl hangs in our environment at this point.   I'm
# not exactly sure why its hanging or causing problems; it
# doesn't appear to like that you've re-my'd $TheseArgs.
# By removing my, the script exec's as expected.
#
# Revision 1.44  2000/04/17 04:45:57  mohlerb
# changes from Bruce Mohler including:
# in function LoadFile, when validating the $Mode and when the $Mode
# was "Exec", if there were command line arguments, they were left
# as part of the path and the validation of the path to an existing
# executable always failed; so split out the path from any arguments;
# this defect was found by Michael Seeger <michael.seeger@mchh.siemens.de>
#
# Revision 1.43  2000/04/10 17:27:14  mohlerb
# changes from Bruce Mohler including:
# replaced calls to ReturnArray with either ReturnArrayFile or
# ReturnArrayExec; also removed references to ReturnArrayGlob
#
# Revision 1.42  2000/04/06 14:09:07  mohlerb
# changes from Frank Crawford including:
# the tests in LoadFile needed to do "elsif" between them, rather
# than as separate tests
#
# Revision 1.41  2000/04/03 16:53:49  mohlerb
# changes from Frank Crawford including:
# add ReturnArrayExec, ReturnArrayFile and ReturnArrayGlob
# as discussed on the mailing list recently; improved test
# for Exec in LoadFile; improved check on whether dmesg
# needs to go through StripSyslogTimestamp in GetInputFromCommand
#
# Revision 1.40  2000/03/02 22:03:43  mohlerb
# changes from Frank Crawford including:
# fix to ReturnSwap for SGI which has some short lines in the
# swap function (actually formatting ala Solaris df)
#
# Revision 1.39  2000/02/14 17:24:04  mohlerb
# changes from Bruce Mohler including:
# fixed 2 syntax errors discovered by Moshe Rivel
#
# Revision 1.38  2000/02/09 23:44:18  mohlerb
# changes from Bruce Mohler including:
# cleaned up ReturnServices by moving function to common.pm
# and having it be driven by a @Services array in syssumm.pl;
# note also the option at the end of the Initialize<OS> function
# where you can add OS-specific services that you wish to have
# monitored
#
# Revision 1.37  2000/02/03 23:22:02  mohlerb
# changes from Martin Andrews including:
# IRIX, Linux, and SunOS now report swap file sizes, stubs added
# to other OS modules
#
# Revision 1.36  2000/01/13 15:36:16  mohlerb
# changes from Jim Holthaus including:
# changed ReturnFstab function to return multiple value response
#
# Revision 1.35  2000/01/12 18:16:56  mohlerb
# changes from Frank Crawford including:
# a fix for pattern matching for lpd/lpsched (not fully tested!);
# a fix to get LocateTheSyslogPath to work
#
# Revision 1.34  2000/01/12 01:19:35  mohlerb
# changes from Frank Crawford including:
# mostly changes to comments but a major fix to a typo in
# MailOutput
#
# Revision 1.33  2000/01/07 18:37:39  mohlerb
# changes from Bruce Mohler (and Frank Crawford) including:
# nearly final (?) changes for e-mail delivery after more discussion
#
# Revision 1.32  2000/01/06 19:20:40  mohlerb
# changes from Bruce Mohler including
# further final (?) changes for e-mail delivery after more testing
#
# Revision 1.31  2000/01/06 18:20:56  mohlerb
# changes from Bruce Mohler including:
# final (?) changes for e-mail delivery
#
# Revision 1.30  2000/01/06 16:56:04  mohlerb
# changes from Frank Crawford including:
# Taken $EmailAvailable out of common.pm, syssumm.pl and
# IRIX.pm; Made Sendmail.pm a package of its own; Added a
# Response variable in Sendmail.pm; Reworked error messages,
# etc, in common.pm; Fixed up the end of line in the messages;
# Reworked $Username as discussed
#
# Revision 1.29  2000/01/04 01:15:38  mohlerb
# changes from Bruce Mohler including:
# code mods to MailOutput function and related variables in
# syssumm.pl during testing of new e-mail module
#
# Revision 1.28  2000/01/03 20:11:21  mohlerb
# changes from Frank Crawford including:
# changes syssumm.pl and common.pm to support SMTP delivery
# of the report.  This second one requires a support file (included).
#
# Revision 1.27  1999/11/02 23:29:29  mohlerb
# changes from Bruce Mohler including:
# abstracted Lpsched routines into 3 routines moved to common.pm
# including ReturnRunningSysvLp, ReturnRunningBSDLpr, and
# ReturnRunningLPRng, then inserted code in each of the OS
# modules (except AIX.pm) to call the appropriate routines
# and concatenate the results into a single coherent response
#
# Revision 1.26  1999/11/02 22:15:20  mohlerb
# changes from Alek O. Komarnitsky including:
# support for local exclusions file to prevent one or more
# categories/sub-categories from processing without having
# to specify command line options or change Perl code
#
# Revision 1.25  1999/10/27 20:03:38  mohlerb
# changes from Alek O. Komarnitsky including:
# added ReturnFstab() to common.pm and stubs as appropriate
#
# Revision 1.24  1999/10/24 00:22:58  mohlerb
# changes from Bruce Mohler including:
# restructured LoadFile() subroutines GetInputFromFile() and
# GetInputFromCommand() by abstracting syslog timestamp
# stripping code into it's own function called StripSyslogTimestamp()
# which is now called by both functions; GetInputFromCommand()
# calls the function when it is executing the dmesg command so
# that the dmesg output is stripped of the syslog-like timestamps
#
# Revision 1.23  1999/10/01 19:09:06  mohlerb
# changes from Bruce Mohler including:
# added FQDNtoIP() function to convert fully qualified domain
# name to an IP address (to completed the IPtoFQDN() function)
#
# Revision 1.22  1999/08/30 14:57:20  mohlerb
# changes from Wanderley O. Mendes including:
# changes for new OSF1.pm module
#
# Revision 1.21  1999/08/06 02:36:19  mohlerb
# changes from Bruce Mohler including:
# fix to properly detect Samba running on the local system
#
# Revision 1.20  1999/08/05 22:29:22  mohlerb
# changes from Bruce Mohler including:
# fixed WhatVersionOfLinux; changed where we got disk, tape,
# and network information (from dmesg to /var/log/messages*);
# corrected path to ps command
#
# Revision 1.19  1999/06/17 17:30:54  mohlerb
# changes from Frank Crawford including:
# Moved ReturnDomainNameSunOS to common.pm.
#
# Revision 1.18  1999/06/15 14:04:42  mohlerb
# changes from Frank Crawford including:
# fix for ReturnRunningSamba function: added $InetdConfPath
#
# Revision 1.17  1999/06/11 18:04:35  mohlerb
# changes from Bruce Mohler including:
# modified functionality of LoadFile to treat JustThePath as a
# command whether it has arguments or not, just as long as it's
# executable...
#
# Revision 1.16  1999/06/11 17:47:21  mohlerb
# changes from Bruce Mohler including:
# factored ReturnRunningSamba routine into common.pm
#
# Revision 1.15  1999/06/10 21:58:01  mohlerb
# changes from Bruce Mohler including:
# standardized derivation of NodeName across OS modules:
# it is defined near the beginning of the Initialize${OSName}
# and stored in $main::NodeName.  The ReturnNodeNameField
# merely returns $main::NodeName.
#
# Revision 1.14  1999/06/08 17:47:18  mohlerb
# changes from Frank Crawford including:
# Fix a too specific match in SearchFileForTagValue.
# Reorganise MailOutput to use "system" rather than a program "open"
# and to allow the MailCommand to be given in the ${OS}.pm module.
#
# Revision 1.13  1999/06/07 21:23:14  mohlerb
# changes from Bruce Mohler including:
# reverted MailCommand *back* to mailx not mail with
# leading comments on why this needs to be this way
#
# Revision 1.12  1999/06/07 18:56:13  mohlerb
# change from Bruce Mohler including:
# common ReturnRunAsRoot relocated from individual OS modules
# to common.pm; RunAsRoot variable relocated to syssumm.pl
# (referenced as $main::RunAsRoot); this suggestion was really
# Frank Crawford's...
#
# Revision 1.11  1999/06/02 21:38:12  mohlerb
# changes from Frank Crawford including:
# disable read permission check on executable files.
#
# Revision 1.10  1999/05/30 19:26:52  mohlerb
# changes from Frank Crawford including:
# Generalise the "vmunix" part of the Syslog file.
#
# Revision 1.9  1999/05/28 17:58:11  mohlerb
# changes from Paul Farrall including:
# fix to RE in SearchFileForTagValue function
#
# Revision 1.8  1999/05/27 14:09:57  mohlerb
# changes from Keong Lim including:
# added Header RCS tag above Revision tag
#
# Revision 1.7  1999/05/26 16:30:36  mohlerb
# removed commented out category routines
#
# Revision 1.6  1999/05/26 16:27:31  mohlerb
# changes from Keong Lim including:
# changed LoadFile interface to take reference to array instead of
# returning copy of array; changed CleanupSyslogText to LoadSyslogFile;
# changed LoadFile to call LoadSyslogFile instead of doing "normal"
# file load; changed LoadSyslogFile to use slightly more efficient
# regexp substitutions
#
# Revision 1.5  1999/05/25 19:39:57  mohlerb
# changes from Bruce Mohler including:
# Diked out "FauxNameServer" routine in common which was cute
# but silly and non-portable and replaced it with call to
# gethostbyaddr via IPtoFQDN routine in common.pm
#
# Revision 1.4  1999/05/25 17:39:38  mohlerb
# changes from Frank Crawford including:
# Added common routine to get hostname using Perl5 modules.
# Modified return from Tag lines to not include quote character.
#
# Revision 1.3  1999/05/24 23:57:09  mohlerb
# changes from Frank Crawford including:
# Made to use POSIX standard as supported in older versions of Perl5.
#
# Revision 1.2  1999/05/24 23:53:55  mohlerb
# changes from Frank Crawford including:
# added a function to return the current time as a ctime for return
# fixed up MailOutput (BTW it should use system() but I haven't yet)
#
# Revision 1.1  1999/05/14 22:58:05  mohlerb
# Initial revision
#

use POSIX;
use Socket;

# this module should be in the same directory as the syssumm.pl script
use Sendmail;

require Exporter;
@ISA = qw(Exporter);
###########################################################################
# the following sub-routines (&) are variables ($) will be 
# visible in a program using this module...
@EXPORT = qw(
	&LocateTheSyslogPath

	&RemoveLocalExclusions

	&ReturnCtimeField
	&ReturnNodeNameField

	&IPtoFQDN
	&FQDNtoIP
	&ReturnDomainName

	&ReturnServices
	&ReturnRunningSysvLp
	&ReturnRunningBSDLpr
	&ReturnRunningLPRng
	&ReturnRunningSamba
	&ReturnFstab
	&ReturnSwap

	&ReturnRunAsRoot

	&ReturnNotApplicableField
	&ReturnUnknownField
	&ReturnProtectedField

	&ReturnArray
	&ReturnArrayExec
	&ReturnArrayFile
	&SearchFileForPattern
	&SearchFileForTagValue

	&ForceMultipleResponses

	&ConvertToNumericMonth

	&MailOutput

	$NotFound
	$ForceMultipleResponseTag
);
###########################################################################


###########################################################################
# Public Global Variables (must be declared above)
###########################################################################

$NotFound = -1;					# used by SearchFileForTagValue
$ForceMultipleResponseTag = "FORCEMULTIPLE";


###########################################################################
# Private Global Variables
###########################################################################

# where commands are located
# Solaris 2.7 is "/usr/lib/lpsched"
$SysVLpPattern = "lpsched";
# RH 6.0 is "lpd"
# RH 6.1 is "lpd Waiting"
$BSDLpPattern = "lpd";
$PrintcapPath = "/etc/printcap";
$LPRngPath = "/etc/lpd.conf";

# where files are located
$EtcFstabPath = "/etc/fstab";			# everyone else...
$EtcVfstabPath = "/etc/vfstab";			# Solaris...
$LocalExclusionsPath = "./local.exclusions";	# in current directory
# 
# It appears DEC Unix makes its log file hard to find in
# the syslog.conf file...
# 
$PathToSyslogOSF1 = "/var/adm/messages";

# Search Patterns
# this pattern doesn't seem to work...
# $SambaCommandPattern = "[/\s+][sn]mbd";
$SambaCommandPattern = "[sn]mbd";

@MonthNames = ( 
	"Jan", "Feb", "Mar", "Apr", "May", "Jun", 
	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
);


###########################################################################
# Global Subroutines
###########################################################################

sub LocateTheSyslogPath ($)				# PUBLIC
{	my $SyslogConfPath = shift;
	my $PathToSyslog = "";

	print "LocateTheSyslogPath($SyslogConfPath)...\n" if $main::Debug;

	open SYSLOGCONF, "$SyslogConfPath" or
		die "Can't open $SyslogConfPath for input: $!";
	my @SyslogConfText = <SYSLOGCONF>;
	close(SYSLOGCONF);
	chomp(@SyslogConfText);

	# locate the syslog path
	foreach $Line (@SyslogConfText) {
		next if $Line =~ /^[\s]*#.*/;
		next if $Line =~ /^[\s]*;.*/;
		next if $Line =~ m:/dev/console:;
		next if $Line !~ m:/:;
		if ( $Line =~ /kern\./ ) {
			$Line =~ /.+[\s](\/[\w]+\/[\w]+.*$)/;
			$PathToSyslog = $1;
			last;
		}
	}
	if ( ! $PathToSyslog && main::FigureOutWhatOSWereRunning eq OSF1 ) {
		if ( -e "$PathToSyslogOSF1" ) {
			$PathToSyslog = "$PathToSyslogOSF1";
		}
		else {
			die "Can't find path to syslog file: $!";
		}
	}

	return $PathToSyslog;
}


###########################################################################
# General Subroutines
###########################################################################

# 
# The purpose of this file is to remove one or more
# categories/sub-categories at the discretion of a local
# site (rather than having to hack the source code).
# 
# The behavior of this function is controlled by a file
# in the current directory where you're running syssumm.
# The name of the file is defined in $LocalExclusionsPath.
# 
# Lines where the first non-whitespace character is '#' 
# are considered comments.  Lines with actual "data" are
# formatted like:
# 
#	category:subcategory
# 
# and an actual example would look like:
# 
#	Software:Printers
# 
# With this line specified, syssumm.pl would *NOT* process
# the Printers sub-category of the Software category.
# 

sub RemoveLocalExclusions ()				# PUBLIC
{
	print "RemoveLocalExclusions()...\n" if $main::Debug;

	# if it doesn't exist, then we're done!
	return if ( ! -r $LocalExclusionsPath );

	open EXCLUSIONSFILE, "<$LocalExclusionsPath" or
		die "Can't open $LocalExclusionsPath for input: $!";
	while (defined ($Line = <EXCLUSIONSFILE>) ) {
		chomp $Line;
		# skip comments
		next if $Line =~ /^\s*#/;

		my ( $Category, $Subcategory ) = split(/:/, $Line);
		if ( exists ( $main::Routines{$Category}{$Subcategory} ) ) {
			delete $main::Routines{$Category}{$Subcategory};
			print "\tProcessed local exclusion: $Category, $Subcategory...\n" if $main::Debug;
		}
		else {
			print STDERR "\tCould not process local exclusion: $Category, $Subcategory...\n";
			exit(1);
		}
	}
	close EXCLUSIONSFILE;

	return;
}


###########################################################################
# Hardware Subroutines
###########################################################################

sub ReturnCtimeField					# PUBLIC
{	my $Subcategory = shift;			# Ignored
	my @Response = ( ctime(time) );

	print "ReturnCtimeField(\"$Subcategory\")...\n" if $main::Debug;

	chomp(@Response);

	return @Response;
}


sub ReturnNodeNameField					# PUBLIC
{	my $Subcategory = shift;			# Ignored

	print "ReturnNodeNameField(\"$Subcategory\")...\n" if $main::Debug;

	return($main::NodeName);
}


###########################################################################
# Network Subroutines
###########################################################################

#
# Takes IP address like "nnn.nnn.nnn.nnn" as arg
# If the supplied IP address appears to be a valid format
# Returns string containing FQDN and $ThisIPAddress
# or just $ThisIPAddress if unable to resolve FQDN
#
# If the IP address in arg is not a valid format
# Returns "unknown"
#
# returns string "unknown" if it encounters any problems
# 

sub IPtoFQDN						# PUBLIC
{	my $ThisIPAddress = shift;
	my $ThisFQDN = "";

	print "IPtoFQDN($ThisIPAddress)...\n" if $main::Debug;

	# Test for valid IP address and then
	# pack it into binary structure apropo for gethostbyaddr() function
	if ( $ThisIPAddress =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/ ) {
		my ($a, $b, $c, $d) = ($1, $2, $3, $4);
		my $IPAddr = pack "C4", $a, $b, $c, $d;

		$ThisFQDN = gethostbyaddr($IPAddr, AF_INET);

		# XXX there may be a problem with this next section in OSF1.pm
		if ( $ThisFQDN ) {
			$ThisFQDN = $ThisFQDN . " (" .  $ThisIPAddress . ")";
		}
		else {
			$ThisFQDN = $ThisIPAddress;
		}
	} else {
		# Supplied IP didn't match regex, so we can't process it
		$ThisFQDN = "unknown";
	}

	return $ThisFQDN;
}


sub FQDNtoIP                                            # PUBLIC
{	my $ThisFQDN = shift;
	my $ThisIPAddress = "";

	# 
	# I had to modify this routine for one more Compaq
	# Tru64 anomaly where they use fixed columns in the
	# output of netstat.  If the FQDN of the default 
	# router is too long, it just gets truncated which
	# tended to break this function.
	# 

	my @Addresses = (gethostbyname $ThisFQDN)[4];
	if ( @Addresses ) {
		my ($a, $b, $c, $d) = unpack('C4', $Addresses[0]);
		$ThisIPAddress = "$a." . "$b." . "$c." . "$d" ;
		return $ThisIPAddress;
	}
	else {
		return "???.???.???.???";
	}

}


sub ReturnDomainName ($$)				# PUBLIC
{	my $Subcategory = shift;
	my $ResolvConfPath = shift;

	$FullyQualifiedDomainName = main::hostname();

	if ( ($FullyQualifiedDomainName =~ tr/././) == 0) {
		my ( $Name, $Aliases, $AddrType, $Length, @Addresses ) =
			gethostbyname($FullyQualifiedDomainName);

		if ( defined($Name) and ($Name =~ tr/././) != 0 ) {
			$FullyQualifiedDomainName = $Name;
		}
		else {
			my $Domain = SearchFileForPattern($ResolvConfPath, "domain", 1);
			if ( $Domain ne $NotFound ) {
				$FullyQualifiedDomainName .= '.' . $Domain;
			}
		}
	}

	return ( $FullyQualifiedDomainName );
}


###########################################################################
# Software
###########################################################################

sub ReturnServices ($)					# PUBLIC
{	my $Subcategory = shift;
	my @Response = ( );

	foreach $Service ( @main::Services ) {
		my $Response = "";

		print "Processing Service $Service...\n" if $main::Debug;

		my $FunctionName = "$main::OSName" . "::" . 
			"ReturnRunning" . $Service . $main::OSName;
		print "\tFunctionName is $FunctionName...\n" if $main::Debug;

		$Response = &{$FunctionName}($Subcategory);

		push @Response, "$Service: $Response";
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnRunningSysvLp ($$$)				# PUBLIC
{	my $Subcategory = shift;
	my $PsCommand = shift;
	my $PsArgs = shift;

	my @RunningProcs = ReturnArrayExec("$PsCommand $PsArgs");
	my $Lpsched = ( grep /$SysVLpPattern/, @RunningProcs ) ? "Yes (SysV)" : "No";
	print "\tReturnRunningSysvLp: $Lpsched\n" if $main::Debug;

	return ( $Lpsched );
}


# 
# This function caused me a great deal of grief under Red Hat
# Linux.  Under RH 6.0, the ps output looks like:
# 
#	root     20454     1  0 Apr06 ?        00:00:00 lpd
# 
# Under RH 6.1, the ps output looks like:
# 
# 	daemon     465     1  0 Apr24 ?        00:00:00 lpd Waiting
# 
# [Granted, I'm running LPRng on the 6.1 system.]  But I couldn't
# get the pattern used in the grep below to work under both
# instances.  I ended up having to remove the "\b" word
# boundary markers from the pattern.
# 
# Suggestions?
# 

sub ReturnRunningBSDLpr ($$$)				# PUBLIC
{	my $Subcategory = shift;
	my $PsCommand = shift;
	my $PsArgs = shift;

	my @RunningProcs = ReturnArrayExec("$PsCommand $PsArgs");
	my $Lpsched = ( ( grep /$BSDLpPattern/, @RunningProcs ) and
		        ( -f $PrintcapPath ) ) ? "Yes (BSD)" : "No";
	print "\tReturnRunningBSDLpr: $Lpsched\n" if $main::Debug;

	return ( $Lpsched );
}


sub ReturnRunningLPRng ($)				# PUBLIC
{	my $Subcategory = shift;

	my $Lpsched = ( -f $LPRngPath ) ? "Yes (LPRng)" : "No";
	print "\tReturnRunningLPRng: $Lpsched\n" if $main::Debug;

	return ( $Lpsched );
}


sub ReturnRunningSamba ($$$$)				# PUBLIC
{	my $Subcategory = shift;
	my $PsCommand = shift;
	my $PsArgs = shift;
	my $InetdConfPath = shift;

	my @PsText = ReturnArrayExec("$PsCommand $PsArgs");
	foreach $Line (@PsText) {
		if ( $Line =~ /$SambaCommandPattern/ ) {
			return ( 'Yes' );
		}
	}
	# XXX there may be a problem with this next section in OSF1.pm
	my @InetdConfText = ReturnArrayFile("$InetdConfPath");
	foreach $Line (@InetdConfText) {
		if ( $Line =~ /$SambaCommandPattern/ ) {
			return ( 'Yes' );
		}
	}

	return ( 'No' );
}


sub ReturnFstab ($)					# PUBLIC
{	my $Subcategory = shift;
	my @FstabText = ( );
	my @Response = ( );

	# first try the /etc/vfstab file
	if ( -r $EtcVfstabPath ) {
		@FstabText = ReturnArrayFile("$EtcVfstabPath");
	}
	# then try the "normal" /etc/fstab file
	elsif ( -r $EtcFstabPath ) {
		@FstabText = ReturnArrayFile("$EtcFstabPath");
	} else {
		return "$EtcFstabPath was not readable";
	}

	foreach $Line (@FstabText) {
		next if ($Line =~ /^\s*$/);
		next if ($Line =~ /^#/);
		next if ($Line =~ /fd.*\/dev\/fd.*fd/);
		next if ($Line =~ /\/proc.*\/proc.*proc/);
		# it's a single value return, but let the embedded breaks
		# format it onto multiple lines
		push @Response, $Line;
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnSwap ($$)
{	my $Subcategory = shift;
	my $SwapCommand = shift;
	my @Lines = ReturnArrayExec($SwapCommand);
	my @Headings = split(' ', shift(@Lines));
	my $Blocks = 0;
	my $Column;

	if ( $SwapCommand eq "" ) {
		return ( ReturnUnknownField($Subcategory) );
	}

	my $i = 0;
	foreach $Heading (@Headings) {
		if ( $Heading eq 'blocks' ) {
			$Column = $i;
			last;
		}
		$i++;
	}
	if ( ! defined($Column) ) {
		return ( ReturnUnknownField($Subcategory) );
	}

	foreach $Line (@Lines) {
		my @Vals = split(' ', $Line);
		$Blocks += $Vals[$Column] unless @Vals < $Column;
	}

	return ( int ( $Blocks / ( 1024 * 2 ) ) ) . "MB";
}


###########################################################################
# Comments
###########################################################################

sub ReturnRunAsRoot					# PUBLIC
{	my $Subcategory = shift;			# Ignored

	my $RunAsRootInfo = "IGNORE";
	if ( $main::RunAsRoot ) {
		$RunAsRootInfo = "Run as root for more information";
	}

	return ($RunAsRootInfo);
}


###########################################################################
# Search Subroutines
###########################################################################

# 
# Note that these routines will preload the file into
# the main:: namespace the first time the file is accessed.
# All future requests search against the text that is 
# already loaded into memory...
# 

sub SearchFileForPattern				# PUBLIC
{	my $ThisPath = $_[0];
	my $ThisPattern = $_[1];
	my $ThisIndex = $_[2];
	my $ThisMode = $_[3];
	my $Response = "";

	print "SearchFileForPattern($ThisPath, $ThisPattern, $ThisIndex)...\n" if $main::Debug;

	my @TextFile;
	LoadFile($ThisPath, \@TextFile, $ThisMode);

	# 
	# This code will return *all* occurances of the pattern
	# in the requested file...
	# 

	foreach $Line (@TextFile) {
		if ( $Line =~ /$ThisPattern/ ) {
			# ignore lines that might be comments
			next if $Line =~ /^[\s]*#.*/;
			next if $Line =~ /^[\s]*;.*/;
			print "\tfound line: $Line\n" if $main::Debug;
			# clean it up before extracting the field
			$Line =~ s/^[\s]*//;		# no leading spaces
			$Line =~ s/[\s]*$//;		# no trailing spaces
			# use split(" ") instead of split(/ /) to emulate awk
			# multiple spaces treated as a single field separator
			# instead of multiple empty fields
			my @Fields = split(" ", $Line);
			if ( $ThisIndex == -1 ) {
				# then return the entire line
				@Fields = ( );
				$ThisIndex = 0;
				$Fields[$ThisIndex] = $Line;

			}
			if ( $Response ) {
				$Response = $Response . " " . $Fields[$ThisIndex];
			}
			else {
				$Response = $Fields[$ThisIndex];
			}
		}
	}

	return ( $Response ? $Response : $NotFound )
}


sub SearchFileForTagValue				# PUBLIC
{	my $ThisPath = $_[0];
	my $ThisTag = $_[1];
	my $ThisMode = $_[2];
	my $Value = $NotFound;

	print "SearchFileForTagValue($ThisPath, $ThisTag)...\n" if $main::Debug;

	my @TextFile;
	LoadFile($ThisPath, \@TextFile, $ThisMode);

	foreach $Line (@TextFile) {
		if ( $Line =~ /\b$ThisTag\b/ ) {
			# ignore lines that might be comments
			next if $Line =~ /^[\s]*#.*/;
			next if $Line =~ /^[\s]*;.*/;
			# XXX this next line may cause problems...
			next if $Line =~ /export/;
			print "\t$Line...\n" if $main::Debug;
			my ( $Tag, $Value ) = split(/=/, $Line);
			# clean off any quotation marks
			$Value =~ s/^"//;
			$Value =~ s/"$//;
			return $Value;
		}
	}

	return $NotFound;
}


# 
# The use of this function is deprecated.  Please use
# ReturnArrayExec or ReturnArrayFile
# 

sub ReturnArray ($$)
{	my $ThisPath = shift;
	my $Mode = shift;
	my @TextFile = ( );

	print "ReturnArray($ThisPath)...\n" if $main::Debug;

	LoadFile($ThisPath, \@TextFile, $Mode);

	return @TextFile;
}


sub ReturnArrayExec ($)
{	my $ThisCommand = shift;
	my @TextFile = ( );

	print "ReturnArrayExec($ThisCommand)...\n" if $main::Debug;

	LoadFile($ThisCommand, \@TextFile, "Exec");

	return @TextFile;
}


sub ReturnArrayFile ($)
{	my $ThisPath = shift;
	my @TextFile = ( );

	print "ReturnArrayFile($ThisPath)...\n" if $main::Debug;

	my @TheseFiles = glob($ThisPath);
	my $NbrOfFiles = scalar(@TheseFiles);
	LoadFile($ThisPath, \@TextFile, ( $NbrOfFiles > 1 ) ? "Glob" : "File" );

	return @TextFile;
}


# 
# This function operates in one of 3 modes depending
# on the $ThisPath argument.
# 
# If $ThisPath is a path to an existing file and that
# file has not already been loaded, then it will load
# the file into @$TextFileArray and the function 
# returns.
# 
# If $ThisPath contains any wildcard characters, then
# the path is glob'd, then the series of file, if any,
# are loaded into @$TextFileArray and the function 
# returns.  Note that the *order* that the files are
# loaded into the array are determined by the glob()
# function.
# 
# If the file is executable (with or WITHOUT any
# command line arguments), then we assume that it is
# a program and run it rather than reading it, storing 
# its output in the array.  If no command line args are 
# provided, then even if it is executable, just read it 
# in.
# 

sub LoadFile ($$$)					# PRIVATE
{	my ($ThisPath, $TextFileArray, $Mode) = @_;
	my $TheseArgs = "";
	my @TheseFiles = ( );

	print "LoadFile($ThisPath)...\n" if $main::Debug;

	$main::CurrentPath = $ThisPath;

	# compatibility mode for modules calling ReturnArray()
	if (!$Mode) {
		# what mode are we working in?
		# if there are arguments to a command...
		if ( ( index $ThisPath, " " ) > 1 ) {
			( $ThisPath, $TheseArgs ) = split(/ /, $ThisPath, 2);
			$Mode = "Exec";
		}
		else {
			# assume that it's a file...
			$Mode = "File";

			@TheseFiles = glob($ThisPath);
			my $NbrOfFiles = scalar(@TheseFiles);
			# if more than one file results...
			if ( $NbrOfFiles > 1 ) {
				$Mode = "Glob"
			}
			# if it's executable...
			elsif ( -x $ThisPath ) {
				if ( ! -r $ThisPath || -B $ThisPath ) {
					$Mode = "Exec";
				} else {
					if (open(TEXTFILE, "<$ThisPath")) {
					my $Interp = scalar(<TEXTFILE>);
					$Mode = "Exec" if substr($Interp, 0, 2) eq '#!';
					}
					close(TEXTFILE);
				}
			}
		}
	}
	print "\tInitial mode is $Mode...\n" if $main::Debug;

	# validate the mode we're working in
	my $AnErrorOccurred = 0;			# currently "off"
	if ( $Mode eq "File" ) {
		# does the file exist?
		if ( ! -f $ThisPath ) {
			print "\tFile $ThisPath doesn't exist...\n" if $main::Debug;
			$AnErrorOccurred = 1;
		}
		# is it readable?
		elsif ( ! -r $ThisPath ) {
			print "\tFile $ThisPath exists but is not readable...\n" if $main::Debug;
			$AnErrorOccurred = 1;
		}
	}
	elsif ( $Mode eq "Glob" ) {
		# do the files exist?
		foreach $File (@TheseFiles) {
			if ( ! -f $File ) {
				print "\tFile $File doesn't exist...\n" if $main::Debug;
				$AnErrorOccurred = 1;
			}
			# are they readable?
			elsif ( ! -r $File ) {
				print "\tFile $File exists but is not readable...\n" if $main::Debug;
				$AnErrorOccurred = 1;
			}
		}
	}
	elsif ( $Mode eq "Exec" ) {
		# split out the command line arguments, if they exist
		$TheseArgs = "";
		if ( ( index $ThisPath, " " ) > 1 ) {
			( $ThisPath, $TheseArgs ) = split(/ /, $ThisPath, 2);
		}
		# does the command exist and is it executable?
		if ( ! -x $ThisPath ) {
			print "\tCommand $ThisPath isn't executable...\n" if $main::Debug;
			$AnErrorOccurred = 1;
		}
	}
	else {
		die "Invalid mode: $Mode: $!";
	}
	if ( $AnErrorOccurred ) {
		# "zero out" the tables
		@{$main::CurrentPath} = ( );
		@$TextFileArray = ( );
		print "\tFile doesn't exist...\n" if $main::Debug;
		# end of the road
		return;
	}

	# a debugging "checkpoint"
	print "\tMode is $Mode...\n" if $main::Debug;
	print "\tThisPath is $ThisPath...\n" if $main::Debug;
	print "\tTheseArgs is $TheseArgs...\n" if $main::Debug;
	print "\tTheseFiles is @TheseFiles...\n" if $main::Debug;

	# if the array doesn't already exist...
	if ( ! defined(@{$main::CurrentPath}) ) {

		print "\tThe array for $ThisPath doesn't exist yet...\n" if $main::Debug;

		@{$main::CurrentPath} = ( );

		# load based on $Mode
		if ( $Mode eq "File" ) {
			@{$main::CurrentPath} = GetInputFromFile($ThisPath);
		}
		elsif ( $Mode eq "Glob" ) {
			@{$main::CurrentPath} = GetInputFromGlob(@TheseFiles);
		}
		elsif ( $Mode eq "Exec" ) {
			@{$main::CurrentPath} = GetInputFromCommand($ThisPath, $TheseArgs);
		}

		my $NbrOfLines = scalar(@{$main::CurrentPath});
		print "\tRead $NbrOfLines lines mode $Mode from $ThisPath...\n" if $main::Debug;
	}
	else {
		print "\tThe array for $ThisPath already exists...\n" if $main::Debug;
	}

	@$TextFileArray = @{$main::CurrentPath};

	return;
}


# 
# This function expects a single path to an existing file as 
# an input argument and returns an array
# 

sub GetInputFromFile ($)				# PRIVATE
{	my ( $ThisPath ) = @_;
	my @ArrayOfText = ( );

	print "GetInputFromFile($ThisPath)...\n" if $main::Debug;

	open TEXTFILE, "<$ThisPath" or
		die "Can't open $ThisPath for input: $!";
	@ArrayOfText = <TEXTFILE>;
	chomp(@ArrayOfText);
	close TEXTFILE;

	# if it's syslog text, strip off the leading timestamp...
	if ( $ThisPath =~ /syslog/ or $ThisPath =~ /messages/ ) {
		return StripSyslogTimestamp(@ArrayOfText);
	}
	else {
		return @ArrayOfText;
	}
}


# 
# This function expects an array of paths to existing files 
# as an input argument and returns an array
# 

sub GetInputFromGlob (@)				# PRIVATE
{	my @TheseFiles = @_;
	my @ReturnThis = ( );

	print "GetInputFromGlob(@TheseFiles)...\n" if $main::Debug;

	foreach $ThisPath (@TheseFiles) {
		my @TempArray = GetInputFromFile($ThisPath);

		my $NbrOfLines = scalar(@TempArray);
		print "\tRead $NbrOfLines lines from $ThisPath...\n" if $main::Debug;

		# Perl Cookbook, p. 108
		push(@ReturnThis, @TempArray);
	}

	return @ReturnThis;
}


# 
# This function expects a path to a command as an array
# containing comand line arguments for that command and 
# returns an array
# 

sub GetInputFromCommand ($$)				# PRIVATE
{	my ( $ThisPath, $TheseArgs ) = @_;
	my @ArrayOfText = ( );

	print "GetInputFromCommand($ThisPath $TheseArgs)...\n" if $main::Debug;

	open TEXTFILE, "$ThisPath $TheseArgs |" or
		die "Can't read input from $ThisPath: $!";
	@ArrayOfText = <TEXTFILE>;
	chomp(@ArrayOfText);
	close TEXTFILE;

	# if it's syslog text, strip off the leading timestamp...
	if ( $ThisPath =~ /dmesg/ && $ArrayOfText[0] =~ /$main::NodeName [^:]+:/ ) {
		# early return
		return StripSyslogTimestamp(@ArrayOfText);
	}

	return @ArrayOfText;
}


sub StripSyslogTimestamp (@)				# PRIVATE
{	my @ReturnThis = ( );

	foreach $Line (@_) {
		# filter out extraneous messages
		next unless ($Line =~ /$main::NodeName [^:]+:/);

		# 
		# drop off timestamp and ...vmunix
		# get rid of leading/trailing spaces
		#
		# don't use back references like: $Line =~ s/vmnuix: (.*)/$1/;
		# as this can be extremely slow
		#
		$Line =~ s/^.*$main::NodeName [^:]+:\s+//;
		$Line =~ s/\s+$//;

		push @ReturnThis, $Line;
	}

	return @ReturnThis;
}


###########################################################################
# Category Subroutines
###########################################################################

sub ReturnNotApplicableField				# PUBLIC
{	my $Subcategory = shift;
	my @Response = ( "Not Applicable" );

	print "ReturnNotApplicableField(\"$Subcategory\")...\n" if $main::Debug;

	return @Response;
}


sub ReturnUnknownField					# PUBLIC
{	my $Subcategory = shift;
	my @Response = ( "UNKNOWN" );

	print "ReturnUnknownField(\"$Subcategory\")...\n" if $main::Debug;

	return @Response;
}


sub ReturnProtectedField				# PUBLIC
{	my $Subcategory = shift;
	my @Response = ( "PROTECTED" );

	print "ReturnProtectedField(\"$Subcategory\")...\n" if $main::Debug;

	return @Response;
}


###########################################################################
# Utility Subroutines
###########################################################################

sub ForceMultipleResponses
{	my $ThisArrayRef = shift;

	print "ForceMultipleResponses()...\n" if $main::Debug;

	my @ThisArray = @{$ThisArrayRef};
	my $NbrOfMembers = scalar(@ThisArray);

	if ( $NbrOfMembers < 1 ) {
		push @ThisArray, $ForceMultipleResponseTag;
		push @ThisArray, $ForceMultipleResponseTag;
	}
	elsif ( $NbrOfMembers < 2 ) {
		push @ThisArray, $ForceMultipleResponseTag;
	}

	return @ThisArray;
}


# 
# Mail output to remote system for HTML processing
# 
# This function is structured to meet the following requirements:
# 
#	If the sysadm specified an SMTP host on the command line
#	of the calling script via the '-s' command line option, it 
#	is because it took an MX record to get to that host and we
#	know that the Sendmail.pm module *DOESN'T* support MX
#	processing (so we had to do the "lookup" for it via the 
#	'-s' command line option).
# 
#	However, if no SMTP host was specified, so that we can 
#	allow for the possibility that the web server may be down, 
#	if e-mail is available on the localhost, we'll deliver the 
#	message there to let it's MTA deliver the message when it 
#	can.
# 
#	If no SMTP host was specified by the sysadm and mail to
#	the local host fails, we'll strip the server name off of
#	the e-mail address (to the right of the '@') and try to
#	deliver the message there.
# 
#	If that fails, we give up in disgrace.
# 

sub MailOutput						# PUBLIC
{	my $MessageDelivered = 0;
	my $Response = 0;
	my @SMTPHosts = ( );

	print "\nMailOutput($main::EmailAddress)...\n" if $main::Debug;

	if ( ! $main::EmailAddress ) {
		print "Output not e-mailed to web server...\n" if $main::Verbose;
		return;
	}

	# where is we at?
	$main::FullyQualifiedDomainName =
		&{ $main::Routines{Network}{DomainName}}(DomainName)
			unless $main::FullyQualifiedDomainName;

	# build list of SMTP hosts to try to deliver the message to
	# if user specified SMTPHost, use it first
	if ( $main::SMTPHost ) {
		push @SMTPHosts, $main::SMTPHost;
	}
	if ( $main::EmailAvailable ) {
		push @SMTPHosts, "localhost";
	}
	# if user didn't specify SMTPHost, use host in EmailAddress
	if ( ! $main::SMTPHost ) {
		# we verified that $main::EmailAddress was valid earlier
		my $SMTPHost = (split('@', $main::EmailAddress))[1];
		push @SMTPHosts, $SMTPHost;
	}

	# now, deliver the message
	foreach $Host (@SMTPHosts) {
		print "\tAttempting to deliver message to $Host\n" if $main::Debug;
		$Response = SendMessage($Host);
		if ( $Response < 0 ) {
			print "\tDelivery to $Host failed: $Sendmail::Result{$Response}\n" if $main::Debug;
			next;
		}
		else {
			$MessageDelivered = 1;
			last;
		}
	}

	if ( $MessageDelivered ) {
		print "\tDelivery succeeded\n" if $main::Debug;
	}
	else {
		warn "You will need to move the file $main::OutputFile to another system\n";
		warn "and e-mail it to the web server system with the subject\n";
		die "\"System Summary from $main::FullyQualifiedDomainName\".\n";
	}

	print "Output e-mailed...\n" if $main::Verbose;

	return;
}


# 
# This function will actually send the message
# 
# Arguments:
# 	SMTPHost to send the message to
# 
# Returns:
#	Response from sendmail() function
# 

sub SendMessage						# PRIVATE
{	my $SMTPHost = shift;

	print "SendMessage($SMTPHost)...\n" if $main::Debug;

	# map local vars to sendmail() args
	my $from = $main::Username;
	if ( ! ( $from =~ /\@/ ) ) {
		$from .= '@' . $main::FullyQualifiedDomainName;
	}
	my $replyto = "";
	my $to = $main::EmailAddress;
	my $smtp = $SMTPHost;
	my $subject = "System Summary from $main::FullyQualifiedDomainName";
	my $message = $main::EmailMessage;
	my $hostname = $main::FullyQualifiedDomainName;

	print "\tFrom: $from\n" if $main::Debug;
	print "\tTo: $to\n" if $main::Debug;
	print "\tSmtp: $smtp\n" if $main::Debug;

	my $Response = main::sendmail(
		$from,			# sender's email address
		$replyto,		# reply-to email address
		$to,			# receiver's to email address
		$smtp,			# name/IP address of smtp server
		$subject,		# subject line
		$message,		# multiline message (-d "\r\n")
		$hostname );		# hostname

	print "\tResponse is $Response\n" if $main::Debug;

	return $Response;
}


###########################################################################
# Date Subroutines
###########################################################################

sub ConvertToNumericMonth				# PUBLIC
{	my $MonthName = shift;

	# print "\nConvertToNumericMonth($MonthName)...\n" if $main::Debug;

	my $Index = 0;
	foreach $Month (@MonthNames) {
		if ( $MonthName eq $Month ) {
			return $Index;
		}
		$Index++;
	}

	return "UNKNOWN";
}


1;
