#!/usr/bin/perl

=head1 COPYRIGHT

This file is part of RemoteBOINC.

Copyright (C) 2010 Toni Giorgino, Universitat Pompeu Fabra

RemoteBOINC is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

RemoteBOINC 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

=cut


# $Id$

use strict;


use FindBin qw($Bin);		# where was script installed?
use lib $FindBin::Bin;		# use that dir for libs, too
use lib "$Bin/lib/$]";
use Error qw(:try);

use Getopt::Long qw(:config ignore_case pass_through);
use XML::Simple;
use HTTP::Request::Common qw(POST);
use HTTP::DAV;
use LWP::UserAgent;
use File::Basename;
use File::Temp qw(tempfile);

require qw(boinc_lib.pl);



# ----------------------------------------

# Constants 

use constant XMLROOT => "BoincRemote";

# The temporary description file
my $description_file_xml="description_file.xml";
my ($description_file_fh,$description_file)=
    tempfile("/tmp/description_temp.xml.XXXXX",
	     UNLINK=>1);


# Constant, relative to url
my $cgi_submit="boinc_submit_server.pl";
my $cgi_retrieve="boinc_retrieve_server.pl";




# Todo: replace config with longopts +
# http://www.perl.com/pub/a/2007/07/12/options-and-configuration.html?page=2

my $unparsed_cli="@ARGV";


# Keep these in the same order as help

my $url=$ENV{RBOINC_URL};
my $app_name='meta';
my $user=$ENV{USER};
my $email='';
my $authenticator='';

my $group='';
my $name='';

my $metadata_file='';
my $assign_user_all='';
my $num_steps=1;
my $priority=1;

my $fpops_est='';
my $fpops_bound='';
my $memory_bound='';
my $target_nresults=1;
my $balance=0;
my $min_quorum=1;
my $dry_run='';
my $batch='';

my $help='';
my $help_parameters='';
our $verbose='';



# Do the first pass at option parsing. 
# Keep these in the same order as help

# This list should be as short as possible because it is validated BEFORE the server-passed list.
# Any argument matched here will not be matched against the template-specific one.

GetOptions(
    'url=s' => \$url,
    'app_name=s' => \$app_name,
    'user=s' => \$user,
    'email=s' => \$email,
    'authenticator=s' => \$authenticator,

    'help' => \$help,
    'help_parameters' => \$help_parameters,
    'verbose' => \$verbose,
    ) or die "Error parsing command line";


if($help) {
    printHelp();
    exit(0);
}




############
# Option checking

checkMandatoryArguments(['url','app_name']) or exit 1;


# Ask the server for the template  and parse it
logInfo("Getting WU template");
my $wu_template=getWuTemplate("$url/$cgi_retrieve",$app_name);

logInfo("Parsing WU template");
my $parameters=parseWuTemplate($wu_template);

if($help_parameters) {
    printParametersHelp($wu_template);
    exit(0);
}






# Second pass at option parsing. This is tricky. We will use a version
# of getopts which requires two arguments:
#  a. a hash ref of ( optnameN => ref to destination )
#  b. a list of (optname1=s optname2=i ...) - note the =x part
# To roll-back, go to SVN version 318


#  1. create a hash ($parfiles) which will hold the variable options
#     and their content. It will be written through the references
#     created later
my $parfiles={};		


# 2. create a hash specifying the VARIABLE options and their
#    destination, in parfiles
my %getopt2_files_options=();

foreach my $opt (keys %$parameters) {
    $parfiles->{$opt}='';
    my $rr=\($parfiles->{$opt});
    $getopt2_files_options{$opt."=s"}=$rr;
}


# Now, we shall divide the options parsed at this stage in a
# "constant" group and a "variable" one, taken from the template

#  3. create a hash specifying the CONSTANT options and their
#     destinations (in plain variables)
my %getopt2_constant_options=(
	   'group=s' => \$group,
	   'name=s' => \$name,

	   'metadata_file=s' => \$metadata_file,
	   'assign_user_all=i' => \$assign_user_all,
	   'num_steps=i' => \$num_steps,
	   'priority=i' => \$priority,
	   
	   'fpops_est=f' => \$fpops_est,
	   'fpops_bound=f' => \$fpops_bound,
	   'memory_bound=f' => \$memory_bound,
	   'target_nresults=i' => \$target_nresults,
	   'balance=i' => \$balance,
	   'min_quorum=i' => \$min_quorum,
	   'dry_run' => \$dry_run,
	   'batch=i' => \$batch,
);


