package Linux;

# Linux.pm - these are operating system specific routines for Linux
#
# Rules for these subroutines:
# 
#	1.  These subroutines can be called in any order except
#	    for Initialize${OSName}.  That will always be called
#	    before any others.
#	2.  These subroutines can be called in any order...
# 
# ==============================================================
# 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) 1999, 2000 Martin Andrews
#	Copyright (c) 2000 Gustavo Chaves
#	Copyright (c) 1999, 2000 Frank Crawford
#	Copyright (c) 1999, 2000 Paul Farrall
#	Copyright (c) 1999, 2000 Alek O. Komarnitsky
#	Copyright (c) 1999, 2000 Keong Lim
#	Copyright (c) 2000 Joe Lizzi
#	Copyright (c) 2000 Jens Persson
#	Copyright (c) 1999, 2000 Chuck Powell
#	Copyright (c) 1999, 2000 Uwe Willenbacher
# 
# ==============================================================
# 
# $Header: /export/home/mohlerb/syssumm/remote/RCS/Linux.pm,v 1.51 2000/06/09 20:01:33 mohlerb Exp mohlerb $
# 
# $Revision: 1.51 $
#
# $Log: Linux.pm,v $
# Revision 1.51  2000/06/09 20:01:33  mohlerb
# changes from Jens Persson including:
# fixed ReturnHostIdLinux function
#
# Revision 1.50  2000/06/06 21:04:03  mohlerb
# changes from Jens Persson including:
# support for Debian Linux as well as fixing a typo the
# prevented the Linux module from compiling
#
# Revision 1.49  2000/05/22 00:15:44  mohlerb
# changes from Paul Farrall including:
# Fixed sub ReturnDefaultRouterLinux.  This function was causing
#    "unitialized value" errors because it was sometimes passing empty
#    args to IPtoFQDN().  Added checks on return values from
#    SearchFileForTag to avoid this situation and modified function to
#    return "unknown" if it can't come up with a meaninful response.
#
# Revision 1.48  2000/05/21 23:22:45  mohlerb
# changes from Gustavo Chaves including:
# make sure we use the C locale; modified ReturnProcessorsLinux
# for Alpha processor; made the FormatScsiDeviceDescription
# routine more general; modified ReturnDefaultRouterLinux to
# use netstat to get default router in case it's not statically
# defined; made ReturnNetworkInterfacesLinux more strict in
# getting the interfaces; generalized the way ReturnPatchesLinux
# finds the installation date because it didn't work for
# Conectiva
#
# Revision 1.47  2000/05/21 22:38:07  mohlerb
# changes from Gustavo Chaves including:
# patches to support Conectiva Linux (popular in Brazil)
#
# Revision 1.46  2000/05/04 19:00:42  mohlerb
# changes from Bruce Mohler including:
# code for ReturnDisksLinux was actually in the ReturnMemoryLinux
# function and was called twice (once for memory and once for
# swap); renamed ReturnMemoryLinux to ReturnDisksLinux; restored
# original ReturnMemoryLinux
#
# Revision 1.45  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.44  2000/04/05 14:21:56  mohlerb
# changes from Frank Crawford including:
# some debug text was being printed in non-debug mode, added
# test for $main::Debug
#
# Revision 1.43  2000/04/03 14:33:14  mohlerb
# changes from Frank Crawford including:
# improved test for version of ps; make use of Linux kernel
# version, either added to the Vendor release or used as
# the OSVersion; fixed a bug with the rpm -qa when a ':'
# is in the returned string
#
# Revision 1.42  2000/03/23 17:27:36  mohlerb
# changes from Chuck Powell including:
# added version and release back into information provided
# by ReturnPatchesLinux; added variable to allow listing of
# *all* RPMs, not just the ones added since the last install;
# this is controlled by $ListNewerRpmFiles
#
# Revision 1.41  2000/03/21 14:43:43  mohlerb
# changes from Joe Lizzi including:
# fix to recognize release file properly under Red Hat 4.0
#
# Revision 1.40  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.39  2000/02/09 00:10:50  mohlerb
# changes from Bruce Mohler including:
# completed first draft of re-write of ReturnDisksLinux so as
# to not use dmesg or the syslog as the source of information
# but to parse it from /proc/ide and /proc/scsi; tapes are
# subsumed into the ReturnDisks function but the syssumm.format
# file is not changed
#
# Revision 1.38  2000/02/07 02:56:49  mohlerb
# changes from Chuck Powell including:
# fix to code in InitializeLinux to allow Linux hostname with
# embedded hyphens
#
# Revision 1.37  2000/02/04 00:14:32  mohlerb
# changes from Bruce Mohler including:
# implemented ReturnServices to return all of the Running*
# functions as a multiple value field
#
# Revision 1.36  2000/02/03 23:23:20  mohlerb
# changes from Martin Andrews including:
# IRIX, Linux, and SunOS now report swap file sizes, stubs added
# to other OS modules
#
# Revision 1.35  2000/01/24 19:02:53  mohlerb
# changes from Frank Crawford including:
# a rewrite of the ReturnPatchesLinux function now entirely based on rpm;
# also fixed same function to use timestamp on <distribution>-release
# file for date of install rather than /usr
#
# Revision 1.34  2000/01/11 19:22:48  mohlerb
# changes from Bruce Mohler including:
# the WhatVersionOfLinux function was not setting the value of
# $OSVersion so that it was reported as blank in the profile
#
# Revision 1.33  2000/01/07 01:20:51  mohlerb
# changes from Bruce Mohler including:
# added code to ReturnRunningSendmail* to determine what version
# of sendmail is running from the sendmail.cf file
#
# Revision 1.32  2000/01/03 20:18:18  mohlerb
# changes from Bruce Mohler including:
# cleaned out references to $MailCommand to complete changes
# that Frank Crawford made to syssumm.pl and common.pm in
# order to use the Sendmail.pm module
#
# Revision 1.31  1999/12/30 20:33:58  mohlerb
# changes from Paul Farrall including:
# Paul supplied code to make the ReturnRunningNameserverLinux
# function work!  Not only that, it attempts to extract the
# the type of nameserver and the domains served; note that the
# code that processes the configuration file only works for
# bind 8 since earlier versions store their configuration in
# a different file
#
# Revision 1.30  1999/12/30 19:57:35  mohlerb
# changes from Bruce Mohler including:
# fixes to WhatVersionOfLinux and ReturnVendorLinux to properly
# return vendor and *VENDOR's* version number rather than the
# version number of the kernel (rpm file); fix in InitializeLinux
# function to initialize ps command line args based on Linux
# version; these changes are in preparation for further changes
# from Paul Farrall
#
# Revision 1.29  1999/11/03 19:23:32  mohlerb
# changes from Bruce Mohler including:
# fixed typo in RunningLpsched function: s/BSDLp/BSDLpr/
#
# Revision 1.28  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.27  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.26  1999/10/27 20:03:38  mohlerb
# changes from Alek O. Komarnitsky including:
# added ReturnFstab() to common.pm and stubs as appropriate
#
# Revision 1.25  1999/10/26 22:11:53  mohlerb
# Changes from Alek O. Komarnitsky including:
# added a stub routine for listing the file systems exported from
# the local machine
#
# Revision 1.24  1999/10/26 14:19:15  mohlerb
# Changes from Alek O. Komarnitsky including:
# added a stub routine for dumping the EEPROM for disaster
# recovery purposes
#
# Revision 1.23  1999/09/29 22:08:17  mohlerb
# changes from Uwe Willenbacher including:
# support for multiple StorageManagers; support for
# Veritas storage manager under SunOS
#
# Revision 1.22  1999/08/20 22:42:21  mohlerb
# changes from Paul Farrall including:
# uncommented $LpdCommand = "/usr/sbin/lpd";.  It was causing
# 'unitialized variable' in ReturnRunningLpschedLinux().
# Added test for rpm command before trying to use it since not all
# Linux distributions use rpms.
# fixed 'cpu MHz' typo in first call to CalculateProcessorSpeed.
# It was causing processor speed calc to fail for multiple processors.
# added "686 => 1.0" to %BogoMips.
# Changed CalculateProcessorSpeed() so it Doesn't calculate clock
# speed if it's already given by /proc/cpuinfo.  Listed value
# is returned cleaned up.
# Changed formattting of Processor Speed value.  Added space between
# value and MHz label.  Truncated to value to integer.
# Added support for searching output of dmesg and ifconfig commands
# Search for Network Interface devices occurs in the following order:
# syslog log -> dmesg output -> ifconfig output.
# Changed some variable names to be more consistent with names in
# SunOS.pm.
# Added device type field (e.g. "Ethernet") to output of AddNetInfo.
# Added support for PPP devices to AddNetInfo.
# Added a bunch of comments.
#
# Revision 1.21  1999/08/13 22:29:40  mohlerb
# changes from Bruce Mohler including:
# fix to NFS routines to properly detect NFS server running
# even if no file systems are exported
# fix to printer routine to provide more information about
# configured printers on the system
#
# Revision 1.20  1999/08/06 02:36:19  mohlerb
# changes from Bruce Mohler including:
# fix to properly detect Samba running on the local system
#
# Revision 1.19  1999/08/05 22:29:22  mohlerb
# changes from Bruce Mohler including:
# heavy modification to LoadFile; removed function LoadSyslogFile
# and moved in-line in GetInputFromFile function; folded post
# processing code for IPtoFQDN into IPto FQDN function
#
# Revision 1.18  1999/07/02 21:21:15  mohlerb
# changes from Bruce Mohler including:
# Perl complained about the UnameCommand in syssumm.pl not being
# used because it couldn't see (yet) the references down in the
# OS modules.  Since AIX doesn't have the uname command in the
# same place as the other OSs (so far), I moved the UnameCommand
# local to each OS module.
#
# Revision 1.17  1999/06/17 17:30:54  mohlerb
# changes from Frank Crawford including:
# Moved ReturnDomainNameSunOS to common.pm.
#
# Revision 1.16  1999/06/15 14:04:42  mohlerb
# changes from Frank Crawford including:
# fix for ReturnRunningSamba function: added $InetdConfPath
#
# Revision 1.15  1999/06/11 18:33:14  mohlerb
# changes from Bruce Mohler including:
# put in "stub" for adding domainname to NIS field if NIS running
#
# Revision 1.14  1999/06/11 17:47:21  mohlerb
# changes from Bruce Mohler including:
# factored ReturnRunningSamba routine into common.pm
#
# Revision 1.13  1999/06/10 22:50:38  mohlerb
# changes from Bruce Mohler including:
# modified unified call to Net::Domain::Hostname to determine
# hostname of local system as it calls a CPAN module and we
# want to avoid this for purposes of portability; instead we
# call Sys::Hostname which is part of the core Perl 5 distribution
#
# Revision 1.12  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.11  1999/06/10 21:00:06  mohlerb
# changes from Frank Crawford including:
# Removed some unused variables.
# Cleaned up for prototyping.
# Included new Samba test routine.
#
# Revision 1.10  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.9  1999/06/04 18:51:51  mohlerb
# changes from Bruce Mohler including:
# Added code for "RunAsRoot" functionality.
#
# Revision 1.8  1999/06/03 21:18:45  mohlerb
# changes from Paul Farrall including:
# Added ReturnIPAddressHPUX and ReturnSubnetMaskHPUX functions.
#
# Revision 1.7  1999/05/27 14:35:37  mohlerb
# changes from Frank Crawford including:
# Add a missing $ in a variable.
# Remove an ineffective gethostbyname call.
#
# Revision 1.6  1999/05/27 14:09:57  mohlerb
# changes from Keong Lim including:
# added Header RCS tag above Revision tag
#
# 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:53:09  mohlerb
# changes from Frank Crawford including:
# Added use of Perl5 module Sys::hostname (in common.pm).
# Reworked routines to obtain FQDN.
# Fixed problem when RPM doesn't return an InstallDate.
# Yet another fix to match ethernet device.
# Fixed processor return to return multiple values.
#
# Revision 1.3  1999/05/24 23:56:33  mohlerb
# changes from Frank Crawford including:
# Made pattern match more general for Network device (match "10/100").
# Made $Year match standard for old version of timelocal.
#
# Revision 1.2  1999/05/24 23:51:49  mohlerb
# changes from Frank Crawford including:
# pattern matching (as it wasn't always successful)
# handling of scsi disks (sometimes scsi_info didn't exist so
#    access /proc/scsi/scsi)
# handling new format for Linux 2.2 /proc/cpuinfo
# read MIPS value if available instead of calculating from BogoMips
# added some units to Mb fields
# fix the conversion from kb to Mb (1000 -> 1024)
# limit parsing of disks to the first entry seen for each device
#
# Revision 1.1  1999/05/14 22:58:05  mohlerb
# Initial revision
#

