#! /usr/bin/perl -w
##**************************************************************
##
## 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.
##
##**************************************************************


##*****************************************************************
## Start a dynamic schedd on a pre-installed, pre-configured 
## machine.  This schedd is sort of like a paper plate, incredibly 
## useful for one-time only.  The disposable schedd.
## Author: Joe Meehean
## Date: 6/7/05
##*****************************************************************

##*****************************************************************
## Note1: If the user wants to bind the configuration to the
## binaries at startup time.  The configuration attributes that
## detail the location of the binaries (MASTER, SCHEDD, etc...)
## should be defined relative the the attribute DEPLOYMENT_RELEASE_DIR,
## which stands for late-binding release directory.  This attribute
## is set to the given release directory immediately prior to 
## startup.
##*****************************************************************

##*****************************************************************
## Note2: condor_config_val must be located either in release_dir
## or in bin/ under release_dir
##*****************************************************************

#***
# Uses
#***
use strict;
use FindBin;
use lib ($FindBin::Bin, "$FindBin::Bin/lib", "$FindBin::Bin/../lib");
use Execute;
use Getopt::Long;
use File::Find;
use File::Spec;
use Cwd;
use English;
use Sys::Hostname;
use Socket;

#***
# Constant Static Variables
#***
my $USAGE = 
    "Usage: condor_local_start <-configfile config_file> <-basedir release_dir>\n".
    "[-localdir local_install_dir] [-localconfig local_config_file]\n".
    "[-logarchive log_archive] [-spoolarchive spool_archive]\n".
    "[-execarchive exec_archive]\n".
    "[-filelock] [-pid] [-artifact artifact_file]\n".
    "[-wget] [-globuslocation globus_location]\n".
    #"[-dyn] [-help]\n";
    "[-help]\n";

my $FG_OPT = '-f';

my $CONDOR_CONFIG_VAL = 'condor_config_val';

my $CONDOR_CONFIG_BIND = 'condor_config_bind';
my $BINDING_CONFIG_NAME = 'condor_config.binding';

my $CONDOR_INSTALL_LOCAL = 'condor_install_local';
my $LOCALDIR_OPT = '-localdir';
my $CONFIGFILE_OPT = '-configfile';
my $CONDOR_INSTALL_CONFIG = 'condor_config.local_install';
my $LOG_ARCHIVE_OPT = '-logarchive';
my $SPOOL_ARCHIVE_OPT = '-spoolarchive';
my $EXEC_ARCHIVE_OPT = '-execarchive';
my $WGET_OPT = '-wget';
my $GLOBUS_LOC_OPT = '-globuslocation';

my $FILELOCK_MIDWIFE = 'filelock_midwife';
my $PID_MIDWIFE = 'uniq_pid_midwife';
my $MIDWIFE_FILE_OPT ='--file';

#  environment variable names
my $GLOBAL_CONFIG_ENV = 'CONDOR_CONFIG';
my $LOCAL_CONFIG_FILE_ENV = '_CONDOR_LOCAL_CONFIG_FILE';
my $DEPLOYMENT_RELEASE_DIR_ENV = '_CONDOR_DEPLOYMENT_RELEASE_DIR';
my $DEPLOYMENT_LOCAL_DIR_ENV = '_CONDOR_DEPLOYMENT_LOCAL_DIR';
my $EXECUTE_DIR_ENV = '_CONDOR_EXECUTE';
my $LOG_DIR_ENV = '_CONDOR_LOG';
my $SPOOL_DIR_ENV = '_CONDOR_SPOOL';


#  condor_config attributes
my $MASTER_ATTR = 'MASTER';
my $REQUIRED_EXEC_ATTR = 'DEPLOYMENT_REQUIRED_EXECS';
my $REQUIRED_DIR_ATTR = 'DEPLOYMENT_REQUIRED_DIRS';
my $RECOMMENDED_EXEC_ATTR = 'DEPLOYMENT_RECOMMENDED_EXECS';
my $RECOMMENDED_DIR_ATTR = 'DEPLOYMENT_RECOMMENDED_DIRS';