#  4. merge 2+3 into "all options"
my %getopt2_all_options=(%getopt2_files_options,%getopt2_constant_options);


#  5. use "copy_fix_options" on 2+3 to generate variables required at
#     points a+b above
my $temp_all_opts=copy_fix_options(\%getopt2_all_options);


Getopt::Long::Configure("no_pass_through"); # this time strict parsing
GetOptions($temp_all_opts, keys %getopt2_all_options) or die "Error parsing command line";

# End stage-2 option parsing. 




# Mandatory ones
checkMandatoryArguments(['name',
			 'group']) or exit 1;


# Now in parfiles we have the user-supplied arguments, minus the
# missing ones, as a hash. In $parameters we have the complete
# list. Check for missing arguments and unreadable files.

foreach my $o (keys %$parameters) {
    my $ov=$parfiles->{$o};
    if($ov) {
	logInfo("Option -$o has value $ov");
	if(! -r $ov) {
	    die "Can't open file $ov";
	}
    } else {
	logInfo("Option -$o is empty");
	if($parameters->{$o}->{optional}) {
	    logInfo("...which is allowed, server will supply a default");
	} else {
	    print STDERR "Mandatory file for parameter -$o is missing.\n";
	    exit 1;
	}
    }
}



############
# Build up list of files to upload. Currently this is
# "$description_file.xml", created above, plus those referenced by the
# keys of the %parameters hash.

# Keys is the remote file, value the local one.
# We already have this list.

my %toupload=%$parfiles;

$toupload{$description_file_xml}=$description_file;




############
# Make a random number as an upload id
my $rid=sprintf('up%06d',int(rand(999999)));



############
# Build the description list

my $oh={};

$oh->{RandomId}=$rid;
$oh->{UnparsedCommandLine}={content => $unparsed_cli};
$oh->{UploadedFiles}={file => [values %toupload] };
$oh->{TimeStamp}={Unix   => time,
		  String => scalar localtime};
$oh->{ClientRevision}='$Revision: 356 $';
$oh->{LoginName}=$user;

$oh->{Email}=$email;
$oh->{Group}=$group;
$oh->{Name}=$name;

$oh->{Template}=$app_name;
$oh->{AssignUserAll}=$assign_user_all;
$oh->{NumSteps}=$num_steps;
$oh->{Priority}=$priority;
$oh->{FPopsEst}=$fpops_est;
$oh->{FPopsBound}=$fpops_bound;
$oh->{MemoryBound}=$memory_bound;

$oh->{TargetNResults}=$target_nresults;
$oh->{Balance}=$balance;
$oh->{MinQuorum}=$min_quorum;
$oh->{Batch}=$batch;

$oh->{DryRun}=$dry_run;



my $desc= XMLout($oh, RootName => XMLROOT, AttrIndent => 1 );
print $description_file_fh $desc;
close $description_file_fh;

logInfo("Description file:\n$desc\n",1);





##############
# Make the DAV connection

logInfo("Requesting the  DAV address");
my $dav_url=getDavUrl("$url/$cgi_retrieve");

logInfo("Connecting to DAV server");
my $dav = new HTTP::DAV;
# optionally: set credentials $dav->credentials
$dav->open( -url=> $dav_url )
    or die("Couldn’t open $dav_url: " .$dav->message . "\n");

# optionally: lock

# Create a directory and cd in it
$dav->mkcol($rid);
$dav->cwd($rid);




############
# Upload  files

logInfo("Uploading files in dir $rid.");

foreach my $upfile (keys %toupload) {
    $dav->put(-local=>$toupload{$upfile},
	      -url=>$upfile);
    # add error checking
}




############
# Remove the description file - not necessary: it is handled by UNLINK and tempfile()

# unlink $description_file or die "Error removing temporary description file: $!";




############
# Call the remote endpoint

logInfo("Invoking remote CGI");

my $ua = new LWP::UserAgent; 
my $cgi_url = "$url/$cgi_submit";
my $response = $ua->post( $cgi_url,
			  { random_id => $rid,
			    # more parameters here if needed
			  });
# error will be checked after deleting the temporary directory
my $content = $response->content; 
logInfo("Response received:\n$content",1);