use Sys::Hostname;
use Time::Local;
use common;

require Exporter;
@ISA = qw(Exporter);
###########################################################################
# the following sub-routines (&) are variables ($) will be 
# visible in a program using this module...
@EXPORT = qw(
	&InitializeLinux
);
# 
# Note that you do not need to export the Return* subroutine
# names since they are being called by reference.
# 
###########################################################################

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


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

# parts of OS version - filled in by InitializeHPUX()
$OSVersion = "";
$OSVersionMajor = "";
$OSVersionMinor = "";

$BootProtocol = "Unknown";			# set by SetBootProtocol

# where commands are located			standard args
# CUSTOMIZE
$AmdCommand = "/usr/sbin/amd";
$DfCommand = "/bin/df";				$LocalFileSystems = "-k -t ext2";
						$NFSFileSystems = "-k -t nfs";
$DmesgCommand = "/bin/dmesg";
$DpkgCommand = "/usr/bin/dpkg";
$HostidCommand = "/usr/bin/hostid";
$IfconfigCommand = "/sbin/ifconfig";		$IfconfigArgs = "-a";
# $LpcCommand = "/usr/sbin/lpc";		$LpcArgs = "status";
$LpdCommand = "/usr/sbin/lpd";
$MountdCommand = "/usr/sbin/rpc.mountd";
$NameServerPattern = "named";
$NetstatCommand = "/bin/netstat";		$NetstatArgs = "-nr";
$NfsdCommand = "/usr/sbin/rpc.nfsd";
$NfsdCommandPattern = "nfsd";
$PsCommand = "/bin/ps";				$PsArgs = "set in InitializeLinux";
$RpmCommand = "/bin/rpm";
$SendmailCommand = "/usr/sbin/sendmail";
$UnameCommand = "/bin/uname";

# where files are located
# CUSTOMIZE
$CpuinfoPath = "/proc/cpuinfo";
$ExportsPath = "/etc/exports";
$IdeDiskDevicePath = "/dev/hd[a-z]";
$IdeProcPath = "/proc/ide";
$InetdConfPath = "/etc/inetd.conf";
$IssuePath = "/etc/issue";
$MeminfoPath = "/proc/meminfo";
$NameserverConfPath = "/etc/named.conf";
$NetConfPath = "/etc/sysconfig/network";	$DefaultRouterTag = "GATEWAY";
$PrintcapPath = "/etc/printcap";
$ResolvConfPath = "/etc/resolv.conf";
$ScsiInfoPath = "/proc/scsi/scsi";
$ScsiProcPath = "/proc/scsi/\*";
$SendmailConfPath = '/etc/sendmail.cf';
$SyslogConfPath = "/etc/syslog.conf";
$SyslogPath = "/var/log/messages*";

# where distribution release files are located
# CUSTOMIZE
$CalderaReleasePath = "/etc/issue";
$ConectivaReleasePath = "/etc/vers\xe3o-conectiva";
$DebianReleasePath = "/etc/debian_version";
$MandrakeReleasePath = "/etc/mandrake-release";
$RedHatReleasePath = "/etc/redhat-release";

# Using only C locale.  This is needed for some distros.
delete $ENV{LANG};
delete $ENV{LINGUAS};
delete $ENV{LC_ALL};

# list only RPM files added since install or *all* RPM files?
# CUSTOMIZE
$ListNewerRpmFiles = 1;				# ReturnPatchesLinux()

# BogoMips to clock speed
%BogoMips = (
	486 => 0.5,
	Pentium => 0.4,
	686 => 1.0,
);


###########################################################################
# Subroutines
###########################################################################

# 
# This subroutine is called before any others.  It's
# reason for existence is to plug in the references to
# subrotines below to actually generate the information.
# 

