#!/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 .
=cut
# $Id: boinc_submit.pl 356 2010-03-02 15:00:31Z toni $
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 <{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 <