# Defaults
my $DEFAULT_ARTIFACT = 'MasterMidwife.artifact';

#***
# Non-constant Static Variables
#***
my $condor_config_file = 0;
my $release_dir = 0;
my $local_install_dir = cwd;  #Defaults to the working directory
my $local_config_file = 0;
my $help = 0;
my $condor_config_val_cmd = 0;
#my $dynamic_naming = 0;
my @master_params = ();
my $local_install_config = 0;
my $log_archive = 0;
my $spool_archive = 0;
my $exec_archive = 0;
my $filelock_flag = 0;
my $pid_flag = 0;
#my $midwife_artifact = 'MasterMidwife.artifact';
my $midwife_artifact = 0;
my $globus_location = 0;
my $wget_flag = 0;

#***
# Main Function
#***
GetOptions('configfile=s'=>\$condor_config_file,
	   'basedir=s'=>\$release_dir,
	   'localdir=s'=>\$local_install_dir,
	   'localconfig=s'=>\$local_config_file,
	   'logarchive=s'=>\$log_archive,
	   'spoolarchive=s'=>\$spool_archive,
	   'execarchive=s'=>\$exec_archive,
	   'filelock'=>\$filelock_flag,
	   'pid'=>\$pid_flag,
	   'artifact=s'=>\$midwife_artifact,
	   'wget'=>\$wget_flag,
	   'globuslocation=s'=>\$globus_location,
	   #'dyn'=>\$dynamic_naming,
	   'help'=>\$help);


# Process command-line arguments
my $missing_required_args = !$release_dir || !$condor_config_file;
die "$USAGE" if( $missing_required_args || $help );

# Ensure the arguments are valid
if (!-e $condor_config_file){
    die "$condor_config_file does not exist";
}

if(!-e $release_dir || !-d $release_dir){
    die "$release_dir does not exist or is not a directory";
}

if(!-e $local_install_dir || !-d $local_install_dir ){
    die "$local_install_dir does not exist or is not a directory";
}

if( $local_config_file && !-e $local_config_file ){
    die "$local_config_file does not exist";
}

# Convert all relative file paths to absolute file paths
&ConvertToAbPaths();

# Perform any setup required of the local directory
$local_install_config = &SetupLocalDir();

# Commented out until dynamic naming is enabled
#if( $dynamic_naming ){
#    &SetupLocalDirDyn();
#} else{
     #$local_install_config = &SetupLocalDir();
#}

# Setup the environment
&SetupEnvironment();

# Make sure everything we need is ready
&PreStartDiagnostic();

# Start the master
&StartMaster();

# Success
print "Successfully Started\n";


#*********************************************************************
# We cannot be sure that the user will pass absolute file paths.  
# Therefore, we must convert arbitrary relative paths into absolute
# paths.
#*********************************************************************
sub ConvertToAbPaths(){

    my @tempList = (\$condor_config_file, \$release_dir, 
		    \$local_install_dir, \$local_config_file);

    
    # foreach global reference in the list
    foreach(@tempList){
	
	if( $$_ ){

	    # Convert to absolute paths
	    $$_ = File::Spec->rel2abs($$_);

	    # Ensure the coversion was a success
	    File::Spec->file_name_is_absolute($$_)
		or warn "Could not convert $$_ to an absolute".
		" path (possibly because it is in a different volume".
		" that the current working directory";
	}

    }
}

#*********************************************************************
# Performs any prepartion of the local install directory required.
# This includes creating /log, /spool, and /execute if they don't 
# exist.
#*********************************************************************
sub SetupLocalDir(){
    

    # Create the arguments for install_local
    my $install_config_path = File::Spec->catpath(0, 
						  $local_install_dir, 
						  $CONDOR_INSTALL_CONFIG);
						  
    my @args = ($LOCALDIR_OPT, $local_install_dir,
		$CONFIGFILE_OPT, $install_config_path,
		);

    # add the archive args if neccessary
    push @args, ($LOG_ARCHIVE_OPT, $log_archive) if( $log_archive );
    push @args, ($SPOOL_ARCHIVE_OPT, $spool_archive) if( $spool_archive );
    push @args, ($EXEC_ARCHIVE_OPT, $exec_archive) if($exec_archive);

    # add the transfer option flags if neccessary
    push @args, $WGET_OPT if( $wget_flag );
    push @args, ($GLOBUS_LOC_OPT, $globus_location) if( $globus_location );

    # Find condor_install_local
    my $install_local_path = File::Spec->catpath(0, 
						 $FindBin::Bin, 
						 $CONDOR_INSTALL_LOCAL);
    
    # condor_install_local -localdir $local_install_dir 
    #                      -configname condor_config.local_install
    !system $install_local_path, @args
	or die "FAILED: Failed to create a local directory structure\n";

    # Return the name of the local install config file
    return $install_config_path;
}