sub InitializeLinux ()
{
	print "InitializeLinux()...\n" if $main::Debug;

	WhatVersionOfLinux();

	# we need the nodename for later on...
	my $NodeName = hostname();
	# RH Linux 6.0 tends to return the FQDN rather than just the node name
	$NodeName =~ /^([^.]+)/;
	$main::NodeName = $1;
	print "\tNodeName is $main::NodeName\n" if $main::Debug;

	# 
	# initialize variables tied to OS level
	# 
	my $PsVersion = `$PsCommand --version`;
	chomp($PsVersion);
	if ($PsVersion =~ /^procps version (\d\.\d)/ && $1 >= 1.9) {
		$PsArgs = "-eaf";
	}
	else {
		$PsArgs = "alx";
	}
	print "\tPsVersion is $PsVersion: PsArgs = $PsArgs\n" if $main::Debug;

	# plug in appropriate subroutines
	$main::Routines{General}{Vendor} = \&ReturnVendorLinux;
	$main::Routines{General}{Model} = \&ReturnModelLinux;
	$main::Routines{General}{HostId} = \&ReturnHostIdLinux;

	$main::Routines{Hardware}{Processors} = \&ReturnProcessorsLinux;
	$main::Routines{Hardware}{Memory} = \&ReturnMemoryLinux;
	$main::Routines{Hardware}{Disks} = \&ReturnDisksLinux;
	$main::Routines{Hardware}{Tapes} = \&ReturnTapesLinux;
	$main::Routines{Hardware}{StorageManager} = \&ReturnStorageManagerLinux;
	$main::Routines{Hardware}{Eeprom} = \&ReturnNotApplicableField;

	$main::Routines{Network}{DomainName} = \&ReturnDomainNameLinux;
 	$main::Routines{Network}{DefaultRouter} = \&ReturnDefaultRouterLinux;
 	$main::Routines{Network}{NameServer} = \&ReturnNameServerLinux;
	$main::Routines{Network}{IPAddress} = \&ReturnIPAddressLinux;
	$main::Routines{Network}{SubnetMask} = \&ReturnSubnetMaskLinux;
 	$main::Routines{Network}{NetworkInterfaces} = \&ReturnNetworkInterfacesLinux;
 
	$main::Routines{Software}{OsName} = \&ReturnOsNameLinux;
	$main::Routines{Software}{OsVersion} = \&ReturnOsVersionLinux;
	$main::Routines{Software}{NbrOfLicenses} = \&ReturnNotApplicableField;
	$main::Routines{Software}{Patches} = \&ReturnPatchesLinux;
	$main::Routines{Software}{Printers} = \&ReturnPrintersLinux;
	$main::Routines{Software}{Services} = \&ReturnServices;
	$main::Routines{Software}{IPAddressResolution} = \&ReturnIPAddressResolutionLinux;
	$main::Routines{Software}{GraphicalUserInterface} = \&ReturnGraphicalUserInterfaceLinux;
	$main::Routines{Software}{FontServer} = \&ReturnFontServerLinux;
	$main::Routines{Software}{Fstab} = \&ReturnFstab;
	$main::Routines{Software}{LocalFileSystems} = \&ReturnLocalFileSystemsLinux;
	$main::Routines{Software}{RemoteFileSystems} = \&ReturnRemoteFileSystemsLinux;
	$main::Routines{Software}{ExportedFileSystems} = \&ReturnExportedFileSystemsLinux;
	$main::Routines{Software}{Swap} = \&ReturnMemoryLinux;

	# if you have OS-specific services, add them here
	# push @main::Services, "Local-Service";

	RemoveLocalExclusions();

	return;
}


###########################################################################
# Global
###########################################################################


sub WhatVersionOfLinux
{
	print "WhatVersionOfLinux()...\n" if $main::Debug;

	# 
	# There should be one "paragraph" for each distribution
	# 
	# Note that this code is echoed in the ReturnVendorLinux
	# function - keep these functions in sync!!
	# 
	# You have to check for Mandrake *BEFORE* Red Hat
	# since the Mandrake distribution will *ALSO* have
	# a Red Hat release file...
	# 
	# You have to check for Conectiva *BEFORE* Caldera
	# since the Conectiva distribution will *ALSO* have
	# a Caldera release file...
	# 
	# You have to check for Debian *BEFORE* Caldera
	# since /etc/issue is a standard file in Debian too...
	# 
	# Mandrake
	if ( -f "$MandrakeReleasePath" ) {

		# 
		# This file should contain a single line of text
		# similar to
		# 
		#	Linux Mandrake release 6.0 (Venus)
		# 
		# We only care about the numbers here...
		# 
		my @ReleaseText = ReturnArrayFile($MandrakeReleasePath);
		$ReleaseText[0] =~ /^Linux Mandrake release (\d+)\.(\d+) /;
		$OSVersionMajor = $1;
		$OSVersionMinor = $2;
	}
	# Red Hat
	elsif ( -f "$RedHatReleasePath" ) {

		# 
		# This file should contain a single line of text
		# similar to
		# 
		#	release 4.2 (Biltmore)
		#	Red Hat Linux release 6.0 (Hedwig)
		# 
		# We only care about the numbers here...
		# 
		my @ReleaseText = ReturnArrayFile($RedHatReleasePath);
		$ReleaseText[0] =~ /^.*release (\d+)\.(\d+) /;
		$OSVersionMajor = $1;
		$OSVersionMinor = $2;
	}
	# Debian
	elsif ( -f "$DebianReleasePath" ) {
		#
		# This file should contain a single line of text
		# similar to
		#
		#      2.2
		#
		# Its all in the number...
		my @ReleaseText = ReturnArrayFile($DebianReleasePath);
		$ReleaseText[0] =~ /(\d+)\.(\d+)/;
		$OSVersionMajor = $1;
		$OSVersionMinor = $2;
	}
	# Conectiva
	elsif ( -f "$ConectivaReleasePath" ) {

		#
		# This file should contain a single line of text
		# similar to
		#
		#       Conectiva Linux 5.0
		#
		# We only care about the numbers here...
		#
		my @ReleaseText = ReturnArrayFile($ConectivaReleasePath);
		$ReleaseText[0] =~ /^Conectiva Linux (\d+)\.(\d+)/;
		$OSVersionMajor = $1;
		$OSVersionMinor = $2;
	}
	# Caldera
	elsif ( -f "$CalderaReleasePath" ) {

		# 
		# This file should contain several lines of text
		# similar to
		# 
		#	Caldera OpenLinux(TM)
		#	Version 2.3
		#	Copyright 1996-1999 Caldera Systems, Inc.
		# 
		# We only care about the numbers here...
		# Don't make assumptions about how many blank lines exist...
		# 
		my @ReleaseText = ReturnArrayFile($CalderaReleasePath);
		my $TrapNextLine = 0;
		foreach $Line (@ReleaseText) {
			if ( $Line =~ /^Caldera OpenLinux/ ) {
				$TrapNextLine = 1;
				next;
			}
			if ( $TrapNextLine ) {
				$Line =~ /^Version (\d+)\.(\d+)$/;
				$OSVersionMajor = $1;
				$OSVersionMinor = $2;
			}
		}
		die "syssumm couldn't determine version of Caldera OpenLinux";
	}
	else {
		my $Version = `$UnameCommand -r`;
		chomp($Version);
		my @Version = split('\.', $Version, 3);
		$OSVersionMajor = join('.', @Version[0,1]);
		$OSVersionMinor = $Version[2];
	}

	$OSVersion = "$OSVersionMajor.$OSVersionMinor";

	print "\tOS version is $OSVersion...\n" if $main::Debug;
	print "\tMajor OS version is $OSVersionMajor...\n" if $main::Debug;
	print "\tMinor OS version is $OSVersionMinor...\n" if $main::Debug;

	return;
}


###########################################################################
# General
###########################################################################

