#! /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(tempfile);

# 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 ConfigureModule( $ );
sub ReWriteConfigFile( );
sub UpdateConfigFile( );
sub ParseOldJob( $$$ );
sub ImportOption( $$ );
sub Usage ( $ );
sub Help ( );


# ******************************************************
# Command line options
# ******************************************************
my $Distribution = "hawkeye";
my %Options =
    (
     "[--config=file]"	=> "Specify $Distribution config file",
     "[--update=file]"	=> "Specify config file to update",
     "[--condor]"	=> "\"Condor\" mode",
     "[--hawkeye]"	=> "\"Hawkeye\" mode",
     "[--help|-h]"	=> "Dump help",
    );

# Hash of directories & files
my %Config =
(
 RootDir => "",
 ConfigFile =>
 {
  Base => "",
  Updated => "",
 },
 Daemon => "",
 CronName => "",

);

# Valid setup info
my %ConfigOptions =
(
 continuous	=> { Reconfig => "True", WaitForExit => "True", },
 kill		=> { Kill => "True", },
 nokill		=> { Kill => "False", },
 reconfig	=> { Reconfig => "True", },
 waitforexit	=> { WaitForExit => "True", },
);

# 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};
    }

    # Guess what distribution this is..
    if ( exists $ENV{CONDOR_DISTRIBUTION} )
    {
	$Distribution = $ENV{CONDOR_DISTRIBUTION};
    }
    elsif ( exists $ENV{HAWKEYE_CONFIG} )
    {
	$Distribution = "hawkeye";
    }
    elsif ( exists $ENV{CONDOR_CONFIG} )
    {
	$Distribution = "condor";
    }

    $Program = $Distribution . "_config_update";
    $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( );

    # Write the modified file if required
    my $OldJobs = $ConfigMacros->Expand( $Config{CronJobsVar} );
    if ( $OldJobs ne "" )
    {
	UpdateConfigFile( );
    }

} # end of "main"
# ******************************************************