############
# Delete uploaded dir

logInfo("Deleting temporary DAV directory");

$dav->cwd("..");
$dav->delete($rid);


############
# Die if the response was an error

if(! $response->is_success) {
    my $stat= $response->status_line;
    print STDERR "The remote web server returned an error: $stat\n\n";
    exit 1;
}


############
# Parse submission status

my $server_xml=XMLin($content);

if($server_xml->{Failure}) {
    print STDERR "The server CGI returned an error. Reason: $server_xml->{Failure}->{Reason}\n";
    exit 1;
} 



############
# Print submission status

my $server_id=$server_xml->{Identifier};
my $server_exitcode=$server_xml->{Return}->{ExitCode};
my $server_stdout=$server_xml->{Return}->{StdOut};
my $server_stderr=$server_xml->{Return}->{StdErr};

print <<EOF;


Submission complete.

Stdout
------
$server_stdout

Stderr
------
$server_stderr
EOF

if($server_exitcode) {
	print "\n\nDetailed error information follows.\n";
	print XMLout($server_xml->{Return}, 
		     RootName => "Return",
		     AttrIndent => 1 );
}


exit $server_exitcode;




# ==================================================
# Auxiliary functions


# Check if the calling environment has all the given variables
# defined.  If not, print one of them. Else, return false. These are
# passed as string in order to be able to be able to print their name.
# Sadly, must be duplicated because otherwise does not have access to
# scope.

sub checkMandatoryArguments {
    my $l=shift;
    foreach my $f (@$l) {
	if(! eval("defined \$$f")) {
	    print STDERR "Missing mandatory argument: $f. See -help.\n";
	    return 0;
	}
    }
    return 1;
}



# do this: { a=s => x }  -->  { a => x }

sub copy_fix_options {
    my $q=shift;
    my $r={};
    foreach my $k (keys %$q) {
	my ($oname,$otype)=split(/=/,$k);
	$r->{$oname}=$q->{$k};
    }
    return $r;
}



sub printParametersHelp {
    my $t=shift;

    my $p=parseWuTemplate($t);

    print "\n";
    print "Remote application queue `$app_name'\n";
    print "Description: $t->{WuTemplate}->{rboinc}->{description}\n";
    print "Application on server: `$t->{WuTemplate}->{rboinc}->{application}'\n";
    print "\n";
    print "Options defined for this application queue:\n";
    foreach my $pn (sort keys %$p) {
	print "    -$pn\t\t";
	if($p->{$pn}->{optional}) {
	    print "(optional) ";
	}
	print $p->{$pn}->{description}."\t";
	print "\n";
    }
    print "\n";
}




# ==================================================


# for sending requests: http://snippets.dzone.com/posts/show/3163


sub printHelp {
    print <<EOF;
Boinc remote submission. Usage: boinc_submit [OPTION]...

**  SEE -help_parameters FOR SIMULATION PARAMETERS  **

Authentication:
    -url       URL       RBoinc URL contact point (*)
    -app_name  APP       Remote application     [$app_name]
    -user      NAME      Override username      [$user]
    -email     ADDRESS   (Not implemented)
    -authenticator ID    (Not implemented)

Simulation identifier (mandatory):
    -group     ID        The simulation group 
    -name      ID        The individual job ID

Scheduling:
    -metadata_file FILE  Additional metadata file
    -assign_user_all ID	 Assign to user		[$assign_user_all]
    -num_steps   NUM     Number of steps to run [$num_steps]
    -priority    NUM     BOINC priority level   [$priority]

Resources:
    -fpops_est   FLOAT   Estimated FP count     [$fpops_est]
    -fpops_bound FLOAT   Max FP count           [$fpops_bound]
    -memory_bound FLOAT  Memory requirement     [$memory_bound]
    -target_nresults NUM Redundancy             [$target_nresults]
    -balance     ID      Load balancing alg. (on=1)
    -min_quorum  NUM     (Not implemented)
    -dry_run             Upload, but not start WUs
    -batch       NUM     Batch number           [$batch]

Miscellaneous:
    -help_parameters     Show list of parameters, ie files to be
                         transferred (depending on the application)
    -help                This message
    -verbose             Be verbose

(*) You can also use the RBOINC_URL environment variable.
    For example: http://www.ps3grid.net:8383/rboinc_cgi

EOF
}