#*********************************************************************
# Performs any prepartion of the local install directory required
# when dynamic naming is required.
#*********************************************************************
sub SetupLocalDirDyn(){
    # The master creates the needed local directory structure
    # when we are using dynamic naming
    # This is only needed to guide the master towards where
    # these directories should be located
    $ENV{$EXECUTE_DIR_ENV} = File::Spec->catdir($local_install_dir, 'execute');
    $ENV{$LOG_DIR_ENV} = File::Spec->catdir($local_install_dir, 'log');
    $ENV{$SPOOL_DIR_ENV} = File::Spec->catdir($local_install_dir, 'spool');
}

#*********************************************************************
# Setup the running environment for condor.  This includes the 
# location of binaries, the location of the global configuration, the 
# location of the local directory,  and the set of local 
# configuration files.  Dynamically binds the runtime local config,
# the config created from making the local directory, and the global
# config together if neccessary.
#*********************************************************************
sub SetupEnvironment(){
    
    # Default the global config to the one passed in
    my $global_config = $condor_config_file;
    
    # Determine if we need a binding config
    if( $local_config_file || $local_install_config ){
	
	# Determine which configs need to be bound
	my @configs = ($condor_config_file);
	push @configs, $local_config_file if( $local_config_file );
	push @configs, $local_install_config if( $local_install_config );

	# Set the global config to be the binding config
	$global_config = &CreateBindingConfig(@configs);
    }
    
    # set the configuration file
    $ENV{$GLOBAL_CONFIG_ENV} = $global_config;

    # set the location of the late-binding 
    # release_dir and local dir
    $ENV{$DEPLOYMENT_RELEASE_DIR_ENV} = $release_dir;
    $ENV{$DEPLOYMENT_LOCAL_DIR_ENV} = $local_install_dir;
}

#*********************************************************************
# Binds a local configuration file into the global configuration
# without modifying the global configuration file.  Essentially, it
# adds another level of configuration above the global and 
# user-defined local configuration.  Both of these files are local
# configs to this "higher" binding configuration.  The global config
# is first in the list so the user-defined config can override its
# values.
#*********************************************************************
sub CreateBindingConfig(){

    my @configs = @_;  #Name the parameters

    # Manufacture a name for the binding configuration file
    # The binding config is local to this installation so 
    # it should be in the local dir
    my $binding_config_file = $BINDING_CONFIG_NAME;
    
    # Commented out until dynamic naming is enabled
    #if( $dynamic_naming ){
    #$binding_config_file = &AddDynamicPostFix($BINDING_CONFIG_NAME);
    #} 

    # put the file in the local install dir
    $binding_config_file = File::Spec->catfile($local_install_dir, $binding_config_file);

    # See if it exists
    if( -e $binding_config_file ){
	warn "WARNING: Binding configuration file $binding_config_file".
	    " already exists, attempting to clobber";
    }

    # Construct the arguments to condor_config_bind
    my @args = ('-o', $binding_config_file, @configs);

    # execute condor_config_bind
    my $config_bind_path = File::Spec->catpath(0, $FindBin::Bin, $CONDOR_CONFIG_BIND);
    !system $config_bind_path, @args
	or die "FAILED: $CONDOR_CONFIG_BIND failed to create a binding config: $!";

    # Return the binding config file name
    return $binding_config_file;
}