# ******************************************************
# Parse the command line, etc.
# ******************************************************
sub CommandLine( @ )
{
    foreach my $Arg ( @_ )
    {

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

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

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

	# Set the daemon to import for
	elsif ( $Arg =~ /^--daemon=(\w+)$/ )
	{
	    $Config{Daemon} = $1;
	}
	elsif ( $Arg =~ /^--(startd|schedd)$/ )
	{
	    $Config{Daemon} = $1;
	}

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

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

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

} # 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";

    # Get list of cron name variables
    foreach my $Var ( $ConfigMacros->GrepName( "(?i:_cron_name)\$" ) )
    {
	my $Daemon = $Var;
	$Daemon =~ s/_cron_name//g;
	my $CronName = $ConfigMacros->Expand( $Var );
	my $JlVar = uc($CronName) . "_JOBLIST";
	my $JlVarOld = uc($CronName) . "_JOBS";
	$Config{CronDaemons}{$Daemon}{NameVar} = $Var;
	$Config{CronDaemons}{$Daemon}{CronName} =
	$Config{CronDaemons}{$Daemon}{JobListVar} = $JlVar;
	$Config{CronDaemons}{$Daemon}{JobListVarOld} = $JlVarOld;
    }

    # If we know the 
    if ( $Config{Daemon} ne "" )
    {
	$Config{CronNameVar} = uc($Config{Daemon}) . "_CRON_NAME",
    }

    # If we don't know what daemon cron name variable we're looking for
    # try to guess...
    if ( !defined $Config{CronNameVar} or $Config{CronNameVar} eq "" )
    {
	foreach my $Daemon ( "startd", "schedd" )
	{
	    if ( exists $Config{CronDaemons}{$Daemon} )
	    {
		$Config{Daemon} = $Daemon;
		$Config{CronNameVar} =
		    $Config{CronDaemons}{$Daemon}{NameVar};
		last;
	    }
	}
    }

    # If we still don't know the daemon, assume startd
    if ( exists $Config{CronNameVar} eq "" )
    {
	$Config{Daemon} = "startd";
	$Config{CronNameVar} = "STARTD_CRON_NAME",
    }

    # Add basic things to the config if they're not defined...
    my $NameVar = $Config{CronNameVar};
    $Config{CronName} = $ConfigMacros->Expand( $NameVar );
    if ( ! defined $Config{CronName} )
    {
	if ( $Distribution eq "hawkeye" )
	{
	    $Config{CronName} = "Hawkeye";
	}
	elsif ( $Config{Daemon} eq "startd" )
	{
	    $Config{CronName} = "Cron";
	}
	else
	{
	    $Config{CronName} = "Scron";
	}
	$ConfigFiles->AddText( "$NameVar = $Config{CronName}" );
	$ConfigMacros->Set( $NameVar, $Config{CronName} );
    }
    print "Dist = '$Distribution'\n";
    print "Var  = '$Config{CronNameVar}'\n";
    print "Name = '$Config{CronName}'\n";

    # Jobs defined?
    my $CronJobsVar = uc( $Config{CronName} . "_JOBS" );
    my $Jobs = $ConfigMacros->Expand( $CronJobsVar );
    if ( ! defined $Jobs )
    {
	$ConfigFiles->AddText( "$CronJobsVar = " );
	$ConfigMacros->Set( "$CronJobsVar", "" );
    }
    $Config{CronJobsVar} = $CronJobsVar;

    # Job list defined?
    my $CronJobListVar = uc( $Config{CronName} . "_JOBLIST" );
    $Jobs = $ConfigMacros->Expand( $CronJobsVar );
    if ( ! defined $Jobs )
    {
	$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( $CronJobsVar );
	if ( ( $Info ) && ( $Info->{File} ) )
	{
	    $Config{ConfigFile}{Updated} = $Info->{File};
	    print "Found '$CronJobsVar' in "
		. $Info->{File} . " line " . $Info->{Line} . "\n";
	}
    }

    # Try to find the file that last defined (STARTD|SCHEDD)_CRON_NAME
    if ( $Config{ConfigFile}{Updated} eq "" )
    {
	my $Info = $ConfigMacros->GetInfo( $NameVar );
	if ( ( $Info ) && ( $Info->{File} ) )
	{
	    $Config{ConfigFile}{Updated} = $Info->{File};
	    print "Found '$NameVar' 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 = "(?i:jobs|joblist|hawkeye|scron)\$";
	my @List = $ConfigFiles->GrepName( $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
# ******************************************************

# ******************************************************
# DoUpdateConfig
# ******************************************************
sub UpdateConfig( $ )
{
    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" );
    }

    $ConfigFiles->AddText
	(
	 "$Config{CronJobsVar} = \$($Config{CronJobsVar}) " .
	 $Name . ":" . $Data->{Prefix} . ":" .
	 "\$(MODULES)/" . $Data->{ModuleFile} . ":" .
	 $Data->{Period} . ":" . join(":", @{$Data->{Options}} )
	);
    $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 ReWriteConfigFile( )
{
    my $File = $ConfigFiles->GetUpdateFile( );
    open( ORIG, $File ) or die "Can't read config file '$File'";

    my $Pattern = "$File.XXXXXXX";
    my ( $tmp_fh, $tmp_filename ) =
	tempfile( $Pattern, UNLINK => 0 );
    if ( !defined $tmp_fh ) {
	die "error: Can't create temporary file\n";
    }

    # OK, now read the whole file, write to the temp file
    my @Stack;
    my @JobList;
    while( <ORIG> )
    {
	chomp; s/\s+$//;
	my $DumpStack = 0;
	my @Orig;
	push( @Orig, $_ );

	# Add in continuation lines
	my $Line = "";
	while (  ( !/\#/ ) and ( /(.*)\\$/ )  )
	{
	    $Line = ( $Line eq "" ) ? $1 : $Line . " " . $1;
	    $_ = <ORIG>;
	    chomp; s/\s+$//;
	    push( @Orig, $_ );
	    s/^\s+//;
	    $Line .= $_;
	}
	$Line = $_ if ( $Line eq "" );

	if ( $Line =~ /(.*?)(\w+:\w+:\"[^\"]+\":\S+)([^\\]*)(\\?)/ )
	{
	    my ( $t1, $t2, $t3, $t4 ) = ( $1, $2, $3, $4 );
	    $t4 = undef if ( ( !defined $4 ) or ( $4 eq "" ) );
	    foreach my $t ( @Orig )
	    {
		print $tmp_fh "#OLD $t\n";
	    }

	    my $r = ParseOldJob( $t1, $t2, \@Stack );
	    if ( ! $r )
	    {
		print STDERR "Error importing old job '$t2'\n";
		next;
	    }
	    my $Name      = $r->{Name};
	    my $Prefix    = $r->{Prefix};
	    my $IsComment = $r->{IsComment};
	    print "  Importing old job '$Name' ($t2) from line $.: " .
		( !$IsComment ? "OK\n" : "Commented out\n" );
	    push( @JobList, $Name ) if ( ! $IsComment );

	    my $New = "$Prefix" . $Name . "$t3";
	    $New .= " \\" if ( defined $t4 );
	    print $tmp_fh "$New\n";

	    $DumpStack = 1 if ( ! defined $t4 );
	}
	elsif ( $Line =~ /(.*?)(\w+:\w+:[^:]+:\S+)([^\\]*)(\\?)/ )
	{
	    my ( $t1, $t2, $t3, $t4 ) = ( $1, $2, $3, $4 );
	    $t4 = undef if ( ( !defined $4 ) or ( $4 eq "" ) );
	    foreach my $t ( @Orig )
	    {
		print $tmp_fh "#OLD: $t\n";
	    }

	    my $r = ParseOldJob( $t1, $t2, \@Stack );
	    if ( ! $r )
	    {
		print STDERR "Error importing old job '$t2'\n";
		next;
	    }
	    my $Name      = $r->{Name};
	    my $Prefix    = $r->{Prefix};
	    my $IsComment = $r->{IsComment};
	    print "  Importing old job '$Name' ($t2) from line $.: " .
		( !$IsComment ? "OK\n" : "Commented out\n" );
	    push( @JobList, $Name ) if ( ! $IsComment );

	    my $New = "$Prefix" . $Name . "$t3";
	    $New .= " \\" if ( defined $t4 );
	    print $tmp_fh "$New\n";

	    $DumpStack = 1 if ( ! defined $t4 );
	}
	else
	{
	    foreach my $d ( keys %{$Config{CronDaemons}} )
	    {
		my $JlVar = $Config{CronDaemons}{$d}{JobListVar};
		my $JlVarOld = $Config{CronDaemons}{$d}{JobListVarOld};
		$Line =~ s/$JlVarOld/$JlVar/ig;
	    }
	    print $tmp_fh "$Line\n";
	}
	if ( $DumpStack )
	{
	    while ( my $Line = shift @Stack )
	    {
		print $tmp_fh "$Line\n";
	    }
	}
    }
    close( ORIG );
    close( $tmp_fh );

    # Done
    return ( $File, $tmp_filename, @JobList );

} # ReWriteConfigFile()
# ******************************************************

# ******************************************************
# Parse old job line
# ******************************************************
sub ParseOldJob( $$$ )
{
    my $LinePrefix = shift;
    my $Line = shift;
    my $Stack = shift;

    # Look at the prefix
    my $IsComment = ( $LinePrefix =~ /#/ ) ? 1 : 0;
    my $Comment = $IsComment ? "#" : "";
    my $CronName = $Config{CronName};
    if ( $LinePrefix =~ /(\w+)_jobs\)/i )
    {
	$CronName = $1;
    }
    $LinePrefix =~ s/_jobs/_JOBLIST/ig;

    my $Name;
    my $Prefix;
    my $Exe;
    my $End;
    if ( $Line =~ /(\w+):(\w+):(\"[^\"]+\"):(\S+)/ )
    {
	$Name = $1;
	$Prefix = $2;
	$Exe = $3;
	$End = $4;
    }
    elsif ( $Line =~ /(\w+):(\w+):([^:]+):(\S+)/ )
    {
	$Name = $1;
	$Prefix = $2;
	$Exe = $3;
	$End = $4;
    }
    else
    {
	return undef;
    }

    my ( $Period,  @Opts ) = split( /:/, $End );
    my $FullName = uc( $CronName . "_" . $Name );

    my %r;
    $r{Name} = $Name;
    $r{Prefix} = $LinePrefix;
    $r{IsComment} = $IsComment;
    push( @{$Stack}, $Comment . $FullName . "_PREFIX = $Prefix" );
    push( @{$Stack}, $Comment . $FullName . "_EXECUTABLE = $Exe" );
    push( @{$Stack}, $Comment . $FullName . "_PERIOD = $Period" );

    my %Data;
    foreach my $Opt ( @Opts )
    {
	if (! ImportOption( $Opt, \%Data ) )
	{
	    print STDERR
		"Line $.: Ignoring unknown option '$Opt' for job '$Name'\n";
	}
    }
    foreach my $Key ( keys %Data )
    {
	my $Parm = uc( $FullName . "_" . $Key );
	my $Value = $Data{$Key};
	push( @{$Stack}, "$Comment$Parm = $Value" );
    }
    return \%r;

} # ParseOldJob
# ******************************************************

# ******************************************************
# Update the config file
# ******************************************************
sub UpdateConfigFile( )
{

    # Write the updates to the temp file
    my ( $File, $TmpFile, @ImportedJobs ) = ReWriteConfigFile( );
    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\nBackup saved in $Backup\n";
	my $Cmd = $ConfigFiles->GetConfigProg() . " " .
	    $Config{CronJobListVar};
	my $List = `$Cmd`;
	chomp $List;
	$List =~ s/\s+$//; $List =~ s/^\s+//;
	my @List = split( /\s+/, $List );

	print "List of jobs after reconfig:\n\t" .
	    join( " ", sort( @List ) ) . "\n";
	print "Imported job list:\n\t" .
	    join( " ", sort( @ImportedJobs ) ) . "\n";
	if ( $#List != $#ImportedJobs )
	{
	    print STDERR "\n!! WARNING: Lists are different size. !!\n";
	    print STDERR "\tYou should hand examine the files\n";
	}
    }
    else
    {
	print "Updated config saved in $TmpFile\n";
    }

    return 1;

} # UpdateConfigFile()
# ******************************************************

# ******************************************************
# Import an option line
# ******************************************************
sub ImportOption( $$ )
{
    my $Opt = shift;
    my $Data = shift;

    $Opt = lc( $Opt );
    if ( ! exists $ConfigOptions{$Opt} )
    {
	return 0;
    }
    else
    {
	foreach my $Key ( keys %{$ConfigOptions{$Opt}} )
	{
	    $Data->{$Key} = $ConfigOptions{$Opt}{$Key};
	}
    }

    return 1;

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

    print "$Program: unknown option '$Unknown'\n" if ( defined $Unknown );
    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 ()
# ******************************************************