sub ReturnVendorLinux ($)
{	my $Subcategory = shift;

	# 
	# There should be one "paragraph" for each distribution
	# 
	# Note that this code is echoed in the WhatVersionOfLinux
	# function - keep these functions in sync!!
	# 
	# You have to check for Mandrake *BEFORE* Red Hat
	# since the Mandrake distribution will *ALSO* have
	# a Red Hat release file...
	# 
	# Mandrake
	if ( -f "$MandrakeReleasePath" ) {

		# 
		# This file should contain a single line of text
		# similar to
		# 
		#	Linux Mandrake release 6.0 (Venus)
		# 
		my @ReleaseText = ReturnArrayFile($MandrakeReleasePath);
		$ReleaseText[0] =~ /^\w+ (\w+) release/;
		return ( "$1" );
	}
	# Red Hat
	elsif ( -f "$RedHatReleasePath" ) {

		# 
		# This file should contain a single line of text
		# similar to
		# 
		#	Red Hat Linux release 6.0 (Hedwig)
		# 
		# We only care about the numbers here...
		# 
		my @ReleaseText = ReturnArrayFile($RedHatReleasePath);
		if ($ReleaseText[0] =~ /^(\w+ \w+) Linux release /) {
			return ( "$1" );
		}
		else {        # Old versions just contain "release 4.2"
			return ( "Red Hat" );
		} 
	}
	# Debian
	elsif ( -f "$DebianReleasePath" ) {
		#
		# This file should contain a single line of text
		# similar to
		#
		#      2.2
		return ( "Debian" );
	}
	# Conectiva
	elsif ( -f "$ConectivaReleasePath" ) {

		#
		# This file should contain a single line of text
		# similar to
		#
		#       Conectiva Linux 5.0
		#
		# We only care about the numbers here...
		#
		return ( "Conectiva" );
	}
	# Caldera
	elsif ( -f "$CalderaReleasePath" ) {

		# 
		# This file should contain several lines of text
		# similar to
		# 
		#	Caldera OpenLinux(TM)
		#	Version 2.3
		#	Copyright 1996-1999 Caldera Systems, Inc.
		# 
		# We only care about the numbers here...
		# Don't make assumptions about how many blank lines exist...
		# 
		my @ReleaseText = ReturnArrayFile($CalderaReleasePath);
		foreach $Line (@ReleaseText) {
			if ( $Line =~ /^(Caldera OpenLinux)/ ) {
				return ( "$1" );
				next;
			}
		}
		die "syssumm couldn't determine vendor of Linux";
	}

# 	# CUSTOMIZE
# 	my @IssueText = ReturnArrayFile($IssuePath);
# 	foreach $Line (@IssueText) {
# 		next if ( $Line eq "" );
# 		return ( "$Line" );
# 	}

	# just in case...
	return ( "Unknown" );
}


sub ReturnModelLinux ($)
{	my $Subcategory = shift;

	my $Model = `$UnameCommand -m`;
	chomp($Model);

	return ( $Model );
}


sub ReturnHostIdLinux ($)
{	my $Subcategory = shift;

	my $IdNbr = `$HostidCommand`;
	chomp($IdNbr);

	return ( $IdNbr );
}


###########################################################################
# Hardware
###########################################################################

# 
# The /proc directory is a source of lots of hardware
# configuration information...
# 

sub ReturnProcessorsLinux ($)
{	my $Subcategory = shift;
	my %Processor = ( );		# temporary storage for each processor
	my @Processors = ( );		# each entry contains data from %Processors
	my $ProcessorSpeed = "";	# MHz
	my @Response = ( );		# return value. contains \@Processors

	# 
	# We get the information about the machine's processors
	# from the /proc/cpuinfo file.  Unfortunately, each
	# architecture (Intel, Alpha, SPARC, etc.) has a 
	# different format for this file.
	# 

	# get data from /proc/cpuinfo
	my @CpuinfoText = ReturnArrayFile($CpuinfoPath);
	if ($CpuinfoText[0] =~ /^processor/) {
		# For Intel machines, each processor is described in
		# the following format:
		#
		#       processor       : 0
		#       cpu             : 486
		#       model           : DX/2
		#       vendor_id       : GenuineIntel
		#       stepping        : 5
		#       fdiv_bug        : no
		#       hlt_bug         : no
		#       f00f_bug        : no
		#       fpu             : yes
		#       fpu_exception   : yes
		#       cpuid           : yes
		#       wp              : yes
		#       flags           : fpu vme
		#       bogomips        : 33.18                                 

		foreach $Line ( @CpuinfoText ) {

			next unless $Line =~ /:/;

			# strip out most spaces
			$Line =~ s/\s*:\s*/:/g;
# 			print "\tLine is $Line\n" if $main::Debug;

			# split tag and value(s)
			my ( $Tag, $Value ) = split(/:/, $Line);
# 			print "\tTag/Value is $Tag/$Value\n" if $main::Debug;

			# 
			# See if $Processor{"processor"} is defined.  List of
			# data always begins with "processor".  If defined,
			# we've already seen at least one processor so push
			# data from previous iteration.
			# 
			if ( ($Tag eq "processor") &&  (defined($Processor{"processor"})) ) {
				$ProcessorSpeed = CalculateProcessorSpeed($Processor{bogomips},
					$Processor{cpu}, $Processor{'cpu MHz'});
				push @Processors, "processor $Processor{processor} $Processor{vendor_id} " . "$Processor{cpu} $Processor{model} $ProcessorSpeed";
	
				%Processor = ();  # re-initialize for next processor
			}   
			# end pushing out data for previous

			# Assign $Tags/$Values to %Processors hash
			# The following 2 tags need special treatment?      
			if ( $Tag eq "model name" ) {                   
				$Processor{'model'} = $Value;               
				$Processor{'cpu'} = "";
				next;
	
				# Assign the rest of the tags
			} elsif ( $Tag eq "cpu" or $Tag eq "model" or $Tag eq "vendor_id"
				or $Tag eq "bogomips" or $Tag eq "cpu MHz" or $Tag eq  
	"processor") {
	
				$Processor{$Tag} = $Value;
				next;
			}        
		}
# 		$Value =~ s/Genuine//;	# strip 'Genuine' label if it exists.

		# Write data from last processor
		$ProcessorSpeed = CalculateProcessorSpeed($Processor{bogomips}, 
			$Processor{cpu}, $Processor{'cpu MHz'});
		push @Processors, "processor $Processor{processor} " . "$Processor{vendor_id} " . "$Processor{cpu} " . "$Processor{model} $ProcessorSpeed";
	} elsif ($CpuinfoText[0] =~ /^cpu/) {
		# For Alpha machines there's only one processor description
		# no matter how many there are:
		#
		#	cpu                     : Alpha
		#	cpu model               : EV56
		#       cpu variation           : 7
		#	cpu revision            : 0
		#	cpu serial number       :
		#	system type             : Noritake
		#	system variation        : 0
		#	system revision         : 0
		#	system serial number    :
		#	cycle frequency [Hz]    : 400000000
		#	timer frequency [Hz]    : 1024.00
		#	page size [bytes]       : 8192
		#	phys. address bits      : 40
		#	max. addr. space #      : 127
		#	BogoMIPS                : 395.31
		#	kernel unaligned acc    : 0 (pc=0,va=0)
		#	user unaligned acc      : 4 (pc=20000185b4c,va=11ffffbdc)
		#	platform string         : AlphaServer 1000A 5/400
		#
		# If there are multiple CPUs, there will be a line like this
		# following the above:
		#
		#	CPUs probed %d active %d map 0x%lx IPIs %ld

		foreach $Line ( @CpuinfoText ) {
			next unless $Line =~ s/\s*:\s*/:/g;
			my ($Tag, $Value) = split(/:/, $Line);
			$Processor{$Tag} = $Value;
		}
		my $n_cpus = 1;
		if ( defined $Line &&
			$Line =~ /^CPUs probed (\d+) active (\d+) map 0x([0-9A-Fa-f]+) IPIs (\d+)$/ ) {
			$n_cpus = $1;
		}
		for ($i = 0; $i < $n_cpus; ++$i) {
			push @Processors, "processor $i $Processor{cpu} $Processor{'cpu model'}-$Processor{'cpu variation'}-$Processor{'cpu revision'} $Processor{BogoMIPS} BogoMIPS";
		}
	}

# 	print "\tProcessors contains:\n" if $main::Debug;
# 	foreach $Processor (@Processors) {
# 		print "\t\t$Processor\n" if $main::Debug;
# 	}

	@Response = ForceMultipleResponses(\@Processors);

	return @Response;
}


# 
# See the BogoMips Mini-HOWTO...
# 
# The equation is clock speed (MHz) = BogoMips value / Constant value
# dependent on architecture)
# Return value is clock speed or "speed unknown"
# 