#*********************************************************************
# Currently this function just ensures that all of the binaries and 
# directories we will need exist.  Later incarnations of this function 
# can perform any number of pre-start sanity checks.
#*********************************************************************
sub PreStartDiagnostic(){
    
    # Create a hash to map config attribute names
    # to the functions that will evaluate whether the attribute value
    # is valid
    my %AttrToEvalFunction;

    
    # Query the recommended and required attributes
    my $reqExecString = &QueryCondorConfig($REQUIRED_EXEC_ATTR);
    my $reqDirString = &QueryCondorConfig($REQUIRED_DIR_ATTR);
    my $recExecString =  &QueryCondorConfig($RECOMMENDED_EXEC_ATTR);
    my $recDirString = &QueryCondorConfig($RECOMMENDED_DIR_ATTR);
    
    # Map the attributes to the evaluation functions
    $AttrToEvalFunction{$reqExecString} = \&RequiredExec if( $recExecString );
    $AttrToEvalFunction{$reqDirString} = \&RequiredDir if( $reqDirString );
    $AttrToEvalFunction{$recExecString} = \&RecommendedExec if( $recExecString );
    $AttrToEvalFunction{$recDirString} = \&RecommendedDir if( $recDirString );
 
    # For each individual attribute name in the required/recommended list
    # perform the evaluation function.
    my $eval_func = 0;
    my @attrList = ();
    foreach my $attrString( keys %AttrToEvalFunction ){
	# retrieve the evaluation function
	$eval_func = $AttrToEvalFunction{$attrString};
	# tokenize the list of required/recommended attributes
	@attrList = split /,\s*/, $attrString;
	
	# foreach attribute, run the evaluation function
	foreach my $attrName(@attrList){
	    &$eval_func($attrName);
	}
	
    }
    
}


#*********************************************************************
# Determines whether the executable represented by the given
# condor_config attribute is defined, exists, and is executable by
# this user.
# Dies on failure.
#*********************************************************************
sub RequiredExec(){
    my ($attrName) = @_;

    # Get the condor config value for the attribute
    # and ensure it exists and
    # has the correct permissions
    my $attrValue = &QueryCondorConfig($attrName);
    if( !$attrValue ){
	die "Pre-Start Diagnostic FAILED: ".
	    "$attrName is not defined\n";
    } elsif( !-e $attrValue || !-x $attrValue ){
	die "Pre-Start Diagnostic FAILED: ".
	    "$attrValue defined by $attrName does not exist".
	    " or is not executable by this user\n";
    }

    
}

#*********************************************************************
# Determines whether the executable represented by the given
# condor_config attribute is defined, exists, and is executable by
# this user.
# Warns on failure.
#*********************************************************************
sub RecommendedExec(){
    my ($attrName) = @_;
    
    # Get the condor config value for the attribute
    # and ensure it exists and
    # has the correct permissions
    my $attrValue = &QueryCondorConfig($attrName);
    if( !$attrValue ){
	warn "Pre-Start Diagnostic WARNING: ".
	    "$attrName is not defined\n";
    } elsif( !-e $attrValue || !-e $attrValue ){
	warn "Pre-Start Diagnostic WARNING: ".
	    "$attrValue defined by $attrName does not exist".
	    " or is not executable by this user\n";
    }
}

#*********************************************************************
# Determines whether the directory represented by the given
# condor_config attribute is defined, exists, and is a directory..
# Dies on failure.
#*********************************************************************
sub RequiredDir(){
    my ($attrName) = @_;

    # Get the condor config value for the attribute
    # and ensure it exists and
    # has the correct permissions
    my $attrValue = &QueryCondorConfig($attrName);
    if( !$attrValue ){
	die "Pre-Start Diagnostic FAILED: ".
	    "$attrName is not defined\n";
    } elsif( !-e $attrValue || !-d $attrValue ){
	die "Pre-Start Diagnostic FAILED: ".
	    "$attrValue defined by $attrName does not exist".
	    " or is not a directory\n";
    }
}

