#! /usr/bin/env perl
##**************************************************************
##
## Copyright (C) 1990-2007, Condor Team, Computer Sciences Department,
## University of Wisconsin-Madison, WI.
## 
## Licensed under the Apache License, Version 2.0 (the "License"); you
## may not use this file except in compliance with the License.  You may
## obtain a copy of the License at
## 
##    http://www.apache.org/licenses/LICENSE-2.0
## 
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
##
##**************************************************************

use strict;
use warnings;

use File::Temp qw(tempdir);

# Update the module include path
BEGIN
{
	my $Dir = $0;
	if ( $Dir =~ /(.*)\/.*/ )
	{
		push @INC, "$1";
	}
}
use CondorConfig;
$| = 1;

# Prototypes
sub main( );
sub CommandLine( @ );
sub FindConfigFile( );
sub ReadConfigFiles( );
sub InstallModules( );
sub ImportOption( $$ );
sub ReadDescriptionFile( $$ );
sub InstallModule( $ );
sub ConfigureModule( $ );
sub WriteConfig( );
sub Usage ( $ );
sub Help ( );


# ******************************************************
# Command line options
# ******************************************************
my $Distribution = "hawkeye";
my %Options =
    (
     "[--modules=dir]"	=> "Specify modules directory",
     "[--config=file]"	=> "Specify $Distribution config file",
     "[--no-update|-n]"	=> "Don't update the config file",
     "[--update=file]"	=> "Specify config file to update",
     "[--update|-u]"	=> "Enable config update <default>",
     "[--manual|-m]"	=> "Manual mode; asks lots of questions",
     "[--defaults]"	=> "Use defaults if possible",
     "[--all|-a]"	=> "Install all modules found in packages",
     "[--ask]"		=> "Ask before installing each module",
     "File [File..]"	=> "List of module package files to install",
     "[--help|-h]"	=> "Dump help",
    );

# Hash of directories & files
my %Config = (
	      Modules => [],
	      RootDir => "",
	      ConfigFile => {
			     Base => "",
			     Updated => "",
			    },
	      DoDoUpdateConfig => 0,
	      ModulesDir => "",
	      UseDefaults => 1,
	      AskAll => 0,
	      InstallAll => 1,
	      Startd => -1,
	      CronName => "",

	      );

# Valid setup info
my %ConfigParms =
(
 # Valid "Options"
 Options => {
	     continuous => { Mode => 1, Reconfig => 1, },
	     kill => { Kill => 1, },
	     nokill => { Kill => 0, },
	     reconfig => { Reconfig => 1, },
	     waitforexit => { Mode => 1, },
	    },

 # Modes
 Modes => { periodic => 0, waitforexit => 1, },
 Reconfig => { true => 1, t => 1, false => 0, f => 0, },
 Kill => { true => 1, t => 1, false => 0, f => 0, },
 ModeStrings => [ qw(Periodic WaitForExit) ],
);

# Condor config stuff
my $ConfigMacros;
my $ConfigFiles;

my $Program;
my $ConfigEnv;

main( );

# Main
sub main( )
{
    # "Main" Logic
    if ( ! exists $ENV{PWD} )
    {
	$ENV{PWD} = `/bin/pwd`; chomp $ENV{PWD};
    }

    # Look at $0, see what we can gleen from it...
    if ( $0 =~ /(condor|hawkeye)_install/ )
    {
	$Distribution = $1;
    }
    elsif ( exists $ENV{CONDOR_DISTRIBUTION} )
    {
	$Distribution = $1;
    }
    $Program = $Distribution . "_install_module";
    $ConfigEnv = uc $Distribution . "_CONFIG";

    # Process command line, etc..
    CommandLine( @ARGV );
    $#ARGV = -1;
    $| = 1;

    # Create the config macro & file handlers
    $ConfigMacros = CondorConfigMacros->new( $Distribution );

    # Construct the config file object
    $ConfigFiles = CondorConfigFiles->new( $ConfigMacros );

    # Find the config file to use
    FindConfigFile( );

    # Read the config file
    ReadConfigFiles( );

    # Copy in the perl modules
    InstallModules( );

} # end of "main"
# ******************************************************