sub CalculateProcessorSpeed
{	my $Bogomips = shift;		# BogoMips value from /proc/cpuinfo
	my $Cpu = shift;		# Type of cpu (determines constant)
	my $ProcessorSpeed = shift;	# Clock speed from /proc/cpuinfo

	print "CalculateProcessorSpeed($Bogomips, $Cpu, $ProcessorSpeed)...\n" if $main::Debug;

	if ( $ProcessorSpeed ) {
		# get rid of digits trailing decimal point and append label
		$ProcessorSpeed = (int $ProcessorSpeed) . " MHz";
	}
	elsif ( exists $BogoMips{$Cpu} ) {
		# %BogoMips contains eqn constants for cpu types
		$ProcessorSpeed = ( int($Bogomips) / $BogoMips{$Cpu} );
	}
	else {
		$ProcessorSpeed = "(speed unknown)";
	}

	return $ProcessorSpeed;
}


sub ReturnMemoryLinux ($)
{	my $Subcategory = shift;
	my $Blocks = "";

	# 
	# Reading from /proc/meminfo will reveal
	# information about how much memory is on 
	# the system and how much of it is in use
	# 
	#	        total:    used:    free:  shared: buffers:  cached:
	#	Mem:  31547392 25264128  6283264 41041920  1118208 13602816
	#	Swap: 133148672   905216 132243456
	#	MemTotal:     30808 kB
	#	MemFree:       6136 kB
	#	MemShared:    40080 kB
	# 

	my @MeminfoText = ReturnArrayFile($MeminfoPath);
	foreach $Line ( @MeminfoText ) {
	if ( $Subcategory eq 'Memory' && $Line =~ /^Mem:\s+(\d+)\s/ ) {
		$Blocks = $1;
		last;
	}
	if ( $Subcategory eq 'Swap' && $Line =~ /^Swap:\s+(\d+)\s/ ) {
		$Blocks = $1;
		last;
	}
	}
	if ( $Blocks ne '' ) {
		return ( int ( $Blocks / ( 1024 * 1024 ) ) ) . "MB";
	}
 
        return ( ReturnUnknownField($Subcategory) );
}