#*********************************************************************
# Determines whether the directory represented by the given
# condor_config attribute is defined, exists, and is a directory..
# Warns on failure.
#*********************************************************************
sub RecommendedDir(){
    my ($attrName) = @_;

    # Get the condor config value for the attribute
    # and ensure it exists and
    # has the correct permissions
    my $attrValue = &QueryCondorConfig($attrName);
    if( !$attrValue ){
	warn "Pre-Start Diagnostic WARNING: ".
	    "$attrName is not defined\n";
    } elsif( !-e $attrValue || !-d $attrValue ){
	warn "Pre-Start Diagnostic WARNING: ".
	    "$attrValue defined by $attrName does not exist".
	    " or is not a directory\n";
    }
}

#*********************************************************************
# Locates and starts the condor_master with a midwife.
#*********************************************************************
sub StartMaster(){
    #Determine the midwife to use
    # defaults to the pid midwife
    my $midwife = 0;
    if( $filelock_flag ){
	$midwife = $FILELOCK_MIDWIFE;
    } else{
	$midwife = $PID_MIDWIFE;
    }

    # Ensure the artifact file is set
    if( !$midwife_artifact ){
	# Give the default name and put it in the local dir
	$midwife_artifact = File::Spec->catpath(0, 
						$local_install_dir, 
						$DEFAULT_ARTIFACT);
    }

    # construct the arguments for the midwife program
    my $midwife_path = File::Spec->catpath(0, $FindBin::Bin, $midwife);
    my @args = ($MIDWIFE_FILE_OPT, $midwife_artifact);


    #Construct the master arguments
    #push(@master_params, '-dyn') if( $dynamic_naming );
    push @master_params, $FG_OPT;
    my $master_cmd = QueryCondorConfig($MASTER_ATTR);
    
    # Add them to the midwife parameters
    push @args, $master_cmd;
    push @args, @master_params;

    # Execute the master with the midwife
    exec $midwife_path, @args
	or die "Failed to start $midwife_path: $!";
    
}

#*********************************************************************
# Queries the condor configuration with the given parameters.
#*********************************************************************
sub QueryCondorConfig(){

    # If we haven't found condor_config_val, look it up now
    if( !$condor_config_val_cmd ){
	&FindCondorConfigVal();
    }

    my $returnHash = ExecuteAndCapture($condor_config_val_cmd, @_);
    
    if( $returnHash->{EXIT_VALUE} ){
	#error return undef
	return;
    }
    
    # Check to see if the result is more than one element
    if( @{$returnHash->{OUTPUT}} > 1 ){
	warn "WARNING: compressing results of condor config_val";
    }

    # compress the results into a single element
    my $result = '';
    for( @{$returnHash->{OUTPUT}} ){
	$result = $result.$_."\n";
    }

    #remove the last endline
    chomp($result);

    #return the result
    return $result;
}

#*********************************************************************
# Sets the global variable condor_config_val_cmd to point to the 
# location of the condor_config_val command.
#
# For right now condor_config_val must be either directly in the 
# release_dir or in a /bin dir under release. This should eventually 
# search (File::Find) under release_dir for condor_config_val. It 
# should also eventually use a 'which' style command to see if
# the binary is already in the path of this program.
#*********************************************************************
sub FindCondorConfigVal(){
    # set up the 2 possible paths
    my $flat_path = $release_dir.'/'.$CONDOR_CONFIG_VAL;
    my $bin_path = $release_dir.'/bin/'.$CONDOR_CONFIG_VAL;
    
    # determine which to use
    if( -e $flat_path ){
	$condor_config_val_cmd = $flat_path;
    } 
    elsif( -e $bin_path ){
	$condor_config_val_cmd = $bin_path;
    } 
    else{
	die "Failed to locate condor_config_val in either $flat_path OR $bin_path";
    }

}

#*********************************************************************
# Creates a dynamic name by appending the IP address and Pid
# of this process to the given name.
#*********************************************************************
sub AddDynamicPostFix(){
    my ($name) = @_;  #name the parameters

    # get the IP Address
    my $hostname = hostname();
    my $IpAddr = gethostbyname( $hostname );
    $IpAddr = inet_ntoa $IpAddr;
    # append the ip address and pid to the name 
    my $dynamic_name = $name.'.'.$IpAddr.'-'.$PID;

    return $dynamic_name;
}