# ******************************************************
# Parse the command line, etc.
# ******************************************************
sub CommandLine( @ )
{
    foreach my $Arg ( @_ )
    {
	# Speicfy modules directory
	if ( $Arg =~ /^--modules=(.+)/ )
	{
	    die "Directory '$1' does not exist" if ( ! -d $1 );
	    $Config{ModulesDir} = $1;
	}

	# Speicify config file
	elsif ( $Arg =~ /--config=(.+)/ )
	{
	    die "Config file '$1' does not exist" if ( ! -f $1 );
	    my $File = $1;
	    if ( ! $File =~ /^\// )
	    {
		$File = $ENV{PWD} . "/$File";
	    }
	    $Config{ConfigFile}{Base} = $File;
	}

	# Config file to update
	elsif ( $Arg =~ /^--update=(.+)/ )
	{
	    $Config{ConfigFile}{Updated} = $1;
	    $Config{DoUpdateConfig} = 1;
	}

	# Config update
	elsif ( ( $Arg eq "-u") || ( $Arg eq "--update" ) )
	{
	    $Config{DoUpdateConfig} = 1;
	}

	# Don't update config
	elsif ( ( $Arg eq "-n") || ( $Arg eq "--no-update" ) )
	{
	    $Config{DoUpdateConfig} = 0;
	}

	# "Install all modules" mode
	elsif ( ( $Arg eq "-a" ) || ( $Arg eq "--all" ) )
	{
	    $Config{InstallAll} = 1;
	}

	# "Ask" mode
	elsif ( $Arg eq "--ask" )
	{
	    $Config{InstallAll} = 0;
	}

	# Manual mode
	elsif ( ( $Arg eq "-m" ) || ( $Arg eq "--manual" ) )
	{
	    $Config{InstallAll} = 0;
	    $Config{UseDefaults} = 0;
	    $Config{AskAll} = 1;
	}

	# Hawkeye mode
	elsif ( $Arg eq "--hawkeye" )
	{
	    $Distribution = "hawkeye";
	}

	# Condor mode
	elsif ( $Arg eq "--condor" )
	{
	    $Distribution = "condor";
	}

	# Specify the modules directory...
	elsif ( $Arg =~ "--modules=(.+)" )
	{
	    $Config{ModulesDir} = $1;
	}

	# Help
	elsif ( ( $Arg =~ /^-h/ ) || ( $Arg eq "--help" ) )
	{
	    Help( );
	    exit 0;
	}

	# Unknown option
	elsif ( $Arg =~ /^-/ )
	{
	    Usage( $Arg );
	    exit( 1 );
	}

	# Must be a file
	else
	{
	    if ( ! -f $Arg )
	    {
		print STDERR "Warning: Can't find $Arg\n";
		next;
	    }

	    # Make sure it's absolute
	    if ( $Arg =~ /^\// )
	    {
		push( @{$Config{Modules}}, $Arg );
	    }
	    else
	    {
		push( @{$Config{Modules}}, $ENV{PWD} . "/$Arg" );
	    }
	}
    }

    # Any modules to install?
    if ( $#{@{$Config{Modules}}} < 0 )
    {
	print STDERR "No modules specified\n";
	Usage( "" );
	exit 1;
    }

} # CommandLine( )
# ******************************************************

# ******************************************************
# Find the configuration file to use
# ******************************************************
sub FindConfigFile( )
{
    my $Base = $ConfigFiles->FindConfig( $ConfigEnv,
					 $Config{ConfigFile}{Base},
					 $Distribution );

    # Finally, let's go check the config
    if ( ! $Base )
    {
	print STDERR "No config found\n";
	Usage( "" );
	print STDERR "\tOr, set the $ConfigEnv env variable\n";
	exit 1;
    }

    # Store off the setting
    $Config{ConfigFile}{Base} = $Base;

    return 1;

} # FindConfigFile()
# ******************************************************

# ******************************************************
# Read the config file
# ******************************************************
sub ReadConfigFiles( )
{

    # Read them all in
    $ConfigFiles->ReadAll( undef ) or die "Failed reading config files";

    # Add basic things to the config if they're not defined...
    $Config{CronName} = $ConfigMacros->Expand( "STARTD_CRON_NAME" );
    if ( ! defined $Config{CronName} )
    {
	$Config{CronName} =
	    ( $Distribution eq "hawkeye" ) ? "Hawkeye" : "Cron";
	$ConfigFiles->AddText( "STARTD_CRON_NAME = $Config{CronName}" );
	$ConfigMacros->Set( "STARTD_CRON_NAME", $Config{CronName} );
    }

    # Find the modules directory location
    my $Modules = $ConfigMacros->Get( "MODULES" );
    if ( ! $Modules )
    {
	my $Hawkeye = $ConfigMacros->Expand( "HAWKEYE" );
	my $HawkeyeDir = $ConfigMacros->Expand( "HAWKEYE_DIR" );
	my $ReleaseDir = $ConfigMacros->Expand( "RELEASE_DIR" );

	# 1. Look for direct config via cmd line
	if ( $Config{ModulesDir} ne "" )
	{
	    $Modules = $Config{ModulesDir};
	}
	# 2. Look for HAWKEYE_DIR in config
	elsif ( ( defined $HawkeyeDir ) && ( -d $HawkeyeDir ) )
	{
	    $Modules = "\$(HAWKEYE_DIR)/modules";
	}
	# 3. Look for HAWKEYE in config
	elsif ( ( defined $Hawkeye ) && ( -d $Hawkeye ) )
	{
	    $Modules = "\$(HAWKEYE)/modules";
	}
	# 4. Look for RELEASE_DIR in config
	elsif ( defined $ReleaseDir )
	{
	    $Modules = "\$(RELEASE_DIR)/modules";
	}
	else
	{
	    print STDERR "No MODULES, HAWKEYE or RELEASE_DIR setting" .
		"found in $Config{ConfigFile}{Base}\n";
	    print STDERR "Specify --modules=<dir> to correct this\n";
	    exit 1;
	}

	# Configure the new values
	$ConfigFiles->AddText( "MODULES = $Modules" );
	$ConfigMacros->Set( "MODULES", $Modules );
    }

    # Now, expand it and set our internal config
    if ( $Config{ModulesDir} eq "" )
    {
	# Bomb if the modules directory doesn't exist
	my $Dir = $ConfigMacros->Expand( "MODULES" );
	if ( ! -d $Dir )
	{
	    print STDERR "Modules directory '$Modules' does not exist!\n";
	    exit 1;
	}

	# Store the expanded version for internal use
	$Config{ModulesDir} = $Dir if ( $Config{ModulesDir} eq "" );
    }

    # Give up...
    die "Unable to find modules directory\n"
	if ( $Config{ModulesDir} eq "" );
    print "Found modules directory '$Config{ModulesDir}'\n";

    # Job list defined?
    my $CronJobListVar = uc( $Config{CronName} . "_JOBLIST" );
    my $JobList = $ConfigMacros->Expand( $CronJobListVar );
    if ( ! defined $JobList )
    {
	$ConfigFiles->AddText( "$CronJobListVar = " );
	$ConfigMacros->Set( "$CronJobListVar", "" );
    }
    $Config{CronJobListVar} = $CronJobListVar;

    # Try to find the file that last definied the job list
    if ( $Config{ConfigFile}{Updated} eq "" )
    {
	my $File = $ConfigMacros->Expand( "INSTALL_MODULE_CONFIG_FILE" );
	$Config{ConfigFile}{Updated} = $File if ( $File );
    }

    # Try to find the file that last definied the job list
    if ( $Config{ConfigFile}{Updated} eq "" )
    {
	my $Info = $ConfigMacros->GetInfo( $CronJobListVar );
	if ( ( $Info ) && ( $Info->{File} ) )
	{
	    $Config{ConfigFile}{Updated} = $Info->{File};
	    print "Found '$CronJobListVar' in "
		. $Info->{File} . " line " . $Info->{Line} . "\n";
	}
    }

    # Try to find the file that last definied the old job list
    if ( $Config{ConfigFile}{Updated} eq "" )
    {
	my $Info = $ConfigMacros->GetInfo( $CronJobListVar );
	if ( ( $Info ) && ( $Info->{File} ) )
	{
	    $Config{ConfigFile}{Updated} = $Info->{File};
	    print "Found '$CronJobListVar' in "
		. $Info->{File} . " line " . $Info->{Line} . "\n";
	}
    }

    # Try to find the file that last defined STARTD_CRON_NAME
    if ( $Config{ConfigFile}{Updated} eq "" )
    {
	my $Info = $ConfigMacros->GetInfo( "STARTD_CRON_NAME" );
	if ( ( $Info ) && ( $Info->{File} ) )
	{
	    $Config{ConfigFile}{Updated} = $Info->{File};
	    print "Found 'STARTD_CRON_NAME' in "
		. $Info->{File} . " line " . $Info->{Line} . "\n";
	}
    }

    # If there's a local config that's named ".jobs" or similar, use it as
    # the config to modify
    if ( $Config{ConfigFile}{Updated} eq "" )
    {
	my $Pat = "(jobs|joblist|hawkeye)\$";
	my @List = $ConfigFiles->GrepNames( $Pat );
	if ( scalar @List )
	{
	    $Config{ConfigFile}{Updated} = shift( @List );
	}
    }

    # If no config specified, use the base
    $Config{ConfigFile}{Updated} = $Config{ConfigFile}{Base}
	if ( $Config{ConfigFile}{Updated} eq "" );
    print "Config Updates will be written to $Config{ConfigFile}{Updated}\n";
    $ConfigFiles->SetUpdateFile( $Config{ConfigFile}{Updated} );


} # ReadConfigFiles
# ******************************************************

# ******************************************************
# Configure & Install all Modules
# ******************************************************
sub InstallModules ( )
{
    my $TmpDir = File::Temp::tempdir( );
    die "tempdir() failed" if ( ! defined $TmpDir );
    print "Temp dir = $TmpDir\n";
    my $OrigDir = $ENV{PWD};
    chdir $TmpDir || die "Can't chdir to $TmpDir";

    # Summary info
    my @Ok;
    my @Failed;
    my $ConfigUpdated = $ConfigFiles->NewTextLines( );

    # Walk through the list of modules...
    print "Installing modules\n";
    foreach my $Module ( @{$Config{Modules}} )
    {
	die "$Module does not exist" if ( ! -f $Module );
	my $Cmd = "gzip -d < $Module | tar xvfp -";
	if ( system "$Cmd 2>&1 > /dev/null" )
	{
	    warn "Error extracting '$Module'\n";
	    next;
	}

	opendir( DIR, $TmpDir ) || die "Can't read dir $TmpDir";
	my @Files = grep { /^[^\.]/ } readdir( DIR );
	closedir( DIR );

	# Find the description ".hawk" file...
	my @DatFiles = grep { /\.hawk$/ } @Files;
	if ( $#DatFiles < 0 )
	{
	    print STDERR
		"$Module does not appear to be a valid module file\n";
	    system "rm *";
	    next;
	}

	# Loop through the list of modules to install...
	foreach my $Description ( @DatFiles )
	{
	    my %ModuleData;

	    # Read the description file for this module..
	    my $Errors = ReadDescriptionFile( $Description,
					      \%ModuleData );
	    if ( $Errors )
	    {
		print "Errors encountered in description " .
		  "file; skipping $Module\n";
		push @Failed, $Module;
		next;
	    }

	    # Get it's name..
	    my $Name = $ModuleData{Name};
	    print "\n\nModule $Name:\n";

	    # Install it?
	    my $Status = InstallModule( \%ModuleData );
	    if ( $Status )
	    {
		push @Failed, $Module if ( $Status < 0 );
		next;
	    }

	    # Enable it?
	    if ( ! $Config{InstallAll} )
	    {
		print "  Do you wish to enable this module? ";
		my $Answer = "";
		while( <> )
		{
		    chomp;
		    if ( /^[yn]/i )
		    {
			$Answer = lc($_);
			last;
		    }
		    print "  Please answer 'y' or 'n': ";
		}
		next if ( $Answer ne "y" );
	    }

	    # Configure it?
	    if ( ! ConfigureModule( \%ModuleData ) )
	    {
		push @Failed, $Module if ( $Status < 0 );
		next;
	    }

	    # Update the config file
	    $Status = DoUpdateConfig( \%ModuleData );

	    # Add to the failed list?
	    if ( $Status < 0 )
	    {
		push @Failed, $Module
	    }
	    elsif ( $Status == 1 )
	    {
		push @Ok, $Name;
		$ConfigUpdated += $ModuleData{ConfigChanged};
	    }
	}

	# Clean up
	system( "rm *" );

    }

    # Create the new config file
    if ( $ConfigUpdated )
    {
	WriteConfig( );
    }
    else
    {
	print "No configuration updates required\n";
    }

    # Final clean-up
    chdir $OrigDir;
    system "rm -fr $TmpDir";

    # Summary
    if ( $#Ok >= 0 )
    {
	print "Fully installed modules:\n";
	foreach my $Tmp ( @Ok )
	{
	    print "   $Tmp\n";
	}
    }
    if ( $#Failed >= 0 )
    {
	print "Failed modules:\n";
	foreach my $Tmp ( @Failed )
	{
	    print "   $Tmp\n";
	}
    }

} # InstallModules()
# ******************************************************

# ******************************************************
# Read the module description file
# ******************************************************
sub ReadDescriptionFile( $$ )
{
    my $DescriptionFile = shift;
    my $Data = shift;

    if ( ! open( DESCRIPTION, $DescriptionFile ) )
    {
	print "Can't read description file '$DescriptionFile'; skipping\n";
	return -1;
    }

    # List of keywords..
    my @KeyWords =
	qw ( modulefiles default options parameter period version
	     mode reconfig kill );
    my $KeyWordsRe =
	"^(" . join( ":)|(", @KeyWords ) . ":)";


    # Default mode, etc.
    $Data->{Mode} = 0;
    $Data->{Kill} = 1;
    $Data->{Reconfig} = 0;

    # Read the description file
    my $Errors = 0;
  READLINE:
    while( <DESCRIPTION> )
    {
	chomp;
	my $Orig = $_;
	s/\#.*//;
	s/^\s+//;
	s/\s+$//;
	next if ( $_ eq "" );

	# Parse the line..
	my $Attribute = "";
	my $Value = "";
	if ( /(\w+):(\s*)(.*)/ )
	{
	    $Attribute = uc $1;
	    $Value = $3 if ( defined $3 );
	}

	# Module files
	if ( $Attribute eq "MODULEFILES" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR
		    "$DescriptionFile line $.: No modules files specfied\n";
		$Errors++;
		last;
	    }
	    my @Files = split( /\s+/, $Value );
	    foreach my $File ( @Files )
	    {
		if ( ! -f $File )
		{
		    print STDERR
			"Can't find '$File' specified in module line $.\n";
		    $Errors++;
		    last;
		}
	    }

	    # Store it all away...
	    $Data->{ModuleFile} = shift @Files
		if ( ! exists $Data->{ModuleFiles} );
	    push( @{$Data->{OtherFiles}}, \@Files )
		if ( $#Files >= 0 );
	    $#Files = -1;
	}

	# Description
	elsif ( $Attribute eq "DESCRIPTION" )
	{

	    # Read the module description
	    my @Description;
	    while( <DESCRIPTION> )
	    {
		chomp;
		s/\s+$//;
		my $Key = $_; $Key =~ s/\s.*$//;
		if ( ( $_ eq "" ) || ( /^\#/ ) || ( $Key =~ /$KeyWordsRe/i ) )
		{
		    #print "Key '$Key' matches\n";
		    $Data->{Description} = \@Description;
		    redo READLINE;
		}
		push @Description, $_;
	    }
	}

	# Default name
	elsif ( $Attribute eq "DEFAULT" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR
		    "$DescriptionFile line $.: No default name specfied\n";
		$Errors++;
		last;
	    }
	    $Data->{DefaultName} = $Value;
	}

	# Prefix
	elsif ( $Attribute eq "PREFIX" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR "$DescriptionFile line $.: No prefix specfied\n";
		$Errors++;
		last;
	    }
	    $Data->{Prefix} = $Value;
	}

	# Version (stored, but not used)
	elsif ( $Attribute eq "VERSION" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR
		    "$DescriptionFile line $.: No version specfied\n";
		$Errors++;
		last;
	    }
	    $Data->{Version} = $Value;
	}

	# Period
	elsif ( $Attribute eq "PERIOD" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR "$DescriptionFile line $.: No period specfied\n";
		$Errors++;
		last;
	    }
	    if ( $Value =~ /((\d+)[sSmMhHdD]?)/ )
	    {
		if ( $2 <= 0 )
		{
		    print STDERR "Period must be > 0\n";
		    $Errors++;
		}
		else
		{
		    $Data->{Period} = $1;
		}
	    }
	    else
	    {
		print STDERR
		    "$DescriptionFile line $.: Invalid period specfied\n";
		$Errors++;
		last;
	    }
	}

	# Options
	elsif ( $Attribute eq "OPTIONS" )
	{
	    foreach my $Opt ( split( /\s+/, $Value ) )
	    {
		if ( ! ImportOption( $Opt, $Data ) )
		{
		    print STDERR "Line $.: Unknown option '$Opt'\n";
		    $Errors++;
		}
	    }
	}

	# Mode
	elsif ( $Attribute eq "MODE" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR
		    "$DescriptionFile line $.: No mode specfied\n";
		$Errors++;
		last;
	    }
	    my $Tmp = lc $Value;
	    if ( ! exists $ConfigParms{Modes}{$Tmp} )
	    {
		print STDERR
		    "$DescriptionFile line $.: " .
			"Invalid mode specfied '$Value'\n";
		$Errors++;
		last;
	    }
	    $Data->{Mode} = $ConfigParms{Modes}{$Tmp};
	}

	# Reconfig yes/no
	elsif ( $Attribute eq "RECONFIG" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR
		    "$DescriptionFile line $.: No reconfig specfied\n";
		$Errors++;
		last;
	    }
	    my $Tmp = lc $Value;
	    if ( ! exists $ConfigParms{Reconfig}{$Tmp} )
	    {
		print STDERR
		    "$DescriptionFile line $.: " .
			"Invalid reconfig specfied '$Value'\n";
		$Errors++;
		last;
	    }
	    $Data->{Reconfig} = $ConfigParms{Reconfig}{$Tmp};
	}

	# Kill yes/no
	elsif ( $Attribute eq "KILL" )
	{
	    if ( $Value eq "" )
	    {
		print STDERR
		    "$DescriptionFile line $.: No kill specfied\n";
		$Errors++;
		last;
	    }
	    my $Tmp = lc $Value;
	    if ( ! exists $ConfigParms{Kill}{$Tmp} )
	    {
		print STDERR
		    "$DescriptionFile line $.: " .
			"Invalid kill specfied '$Value'\n";
		$Errors++;
		last;
	    }
	    $Data->{Kill} = $ConfigParms{Kill}{$Tmp};
	}

	# Parameter
	elsif ( $Attribute eq "PARAMETER" )
	{
	    my $ParamName = "";
	    my $Default = "";
	    if ( $Value =~ /(\w+)(\s+=\s*(.*))?/ )
	    {
		$ParamName = $1;
		$Default = $3 if ( defined $3 );
	    }
	    else
	    {
		print STDERR
		    "$DescriptionFile line $.: No parameter name specified\n";
		$Errors++;
		last;
	    }
	    my $p = ();
	    my $OrigName = $ParamName;
	    $p->{Default} = $Default;

	    # For backward compatibility, mesage the old
	    # HAWKEYE_<NAME>_<PARAM>
	    # parameter names to just plain <PARAM>
	    my $Pat1 = "hawkeye_" . $Data->{DefaultName} . "_(\\w+)";
	    my $Pat2 = "hawkeye_[a-zA-Z]+_(\\w+)";
	    if ( $ParamName =~ /^$Pat1$/i )
	    {
		$ParamName = $1;
	    }
	    elsif ( $ParamName =~ /^$Pat2/i )
	    {
		$ParamName = $1;
	    }
	    $p->{Name} = $ParamName;
	    $p->{OrigName} = $OrigName;
	    $p->{Required} = 0;
	    my @Description;
	    while( <DESCRIPTION> )
	    {
		chomp;
		s/\s+$//;

		# Is it a keyword (ie the start of something else)?
		my $Key = $_; $Key =~ s/\s.*$//;
		if ( ( $_ eq "" ) || ( /^\#/ ) || ( $Key =~ /$KeyWordsRe/i ) )
		{
		    $p->{Description} = \@Description;
		    push ( @{$Data->{Parameters}}, $p );
		    $p = ();
		    redo READLINE;
		}

		# Parse flags, if provided
		elsif ( /^flags:\s*(.*)/i )
		{
		    foreach my $Flag ( split( /[\s+\,]+/, lc $1 ) )
		    {
			if ( $Flag eq "required" )
			{
			    $p->{Required} = 1;
			}
			else
			{
			    print STDERR "Line $.: Unknown flag '$Flag'\n";
			    $Errors++;
			}
		    }
		    next;
		}

		# Must be a description line; add it
		else
		{
		    s/$OrigName/$ParamName/ if ( $OrigName ne $ParamName );
		    push @Description, $_;
		}
	    }

	    # Store it
	    $p->{Description} = \@Description;
	    push (@{$Data->{Parameters}}, $p );
	    $p = ();

	    # Done
	    last;
	}

	# Unknown
	else
	{
	    print "Can't parse module description file".
		" '$DescriptionFile' line $.\n";
	    print "$Orig\n";
	    $Errors++;
	    last;
	}
    }
    close( DESCRIPTION );

    print "Data Dump " . __LINE__ . "\n";
    foreach my $k ( keys %{$Data} )
    {
	print "$k = $Data->{$k}\n";
    }

    # For backward compatibility, message the old HAWKEYE_<NAME>_<PARAM>
    # parameter names to just plain <PARAM>
    # Clean up the description, too
    for my $Param ( @{$Data->{Parameters}} )
    {
	next if ( $Param->{Name} eq $Param->{OrigName} );
	foreach my $LineNo ( 0 .. $#{$Data->{Description}} )
	{
	    $Data->{Description}[$LineNo] =~
		s/$Param->{OrigName}/$Param->{Name}/;
	}
    }

    # Final checks
    if ( ! exists $Data->{ModuleFile} )
    {
	print STDERR "No module file specified\n";
	$Errors++;
    }
    if ( ! exists $Data->{Period} )
    {
	print STDERR "No period specified\n";
	$Errors++;
    }

    # Finally, fill in some defaults...
    my $Name = $Data->{ModuleFile};
    $Name = $Data->{DefaultName} if ( exists $Data->{DefaultName} );
    $Data->{Name} = $Name;

    # Prefix
    $Data->{Prefix} = $Name . "_" if ( ! exists $Data->{Prefix} );

    return $Errors;

} # ReadDescriptionFile()
# ******************************************************

# ******************************************************
# Install the module
# ******************************************************
sub InstallModule( $ )
{
    my $Data = shift;
    my $Name = $Data->{Name};

    # Should we install this?
    foreach my $Text ( @{$Data->{Description}} )
    {
	print "\t$Text\n";
    }
    # Only ask if the InstallAll option is off
    if ( ! $Config{InstallAll} )
    {
	print "  Do you wish to install this module? ";
	my $Answer = "";
	while( <> )
	{
	    chomp;
	    if ( /^[yn]/i )
	    {
		$Answer = lc($_);
		last;
	    }
	    print "  Please answer 'y' or 'n': ";
	}
	return 1 if ( $Answer ne "y" );
    }

    # Install the files..
    $| = 1;
    foreach my $File ( $Data->{ModuleFile}, @{$Data->{OtherFiles}} )
    {
	print "  File = '$File'\n";
	my $Cmd = "cp $File $Config{ModulesDir}";
	print "\t$File -> $Config{ModulesDir}...";
	system $Cmd;
	print "\n";
    }

    # Check to see if it's already configured
    my $Found = 0;
    my $JobString = $ConfigMacros->Expand( $Config{CronJobListVar} );
    foreach my $Job ( split( /\s+/, $JobString ) )
    {
	my @Fields = split( /:/, $Job );
	if ( scalar @Fields >= 4 )
	{
	    if ( $Fields[0] eq $Name )
	    {
		$Found++;
		$Data->{Current}{Name} = shift @Fields;
		$Data->{Current}{Prefix} = shift @Fields;
		$Data->{Current}{Path} = shift @Fields;
		$Data->{Current}{Period} = shift @Fields;

		foreach my $Opt ( @Fields )
		{
		    if ( ! ImportOption( $Opt, \$Data->{Current} ) )
		    {
			print STDERR "Line $.: Unknown option '$Opt'\n";
		    }
		}
		last;
	    }
	}
    }
    $Data->{Configured} = $Found;

    # Done
    return 0;

} # InstallModule( )
# ******************************************************

# ******************************************************
# Configure the module
# ******************************************************
sub ConfigureModule( $ )
{
    my $Data = shift;
    my $Name = $Data->{Name};
    my $NeedsConfig = $Config{AskAll};
    my $OldLineNum = -1;
    my $OldFileName = "";
    my $Errors = 0;

    # By default, nothing has changed.
    $Data->{ConfigChanged} = 0;

    # If we've already found this one, try to use the old values
    if ( $Data->{Configured} )
    {
	my $Pat = "(?i)^\\s*" . $Distribution . "_JOBS\\s*=.*\\s$Name:";
	my @Match = $ConfigFiles->Grep( $Pat, "i" );
	my $Match = shift( @Match );
	if ( $Match )
	{
	    $OldLineNum = $Match->{Line} + 1;
	    $OldFileName = $Match->{File}{File};
	}

	my $Import = 0;
	if ( $Config{NoAsk} )
	{
	    $Import = 1;
	}
	else
	{
	    print "  $OldFileName line $OldLineNum:\n";
	    print "  Previous configuration for $Name found;".
		" import it [Y/n]? ";
	    $_ = <>;
	    $Import = /^n/i ? 0 : 1;
	}

	# Ok, import from the old
	if ( $Import )
	{
	    print "  Importing settings for $Name ...";
	    $Data->{Prefix}   = $Data->{Current}{Prefix};
	    $Data->{Period}   = $Data->{Current}{Period};
	    $Data->{Mode}     = $Data->{Current}{Mode};
	    $Data->{Reconfig} = $Data->{Current}{Reconfig};
	    $Data->{Kill}     = $Data->{Current}{Kill};

	    # Import the options
	    $#{$Data->{Options}} = -1;
	    foreach my $Opt ( @{$Data->{Current}{Options}} )
	    {
		$Opt = lc( $Opt );
		if ( ! exists $Config{Options}{$Opt} )
		{
		    print STDERR "Error importing; unknown option '$Opt'\n";
		}
		else
		{
		    foreach my $Tmp ( @{$Config{Options}{$Opt}} )
		    {
			push( @{$Data->{Options}}, $Tmp );
			if ( $Tmp ne $Opt )
			{
			    $Data->{ConfigChanged}++;
			}
		    }
		}
	    }

	    # Update the defaults with the old values
	    foreach my $Param ( @{$Data->{Parameters}} )
	    {
		my $ParamName =
		    $Distribution . "_" . $Name . "_" . $Param->{Name};
		my $Current = $ConfigMacros->Expand( $ParamName );
		$Param->{Current} = $Current;
		$Param->{Default} = $Current
		    if ( defined $Current and $Current ne "" );
	    }
	    print "  Done\n";
	}
    }
    else
    {
	# Nothing to import; by definition modified!!
	$Data->{ConfigChanged}++;
    }

    # Give the user an update
    my @Required;
    print "  Logical name: $Name\n";
    print "  ClassAd prefix: " . $Data->{Prefix} . "\n";
    print "  Mode: " . $ConfigParms{ModeStrings}[$Data->{Mode}] . "\n";
    print "  Kill: " . ($Data->{Kill} ? "True" : "False") . "\n";
    print "  Reconfig: " . ($Data->{Reconfig} ? "True" : "False") . "\n";
    print "  Parameters:\n";
    foreach my $Param ( @{$Data->{Parameters}} )
    {
	$Param->{String} = "<NONE>";
	if ( exists $Param->{Default} and $Param->{Default} ne "" )
	{
	    $Param->{String} = $Param->{Default};
	}
	print "    " . $Param->{Name} . " = " . $Param->{String} . "\n";
	if ( $Param->{Required} )
	{
	    if ( !exists $Param->{Default} or $Param->{Default} eq "" )
	    {
		push( @Required, $Param->{Name} );
		$NeedsConfig++;
	    }
	}
    }

    # Fill in the default parameter values
    foreach my $Param ( @{$Data->{Parameters}} )
    {
	next if ( ! exists $Param->{Default} );
	if ( ( $Param->{Current} ) &&
	     ( $Param->{Current} ne $Param->{Default} ) )
	{
	    $Data->{ConfigChanged}++;
	}
	$Param->{Value} = $Param->{Default};
    }

    # Fill in the location of the original config
    $Data->{OriginalConfigLines} = $OldLineNum;
    $Data->{OriginalConfigFile} = $OldFileName;

    # Check the results
    if ( $Config{NoAsk} )
    {
	# Done
	if ( $Data->{ConfigChanged} && ( $OldLineNum >= 0 ) )
	{
	    print "  Check original configuration lines around line ".
		"$OldLineNum\n";
	}
	return 1;
    }

    # Are we done?
    if ( $NeedsConfig )
    {
	print "  $NeedsConfig parameters require configuration; forcing\n";
    }
    else
    {
	print "  Would you like to change these settings (N/y/q): ";
	$_ = <>;
	return 0 if ( $_ =~ /^q/i );
	$NeedsConfig = 1 if ( $_ =~ /^y/i );
    }

    # What name to give it?
    if ( $NeedsConfig )
    {
	print "  What logical name do you wish to use for this module".
	    " <default=$Name>: ";
	$_ = <>; chomp;
	if ( /\S/ )
	{
	    my $NewName = $_;
	    if ( $NewName ne $Name )
	    {
		$Data->{Name} = $Name = $NewName;
		$Data->{ConfigChanged}++;
	    }
	    print "  New name -> '$Name'\n";
	}
    }

    # Mode value
    if ( $NeedsConfig )
    {
	print "  Default run mode for $Name = ",
	    $ConfigParms{ModeStrings}[$Data->{Mode}] . "\n";

	# Get options from the user
	while( 1 )
	{
	    print "  What run mode should this module use (" .
		join( ", ", @{$ConfigParms{ModeStrings}} ) .
		    ") <enter = default> \n";
	    $_ = <>; chomp; s/^\s+//;
	    my $Mode = lc $_;
	    if ( $_ eq "" )
	    {
		# Do nothing
		last;
	    }
	    elsif ( exists $ConfigParms{Modes}{$Mode} )
	    {
		$Data->{Mode} = $ConfigParms{Modes}{$Mode};
		$Data->{ConfigChanged}++;
		last;
	    }
	    else
	    {
		print "Unknown mode '$_'\n";
	    }
	}
    }

    # Reconfig value -- don't allow user to change this
    if ( $NeedsConfig )
    {
	printf( "  Using default reconfig for $Name = %s\n",
		( $Data->{Reconfig} ? "True" : "False" ) );
    }

    # Kill value
    if ( $NeedsConfig )
    {
	printf( "  Default kill mode for $Name = %s\n",
		( $Data->{Kill} ? "True" : "False" ) );

	# Get options from the user
	while( 1 )
	{
	    print "  What kill mode should this module use (true/false)".
		" <enter = default>\n";
	    $_ = <>; chomp; s/^\s+//;
	    my $Kill = lc $_;
	    if ( $_ eq "" )
	    {
		# Do nothing
		last;
	    }
	    elsif ( $Kill eq "t" or $Kill eq "true" )
	    {
		$Data->{Kill} = 1;
		$Data->{ConfigChanged}++;
		last;
	    }
	    elsif ( $Kill eq "f" or $Kill eq "false" )
	    {
		$Data->{Kill} = 0;
		$Data->{ConfigChanged}++;
		last;
	    }
	    else
	    {
		print "Unknown kill '$_'\n";
	    }
	}
    }

    # Period
    if ( $NeedsConfig )
    {
	my $Default = "";
	my $DefString = "<no default>";
	if ( defined $Data->{Period} )
	{
	    $Default = $Data->{Period};
	    $DefString = "<enter = $Default>";
	}
	print "  Default period for $Name = $Default\n";
	while( 1 )
	{
	    print "  At what period should this module run " .
		"(s/m/h/d modifiers ok) $DefString? ";
	    $_ = <>; chomp;
	    last if ( ( $_ eq "" ) && ( $Default ne "" ) );
	    if ( /^(\d+[sSmMhHdD]?)$/)
	    {
		$Data->{Period} = $1;
		$Data->{ConfigChanged}++;
		last;
	    }
	    print "  '$_' is invalid.  Please enter a number ".
		"optionally followed by an s, an m, h, or a d\n";
	}
    }

    # Prefix
    if ( $Config{AskAll} )
    {
	print "  What prefix would you like for the ClassAd ".
	    "<enter = $Data->{Prefix}> ";
	$_ = <>; chomp;
	if ( $_ ne "" )
	{
	    $Data->{Prefix} = $_;
	    $Data->{ConfigChanged}++;
	}
    }

    # Parameter questions
    if ( $#{@{$Data->{Parameters}}} >= 0 )
    {
	if ( $NeedsConfig || scalar( @Required ) )
	{
	    print "  Parameters for module $Name:\n";
	    foreach my $Param ( @{$Data->{Parameters}} )
	    {
		next if ( !$NeedsConfig &&
			  !$Param->{Required} && $Param->{Value} eq "" );

		print "     $Param->{Name}\n";
		print "\t" . join( "\n\t", @{$Param->{Description}} );
		print "\n\tDefault: " . $Param->{String} . "\n";
		print "\tWhat value would you like for $Param->{Name}?".
		    " <enter for default> ";
		$_ = <>; chomp;

		$Param->{Value} = ( $_ eq "" ) ? $Param->{Default} : $_;
		$Data->{ConfigChanged}++;
	    }
	}
    }

    # Problems?
    if ( $Errors )
    {
	return 0;
    }
    return 1;

} # ConfigureModule()
# ******************************************************

# ******************************************************
# Find the configuration file to use
# ******************************************************
sub ImportOption( $$ )
{
    my $Opt = shift;
    my $Data = shift;

    $Opt = lc( $Opt );
    if ( ! exists $ConfigParms{Options}{$Opt} )
    {
	print "Can't find option '$Opt'\n";
	return 0;
    }
    else
    {
	foreach my $Key ( keys %{$ConfigParms{Options}{$Opt}} )
	{
	    $Data->{$Key} = $ConfigParms{Options}{$Opt}{$Key};
	    print "Importing '$Key': " . $Data->{$Key} . "\n";
	}
    }

    return 1;

} # ImportOption()
# ******************************************************

# ******************************************************
# DoUpdateConfig
# ******************************************************
sub DoUpdateConfig( $ )
{
    my $Data = shift;
    my $Name = $Data->{Name};

    # Config changed?
    return 1 if ( ! $Data->{ConfigChanged} );

    # Feedback if changed
    print "  !!NOTICE!! Configuration for $Name modified; updating..\n";

    # Do it
    $ConfigFiles->AddText( "" );
    $ConfigFiles->AddText( "##" );
    $ConfigFiles->AddText( "## Configuration for Module $Name" );
    foreach my $Text ( @{$Data->{Description}} )
    {
	$ConfigFiles->AddText( "##\t$Text" );
    }

    my $Prefix = uc( $Config{CronName} . "_" . $Name . "_" );
    $ConfigFiles->AddText
	( "$Config{CronJobListVar} = \$($Config{CronJobListVar}) $Name" );
    $ConfigFiles->AddText
	( $Prefix . "EXECUTABLE = " ."\$(MODULES)/" . $Data->{ModuleFile} );
    $ConfigFiles->AddText
	( $Prefix . "PREFIX = " . $Data->{Prefix} );
    $ConfigFiles->AddText
	( $Prefix . "PERIOD = " . $Data->{Period} );
    $ConfigFiles->AddText
	( $Prefix . "MODE = " . $ConfigParms{ModeStrings}[$Data->{Mode}] );
    $ConfigFiles->AddText
	( $Prefix . "RECONFIG = " . $Data->{Reconfig} );
    $ConfigFiles->AddText
	( $Prefix . "KILL = " . $Data->{Kill} );

    $ConfigFiles->AddText( "##  Parameters for module $Name:" );
    foreach my $Param ( @{$Data->{Parameters}} )
    {
	$ConfigFiles->AddText( "##  $Param->{Name}" );
	foreach my $Text ( @{$Param->{Description}} )
	{
	    $ConfigFiles->AddText( "##    $Text" );
	}
	my $Default= (defined $Param->{Default} ) ? $Param->{Default} : "";
	$ConfigFiles->AddText( "##    Default: $Default" );

	my $ParamName = uc( $Config{CronName} . "_" . $Name . "_"  .
			    $Param->{Name} );
	if ( defined $Param->{Value} )
	{
	    $ConfigFiles->AddText( "$ParamName = $Param->{Value}" );
	}
	else
	{
	    $ConfigFiles->AddText( "# $ParamName =" );
	}
	$ConfigFiles->AddText( "##" );
    }

    # Check these lines
    $ConfigFiles->AddText( "##" );
    if ( $Data->{OriginalConfigLines} >= 0 )
    {
	my $LineNo = $Data->{OriginalConfigLines};
	my $File = $Data->{OriginalConfigFile};
	$ConfigFiles->AddText( "## Check original configuration:" );
	$ConfigFiles->AddText( "##  $File line $LineNo" );

    }
    $ConfigFiles->AddText( "## End of Configuration for Module $Name" );
    $ConfigFiles->AddText( "##" );

    return 1;

} # DoUpdateConfig()
# ******************************************************

# ******************************************************
# Write the new config file
# ******************************************************
sub WriteConfig( )
{
    # Any work to do?
    my $Num = $ConfigFiles->NewTextLines( );
    if ( $Num <= 0 )
    {
	print "\nNo config updates required\n";
	return 1;
    }

    # Write the updates to the temp file
    my ( $File, $TmpFile ) = $ConfigFiles->WriteUpdates( );
    if ( ! $File or ! $TmpFile )
    {
	die "Error writing update";
    }

    # Ask the user unless they explicietly asked us to overwrite it
    my $OverWrite = $Config{UpdateConfig};
    if ( ! $OverWrite )
    {
	if ( -f $File )
	{
	    print "\nAbout to overwrite $File; ok [N/y]? ";
	    $OverWrite = 1 if ( <> =~ /^y/ );
	}
	else
	{
	    $OverWrite = 1;
	}
    }

    # OverWrite it?
    if ( $OverWrite )
    {
	my $Backup = "$File.bak";
	unlink( $Backup );
	rename( $File, $Backup );
	rename( $TmpFile, $File );
	print "\n$File updated\n";
    }
    else
    {
	print "Updated config saved in $TmpFile\n";
    }

    return 1;

} # WriteConfig()
# ******************************************************

# ******************************************************
# Dump out usage
# ******************************************************
sub Usage ( $ )
{
    my $Unknown = shift;

    print "$Program: unknown option '$Unknown'\n" if ( $Unknown ne "" );
    printf "usage: $Program [options] [files]\n";
    print "use '-h' for more help\n";
    exit 1;

} # usage ()
# ******************************************************

# ******************************************************
# Dump out help
# ******************************************************
sub Help ( )
{
    my ($opt, $text);

    printf "usage: $Program [options] [files]\n";
    foreach $opt (sort {lc($a) cmp lc($b) } keys %Options)
    {
	printf ("  %15s : %-40s\n", $opt, $Options{$opt} );
    }
    exit 0;

} # help ()
# ******************************************************