sub ReturnDisksLinux ($)
{	my $Subcategory = shift;

	# locate IDE disks
	foreach $Disk ( glob "$IdeDiskDevicePath" ) {
		$Disk =~ /(hd[a-z])$/;
		my $DiskId = $1;
		my $DeviceModelPath = "$IdeProcPath/$DiskId/model";
		next if ( ! -r "$DeviceModelPath" );
		my $DeviceModel = `cat $DeviceModelPath`;
		chomp $DeviceModel;
		my $DeviceMediaPath = "$IdeProcPath/$DiskId/media";
		my $DeviceMedia = `cat $DeviceMediaPath`;
		chomp $DeviceMedia;
		print "\tDisk is $Disk...\n" if $main::Debug;;
		push @Response, "IDE $DeviceMedia dev=$Disk model=\"$DeviceModel\"";
	}

	# locate SCSI controllers and store them into a hash
	print "\nLocating SCSI controllers...\n" if $main::Debug;
	foreach $ControllerDirectory ( glob "$ScsiProcPath" ) {
		# ignore the "scsi" "file" - we'll come back to it later
		next if ( $ControllerDirectory =~ /^scsi$/ );
		# if it's not a directory, then it's not a controller
		next if ( ! -d $ControllerDirectory );
		print "\tProcessing controller directory $ControllerDirectory...\n" if $main::Debug;
		foreach $Controller ( glob "$ControllerDirectory/\*" ) {
			my @ControllerDescription = ( );

			# isolate the controller number
			$Controller =~ /\/(\d+)$/;
			my $ControllerNumber = $1;
			print "\tController number is $ControllerNumber...\n" if $main::Debug;

			@ControllerDescription = ReturnArrayFile("$Controller");
			my $LastLine = 0;
			my $Description = "";
			foreach $Line (@ControllerDescription) {
				if ( $LastLine ) {
					$Line =~ /^\s+(.*)\s*$/;
					$Description .= " " . $1;
					last;
				}
				if ( $Line =~ /SCSI Adapter: (.*)$/ ) {
					$Description = $1;
					# trim off any trailing whitespace
					$Description =~ s/\s*$//;
					$LastLine = 1;
					next;
				}
			}
			print "\tDescription is $Description...\n" if $main::Debug;
			$ScsiControllers{$ControllerNumber} = $Description;
		}
	}

	# locate SCSI disks
	my @ScsiConfiguration = ReturnArrayFile("$ScsiInfoPath");
	# summarize all SCSI devices by controller
	my $CurrentController = -1;
	foreach $Controller ( sort keys %ScsiControllers ) {
		if ( $Controller != $CurrentController ) {
			# output the controller description
			push @Response, "SCSI Controller $Controller $ScsiControllers{$Controller}";
			$CurrentController = $Controller;
		}
		print "\tProcessing controller $Controller...\n" if $main::Debug;

		my $GrabIt = 0;
		my $Line1 = "";
		my $Line2 = "";
		my $Line3 = "";
		LINE: foreach $Line ( @ScsiConfiguration ) {
			if ( $GrabIt ) {
				if ( $Line2 ) {
					$Line3 = $Line;
					my $Response = FormatScsiDeviceDescription( $Line1, $Line2, $Line3 );
					push @Response, $Response;
					$GrabIt = 0;
					next;
				}
				else {
					$Line2 = $Line;
					next;
				}
			}
			if ( $Line =~ /^Host: scsi(\d+)/ ) {
				# correct controller?
				my $ThisController = $1;
				next LINE if ( $ThisController != $Controller );
				# if it's the right controller, setup to 
				# grab this line and the next 2...
				print "\tFound host line...\n$Line\n" if $main::Debug;
				$Line1 = $Line;
				$Line2 = $Line3 = "";
				$GrabIt = 1;
			}
		}
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


# XXX - first draft - kludgey

sub FormatScsiDeviceDescription ($$$)
{	my $Line1 = shift;
	my $Line2 = shift;
	my $Line3 = shift;
	my $Address = "";
	my $Vendor = "";
	my $Model = "";
	my $Type = "";

	# 
	# The format of the data coming into this function
	# is as follows:
	# 
	# Host: scsi0 Channel: 00 Id: 02 Lun: 00
	#   Vendor: ARCHIVE  Model: Python 25588-004 Rev: 1.36
	#   Type:   Sequential-Access                ANSI SCSI revision: 02
	# Host: scsi0 Channel: 00 Id: 04 Lun: 00
	#   Vendor: QUANTUM  Model: FIREBALL1080S    Rev: 1Q09
	#   Type:   Direct-Access                    ANSI SCSI revision: 02
	# 

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

	$Line1 =~ /Id: (\d+) /;
	$Address = $1;

	$Line2 =~ /Vendor:\s+(.+)\s+Model:/;
	$Vendor = $1;
	$Vendor =~ s/\s*$//;

	$Line2 =~ /Model:\s+(.+)\s+Rev:/;
	$Model = $1;
	$Model =~ s/\s*$//;

	$Line3 =~ /Type:\s+(.+)\s+ANSI SCSI/;
	$Type = $1;

	return "SCSI Device addr=$Address model=\"$Vendor $Model\" type=$Type";
}


sub ReturnTapesLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );

	# 
	# Note that we pick up (SCSI) tape drives in the 
	# ReturnDisks function
	# 

	# this assumes that Disk are above Tapes in the syssumm.format
	# file on the web server...
	push @Response, "See Disks";

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnStorageManagerLinux ($)
{	my $Subcategory = shift;
	my @Response = ();

	push @Response, "Not Applicable (yet)";

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


###########################################################################
# Network
###########################################################################

sub ReturnDomainNameLinux ($)
{	my $Subcategory = shift;

	return ReturnDomainName($Subcategory, $ResolvConfPath);
}


sub ReturnDefaultRouterLinux ($)
{	my $Subcategory = shift;

	my $DefaultRouter = SearchFileForTagValue($NetConfPath, $DefaultRouterTag);
	if ( $DefaultRouter eq $NotFound ) {
		# The default router isn't statically defined.
		my @NetstatOutput = ReturnArrayExec("$NetstatCommand $NetstatArgs");
		foreach ( @NetstatOutput ) {
			# Look for a line starting with '0.0.0.0'
			my @Fields = split;
			if ( $Fields[0] eq '0.0.0.0' ) {
				$DefaultRouter = $Fields[1];
				last;
			}
		}
	}

	if ( $DefaultRouter =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/ ) {
		# returned IP address
		return (IPtoFQDN($DefaultRouter));
	} elsif ( $DefaultRouter =~ /\w+/ ) {
		# Returned some other string like hostname
		# add the IP address, if possible
		return (FQDNtoIP($DefaultRouter));
	} else {
		# Didn't return anything meaningful or $NotFound
		return "unknown";
	}
}


sub ReturnNameServerLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );

	my @ResolvConfText = ReturnArrayFile($ResolvConfPath);
	if ( scalar(@ResolvConfText) < 1 ) {
		push @Response, "None";
	}
	else {
		foreach $Line (@ResolvConfText) {
			# find the first line with "nameserver" in it?
			if ( $Line =~ /^nameserver\s+([\d]+\.[\d]+\.[\d]+\.[\d]+)/) {
				my $NameServerIP = $1;
				# do we recognize this nameserver?
				my $NameServerFQDN = IPtoFQDN($NameServerIP);
				push @Response, $NameServerFQDN;
				next;
			}
		}
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnIPAddressLinux ($)
{
	# Returns the IP address associated with "$main::NodeName"
	# The "default" IP address.
	my @Addresses = (gethostbyname $main::NodeName)[4];
	my ($a, $b, $c, $d) = unpack('C4', $Addresses[0]);
	$IPAddress = "$a.$b.$c.$d";

	return ( $IPAddress );
}


sub ReturnSubnetMaskLinux ($)
{	my $SubnetMask = "Unknown";

	my $IPAddress = ReturnIPAddressLinux(@_);  #default IP address
	my @Interfaces = ReturnNetworkInterfacesLinux(@_);            

	# Find interface corresponding to "default"                 
	# IP address and get netmask for it                         
	foreach $Line (@Interfaces) {                               
		next unless ( $Line =~ /$IPAddress/ );                    
		# 
		# remember that we're not scanning the syslog format
		# output, but the stuff from ReturnNetworkInterfacesLinux...
		# 
		if ( $Line =~ /netmask=(\d+\.\d+\.\d+\.\d+)/ ) {          
			$SubnetMask = $1;
		}
	}

	return ( $SubnetMask );
}


# 
# This subroutine searchs for interfaces in syslog log, and 
# output of "dmesg", and ifconfig commands.  Search order is 
# syslog log -> dmesg output -> ifconfig output.  Some of 
# this was borrowed from SunOS.pm routines (by Jeff Putsch?).  
# I tried to use similar methods and the same variable names 
# as in that module for consistency.
# 
# The search in syslog log and dmesg output only search for 
# ethernet devices.  Ifconfig can also find other types of 
# devices (e.g. PPP).
#

sub ReturnNetworkInterfacesLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );
	my %Seen = ( );			# keys are interface name, value is type

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

	# search for interfaces in the syslog log
	my @SyslogText = ReturnArrayFile("$SyslogPath");
	foreach $Line ( @SyslogText ) {

		# we're looking for patterns like "eth0:..."
		if ( $Line =~ /^(eth\d):\s+(.+)\s+at / ) {

			# map regexp variables to something more mnemonic
			my $Iface = $1;		# name of interface found
			my $IfaceDevice = $2;	# type of device like "3COM 3c905"

			# don't add interface to has if it's already been 
			# found somewhere else
			next if defined ( $Seen{$Iface} );

			$Seen{$Iface} = $IfaceDevice;
		}
	}

	# search for interfaces in dmesg output
	if ( -x $DmesgCommand ) {
		my @DmesgOutput = ReturnArrayExec("$DmesgCommand");
		foreach $Line ( @DmesgOutput ) {
	
			# we're looking for patterns like "eth0:..."
			# eth0: 3Com 3c905B Cyclone 100baseTx at 0xec00, 00:c0:4f:6e:74:f0, IRQ 14
			if ( $Line =~ /^(eth\d):\s+(.+)\s+at .* IRQ/ ) {
	
				# map regexp variables to something more mnemonic
				my $Iface = $1;		# name of interface found
				my $IfaceDevice = $2;	# type of device like "3COM 3c905"
	
				# don't add interface to has if it's already been 
				# found somewhere else
				next if defined ( $Seen{$Iface} );
	
				$Seen{$Iface} = $IfaceDevice;
			}
		}
	}

	# search for interfaces in ifconfig output
	if ( -x $IfconfigCommand ) {
		my @IfconfigOutput = ReturnArrayExec("$IfconfigCommand $IfconfigArgs");
		foreach $Line ( @IfconfigOutput ) {
	
			next if ( $Line =~ /^lo/ );	# skip loopback device

			next unless ( $Line =~ /^(\w+)\s+/ );

			my $Iface = $1;			# name of interface found
			my $IfaceDevice = "Unknown" ;	# This info not in 
							# ifconfig value is 
							# passed to AddNetInfo()


			# don't add interface to has if it's already been 
			# found somewhere else
			next if defined ( $Seen{$Iface} );
	
			$Seen{$Iface} = $IfaceDevice;
		}
	}

	# details on found interfaces
	foreach $Interface ( keys %Seen ) {
		my $Response = AddNetInfo($Interface, $Seen{$Interface});
		push @Response, $Response if ( $Response);
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub AddNetInfo ($$)
{	my $NetworkInterfaceName = shift;	# interface descriptor like 'eth0'
	my $NICDevice = shift;			# Specific device. Like: '3COM 3c905'

	print "AddNetInfo($NetworkInterfaceName, $NICDevice)...\n" if $main::Debug;

	# use ifconfig to get the rest of the information
	my @IfconfigText = ReturnArrayExec("$IfconfigCommand $NetworkInterfaceName");

	my $MACAddress = "";
	my $IPAddress = "";
	my $BroadcastMask = "";
	my $NetMask = "";
	my $NICType = "unknown";

	# assume that there could be multiple fields on each line...
	foreach $Line (@IfconfigText) {
		# pick up the hardware address
		if ( $Line =~ /HWaddr (\w+:\w+:\w+:\w+:\w+:\w+)/ ) {
			$MACAddress = $1;
		}
		# grab the IP address
		if ( $Line =~ /inet addr:(\d+\.\d+\.\d+\.\d+)/ ) {
			$IPAddress = $1;
		}
		# grab the broadcast mask
		if ( $Line =~ /Bcast:(\d+\.\d+\.\d+\.\d+)/ ) {
			$BroadcastMask = $1;
		}
		# grab the netmask
		if ( $Line =~ /Mask:(\d+\.\d+\.\d+\.\d+)/ ) {
			$NetMask = $1;
		}
		# get device type
		# This is a little ugly in terms of expansion, but I doubt
		# anyone will have anything but ethernet or PPP anyway.   
		if ( $Line =~ /^eth\d/ ) {
			$NICType = "ether";   
		} elsif ( $Line =~ /^ppp\d/ ) {
			$NICType = "PPP";          
		}
		# defaults to "unknown" - see above
	}

	my $NICDescription = 
		"$NetworkInterfaceName type=$NICType device=$NICDevice macaddr=$MACAddress " .
		"ip=$IPAddress netmask=$NetMask broadcast=$BroadcastMask ";

	return $NICDescription;
}


# 
# XXX where is this called?
# 
sub SetBootProtocol					# PRIVATE
{
	$BootProtocol = SearchFileForTagValue($NetConfPath, $DefaultRouterTag);

	return;
}


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

# 
# We're using alot of the same tests for existance
# that are used by the /etc/rc.d/init.d scripts
# 

sub ReturnOsNameLinux ($)
{	my $Subcategory = shift;

	return ( "Linux" );
}


sub ReturnOsVersionLinux ($)
{	my $Subcategory = shift;

	my $Version = `$UnameCommand -r`;
	chomp($Version);

	$Version = "$OSVersion/$Version" if ($OSVersion ne $Version);
	return ( "$Version" );
}


sub ReturnPatchesLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );

	# 
	# This function depends on the rpm command
	# If this distribution of Linux doesn't have the rpm
	# command, then bail...
	# 
	# XXX This function should be expanded to handle 
	# distributions like Debian and Slackware.  Those
	# distributions should be "upgraded" to support rpms...
	# 
	if ( -x $RpmCommand ) {
		@Response = ReturnRPMPatches($Subcategory);
	}
	elsif (-x $DpkgCommand) {
		@Response = ReturnDPKGPatches($Subcategory);
	}
	else {
		push @Response, "unavailable";
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnRPMPatches ($)
{	my $Subcategory = shift;
	my @Response = ( );

	# first, let's see if we can determine when the OS was installed...
	# 
	# This is really subjective, but it looks like the time
	# stamp on the release RPM entry could be used...
	#
	# This does assume that the vendor RPM entry is of the form:
	# `ReturnVendorLinux`-release (e.g. redhat-release) with the exception
	# of the Conectiva distro which uses a Portuguese translation.
	# I do take out characters that are invalid in rpm names, and lc it.
	# 
	my $ReleaseRPM = ReturnVendorLinux($Subcategory);
	if ( $ReleaseRPM eq 'Conectiva' ) {
		# The package name changed among versions.
		$ReleaseRPM = `$RpmCommand -q --queryformat '%{NAME}' -f $ConectivaReleasePath`;
	} else {
		$ReleaseRPM .= '-release';
	}
	$ReleaseRPM =~ tr/-_a-zA-Z0-9//cd;
	$ReleaseRPM =~ tr/A-Z/a-z/;
	print "\tReleaseRPM = $ReleaseRPM\n" if $main::Debug;

	my $DateOSInstalled = `$RpmCommand -q --queryformat '%{INSTALLTIME}' $ReleaseRPM`;
	unless ( $DateOSInstalled > 0 ) {
		# 
		# XXX Do we really want to bail if we can't figure out
		#	when the OS was installed?  Why not just print
		#	out all of the RPMs?
		# 
		return("unavailable");
	}
	# pad this by an hour
	$DateOSInstalled += ( 60 * 60 );
	print "\tOS installed $DateOSInstalled seconds ago...\n" if $main::Debug;

	# get a list of all packages installed on the system
	my @PackageList = ReturnArrayExec("$RpmCommand -qa --queryformat '%{NAME}-%{VERSION}-%{RELEASE}\a%{SUMMARY}\a%{INSTALLTIME}\n'");
	foreach $Package (sort @PackageList) {

		print "\n\tPackage: $Package...\n" if $main::Debug;

		my ($PackageName, $PackageSummary, $PackageInstallTime)
			= split("\a", $Package);

		# XXX - what if the package was left over from a previous install?
		print "\tPackage time: $PackageInstallTime, OS time: $DateOSInstalled\n" if $main::Debug;
		next if ( $ListNewerRpmFiles and 
			( $PackageInstallTime < $DateOSInstalled ) );

		$PackageSummary = "desc=None" unless $PackageSummary;
		print "\tSummary is $PackageSummary\n" if $main::Debug;

		push @Response, "$PackageName desc=\"$PackageSummary\"";
	}

	# we'll do the ForceMultipleResponses in the calling function

	return @Response;
}


sub ReturnDPKGPatches ($)
{	my $Subcategory = shift;
	my @Response = ( );

	# get a list of all packages installed on the system
	# this should be expanded upon to not include packages
	# from the base dist...

	my @PackageList = ReturnArrayExec("$DpkgCommand --list | tail +6");
	foreach $Package (@PackageList) {

		print "\n\tPackage: $Package...\n" if $main::Debug;

		my ($PackageStatus, $PackageName, $PackageVersion, $PackageSummary)
			= $Package =~ /(..)  ([^ ]*) *([^ ]*) *(.*)$/;

		$PackageSummary = "None" unless $PackageSummary;
		print "\tSummary is $PackageSummary\n" if $main::Debug;

		push @Response, "$PackageName $PackageVersion desc=\"$PackageSummary\"";
	}

	# we'll do the ForceMultipleResponses in the calling function

	return @Response;
}


sub ReturnRunningLpschedLinux ($)
{	my $Subcategory = shift;
	my @Responses = ( );

# 	my $Response = ReturnRunningSysvLp($Subcategory, $PsCommand, $PsArgs);
# 	push @Responses, $Response if $Response =~ /Yes/;
	$Response = ReturnRunningBSDLpr($Subcategory, $PsCommand, $PsArgs);
	push @Responses, $Response if $Response =~ /Yes/;
	$Response = ReturnRunningLPRng($Subcategory);
	push @Responses, $Response if $Response =~ /Yes/;

	# concatenate the final responses as necessary
	my $FinalResponse = "";
	foreach $Response (@Responses) {
		if ( $FinalResponse eq "" ) {
			$FinalResponse = $Response;
		}
		else {
			$FinalResponse = "$FinalResponse, $Response";
		}
	}

	return ($FinalResponse);
}


# 
# lpc doesn't return enough information.  Let's see if 
# we can parse /etc/printcap...
# 

sub ReturnPrintersLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );

	# 
	# This function assumes a fairly "standard" format
	# for the printcap file like:
	#	# a comment
	#	lp|dj:\
	#		:sd=/var/spool/lpd/dj:\
	#		:mx#0:\
	#		:lp=/dev/lp0:\
	#		:sh:
	# 
	# One field per line.
	# 

	my @Printcap = ReturnArrayFile("$PrintcapPath");
	my $Name = "";
	my $System = "";
	my $Queue = "";
	foreach $Line (@Printcap) {
		next if $Line =~ /^#/;		# bypass comments

		# clean up the line
		chomp($Line);
		$Line =~ s/^[\s]+//;		# no leading spaces
		$Line =~ s/\\$//;		# trailing backslash

		next if ( $Line eq "" );	# bypass blank lines

		print "$Line\n" if $Debug;

		# printer line
		if ( $Line =~ /^\w+/ ) {
			# if anything left from previous printer
			# push it onto @Response
			if ( $Name ) {
				my $Location = FormatPrinterLocation($System, $Queue);
				my $PrinterLine = "$Name prints to $Location";
				push @Response, $PrinterLine;
				$Name = "";
				$System = "";
				$Queue = "";
			}

			# extract the name(s) of the printer(s)
			$Line =~ /^(.*):/;
			$Name = $1;
			$Name =~ s/\|/,/g;

			next;
		}

		# bypass fields we know we don't care about
		next if ( $Line =~ /^:[a-z][a-z]#/ );
		next if ( $Line =~ /^:[a-z][a-z]:/ );

		# fields
		if ( $Line =~ /^:/ ) {
			# extract the field label
			$Line =~ /^:([a-z][a-z])=(.*):/;
			my $Label = $1;
			if ( $Label eq "lp" ) {		# local printer
				$Queue = "local";
			}
			elsif ( $Label eq "rm" ) {	# remote server
				$System = $2;
			}
			elsif ( $Label eq "rp" ) {	# remote printer
				$Queue = $2;
			}

			next;
		}

		# ignore anything else...
	}
	if ( $Name ) {
		my $Location = FormatPrinterLocation($System, $Queue);
		my $PrinterLine = "$Name prints to $Location";
		push @Response, $PrinterLine;
	}
	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub FormatPrinterLocation ($$)
{	my $System = shift;
	my $Queue = shift;

	if ( $System ) {			# remote print queue
		return "queue $Queue on $System";
	}
	else {					# local print queue
		return "$Queue queue";
	}
}


# 
# Note that this subroutine has a side-effect of setting
# a global variable in main:: called EmailAvailable if
# sendmail is running...
# 

sub ReturnRunningSendmailLinux ($)
{	my $Subcategory = shift;

	my $Sendmail = ( ( -f "$SendmailCommand" ) ? "Yes" : "No" );

	# if sendmail is available, check to see if it is running
	my $SendmailRunning = 0;
	if ($Sendmail eq "Yes") {
		my @RunningProcs = ReturnArrayExec("$PsCommand $PsArgs");

		foreach $Line (@RunningProcs) {
			if ( $Line =~ /sendmail: accepting/ ) {
				print "\t$Line...\n" if $main::Debug;
				$SendmailRunning = 1;
				last;
			}
		}
	}
	if ( ! $SendmailRunning ) {
		$Sendmail = "No";
	}

	# what version of sendmail are we running?
	if ( ( $Sendmail eq 'Yes' ) and ( -r $SendmailConfPath ) ) {
		@SendmailConfig = ReturnArrayFile("$SendmailConfPath");
		foreach $Line (@SendmailConfig) {
			if ( $Line =~ /^DZ(\d+\.\d+\.\d+)/ ) {
				$Sendmail .= " (version $1)";
				last;
			}
		}
	}

	# side-effect
	$main::EmailAvailable = ( $Sendmail eq "Yes" );

	return ( $Sendmail );
}


sub ReturnRunningNFSLinux ($)
{	my $Subcategory = shift;

	my $NFSState = "";
	my $NFSServer = "";
	if ( -f $NfsdCommand and -f $ExportsPath ) {
		my @Exports = ReturnArrayFile("$ExportsPath");
		$NFSServer = grep(m:^/:, @Exports);
		# 
		# We can get here if nothing is exported
		# from this machine...  let's see if 
		# the server is running...
		# 
		if ( $NFSServer eq "" ) {
			print "\tNothing exported...\n" if $main::Debug;
			my @PsText = ReturnArrayExec("$PsCommand $PsArgs");
			print "\tSearching ps output for $NfsdCommandPattern...\n" if $main::Debug;
			foreach $Line (@PsText) {
				print "\t\t$Line\n" if $main::Debug;
				if ( $Line =~ /$NfsdCommandPattern/ ) {
					$NFSServer = "Yes";
					last;
				}
			}
		}
	}
	print "\tNFSServer is $NFSServer\n" if $main::Debug;
	my $NFSClient = ( -f $MountdCommand );
	print "\tNFSClient is $NFSClient\n" if $main::Debug;
	if ( $NFSServer and $NFSClient) {
		$NFSState = "Yes, both server and client";
	}
	elsif ( $NFSServer ) {
		$NFSState = "Yes, server only";
	}
	elsif ( $NFSClient ) {
		$NFSState = "Yes, client only";
	}
	else {
		$NFSState = "No";
	}

	return ( $NFSState );
}


sub ReturnRunningAutomountLinux ($)
{	my $Subcategory = shift;

	my $Automount = ( ( -f "$AmdCommand" ) ? "Yes" : "No" );

	return ( $Automount );
}


sub ReturnRunningNISLinux ($)
{	my $Subcategory = shift;

	# 
	# XXX - NIS was not installed on the system I was porting this on...
	# 

# 	my $NISState = "";
# 	my $NISmaster = SearchFileForTagValue($NameserverConfPath, $NISmasterTag);
# 	my $NISslave = SearchFileForTagValue($NameserverConfPath, $NISslaveTag);
# 	my $NISclient = SearchFileForTagValue($NameserverConfPath, $NISclientTag);
# 	if ( $NISmaster or $NISslave ) {
# 		$NISmaster = 1;
# 	}
# 	if ( $NISmaster and $NISclient ) {
# 		$NISState = "Yes, both server and client";
# 	}
# 	elsif ( $NISmaster ) {
# 		$NISState = "Yes, server only";
# 	}
# 	elsif ( $NISclient ) {
# 		$NISState = "Yes, client only";
# 	}
# 	else {
# 		$NISState = "No";
# 	}

	my $NISState = "No";				# XXX Fix this

	# XXX add NIS domainname to $NisState if NIS is running...

	return ( $NISState );
}


sub ReturnRunningNameserverLinux ($)
{	my $Subcategory = shift;
	my $NameserverString = "No";
	my @NameserverConfArray = ();
	my $Line = "";
	my $Zone = "";
	my $Type = "";
	my %ZoneHash;

	my @RunningProcs = ReturnArrayExec("$PsCommand $PsArgs");
	if (grep {m,$NameServerPattern,} @RunningProcs) {

		print("Found nameserver\n") if $main::Debug;

		$NameserverString = "Yes";
		# Now try to get some more info from nameserver conf file
		if ( -r "$NameserverConfPath" ) {

			print("Found $NameserverConfPath\n") if $main::Debug;

			@NameserverConfArray = ReturnArrayFile($NameserverConfPath);
			foreach $Line ( @NameserverConfArray ) {

				# named.conf stores relevent data like this
				#
				# //
				# // a caching only nameserver config
				# //
				# zone "." {
				#         type hint;
				#         file "named.ca";
				# };
				#

				next if $Line =~ /^\/\//;  # skip comment Lines

				# Look for zone and type info
				if ( $Line =~ /zone\s+\"(.+)\"/ ) {
					$Zone = $1;
				} elsif ( $Line =~ /type\s+(\w+)/ ) {
					$Type = $1;
				}

				# skip reverse lookup and first undefined entry
				if ( ($Zone =~ /in-addr\.arpa/)  || ($Zone eq "") ) {
					next;
				}

				# if zone is ., it's going to be a caching server?
				if ( $Zone eq "." ) {
					$Zone = "n/a";
				}

				# if type is hint, it's a caching server only
				if ( $Type eq "hint" ) {
					$Type = "Caching";
				}

				$ZoneHash{"$Zone"} = $Type;

			}

			# Append hash data to return string
			foreach $key ( keys %ZoneHash ) {
				$NameserverString .= ", Domain=$key, Type=$ZoneHash{$key}";
			}

		}
	}

	return ( $NameserverString );
}


sub ReturnIPAddressResolutionLinux ($)
{	my $Subcategory = shift;
	my $IPAddressResolution = "Unknown";

	if ( -f $ResolvConfPath ) {
		$IPAddressResolution = "DNS";
	}
	else {
		$IPAddressResolution = "/etc/hosts";
	}

	return ( $IPAddressResolution );
}


sub ReturnRunningSambaLinux ($)
{	my $Subcategory = shift;

	return ReturnRunningSamba($Subcategory, $PsCommand, $PsArgs, $InetdConfPath);
}


sub ReturnGraphicalUserInterfaceLinux ($)
{	my $Subcategory = shift;

	# XXX Let's defer this for now

	return ( "X11R6" );
}


sub ReturnFontServerLinux ($)
{	my $Subcategory = shift;

	# XXX Let's defer this for now

	return ( "No" );
}


sub ReturnLocalFileSystemsLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );

	my @DfText = ReturnArrayExec("$DfCommand $LocalFileSystems");
	my $PreviousLine = "";
	foreach $Line (@DfText) {
		# bypass the header line
		next if $Line =~ /^Filesystem/;
		# partial line?
		if ( $PreviousLine ) {
			$Line = $PreviousLine . $Line;
			$PreviousLine = "";
		}
		# clean and split line
		$Line =~ s/[\s]+/ /g;
		my @DfFields = split(/ /, $Line);
		# partial line?
		if ( scalar(@DfFields) < 2 ) {
			$PreviousLine = $Line;
			next;
		}
		# bypass any cdrom drives or remote file systems
		next if $Line =~ /cdrom/;
		next if $Line =~ /^[\w]/;
		# calc size in MB
		my $Size = sprintf("%.1fMB", ( $DfFields[1] / 1024 ) );
		push @Response, "$DfFields[5] mount=$DfFields[0] size=$Size sat=$DfFields[4]";
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnRemoteFileSystemsLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );

	my @DfText = ReturnArrayExec("$DfCommand $NFSFileSystems");
	my $PreviousLine = "";
	foreach $Line (@DfText) {
		# bypass the header line
		next if $Line =~ /^Filesystem/;
		# partial line?
		if ( $PreviousLine ) {
			$Line = $PreviousLine . $Line;
			$PreviousLine = "";
		}
		# clean and split line
		$Line =~ s/[\s]+/ /g;
		my @DfFields = split(/ /, $Line);
		# partial line?
		if ( scalar(@DfFields) < 2 ) {
			$PreviousLine = $Line;
			next;
		}
		# bypass any cdrom drives or remote file systems
		next if $Line =~ /cdrom/;
		next if $Line =~ /^\//;
		print "\t$Line\n" if $main::Debug;
		# calc size in MB
		my $Size = sprintf("%.1fMB", ( $DfFields[1] / 1024 ) );
		my ( $RemoteSystem, $RemoteFileSystem ) = split(':', $DfFields[0]);
		push @Response, "$DfFields[5] remote=$RemoteSystem mount=$RemoteFileSystem size=$Size sat=$DfFields[4]";
	}

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnExportedFileSystemsLinux ($)
{	my $Subcategory = shift;
	my @Response = ( );

	# XXX - this is a stub routine to be filled in later...

	@Response = ForceMultipleResponses(\@Response);

	return @Response;
}


sub ReturnRunningAccountingLinux ($)
{	my $Subcategory = shift;

	# 
	# XXX - Accounting was not running on the system I was porting this on...
	# 

	# XXX Let's defer this for now

	return ( "Unknown" );
}


###########################################################################
# End of Linux.pm
###########################################################################

1;
