mirror of https://github.com/BOINC/boinc.git
590 lines
12 KiB
Perl
Executable File
590 lines
12 KiB
Perl
Executable File
#!/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 Getopt::Long qw(:config ignore_case auto_help);
|
|
use XML::Simple;
|
|
use HTTP::Request::Common qw(POST);
|
|
use HTTP::DAV;
|
|
use LWP::UserAgent;
|
|
use File::Basename;
|
|
use File::Spec;
|
|
use Pod::Usage;
|
|
use Error qw(:try);
|
|
|
|
use constant XMLROOT => "BoincRemote";
|
|
|
|
require qw(boinc_lib.pl);
|
|
|
|
|
|
|
|
# Constant, relative to url
|
|
my $cgi_retrieve="boinc_retrieve_server.pl";
|
|
|
|
|
|
|
|
# ----------------
|
|
# Parsing command line
|
|
|
|
my $group='';
|
|
|
|
my $name='';
|
|
my $into='',
|
|
my $intotree='',
|
|
our $verbose='';
|
|
my $quiet='';
|
|
my $keep='';
|
|
my $purge='';
|
|
my $stop='';
|
|
my $status='';
|
|
my $gridstatus='';
|
|
my $help='';
|
|
|
|
my $url=$ENV{RBOINC_URL};
|
|
my $user=$ENV{USER};
|
|
my $email='';
|
|
my $authenticator='';
|
|
|
|
|
|
|
|
GetOptions(
|
|
'group=s' => \$group,
|
|
|
|
'name=s' => \$name,
|
|
'into=s' => \$into,
|
|
'intotree=s' => \$intotree,
|
|
'verbose' => \$verbose,
|
|
'quiet' => \$quiet,
|
|
'keep' => \$keep,
|
|
'purge' => \$purge,
|
|
'stop' => \$stop,
|
|
'status' => \$status,
|
|
'gridstatus' => \$gridstatus,
|
|
'help' => \$help,
|
|
|
|
'url=s' => \$url,
|
|
'user=s' => \$user,
|
|
'email=s' => \$email,
|
|
'authenticator=s' => \$authenticator,
|
|
) or die "Error parsing command line";
|
|
|
|
|
|
pod2usage(1) if $help;
|
|
|
|
|
|
|
|
# ----------------
|
|
# Check arguments
|
|
|
|
checkMandatoryArguments(["group","url"]) or exit 1;
|
|
my $cgi_url = "$url/$cgi_retrieve";
|
|
|
|
if($intotree) {
|
|
$into=$intotree;
|
|
}
|
|
|
|
|
|
|
|
# ----------------
|
|
# Authentication TODO
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------
|
|
# Remote action invocation
|
|
|
|
if($purge) {
|
|
handlePurge();
|
|
} elsif($stop) {
|
|
handleStop();
|
|
} elsif($status) {
|
|
handleStatus();
|
|
} elsif($gridstatus) {
|
|
handleGridStatus();
|
|
} else {
|
|
handleRetrieve();
|
|
}
|
|
|
|
exit(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
########################################
|
|
# Handle purge action
|
|
|
|
|
|
sub handlePurge {
|
|
|
|
if($into) {
|
|
print STDERR "Action --purge deletes only. It makes no sense in combination with --into.\n";
|
|
exit 1;
|
|
}
|
|
|
|
confirmOrDie("The operation will IRREVERSIBLY delete results from the server.\n".
|
|
"Note: You won't be able to submit new WUs with equal names until old ones\nwill be pending in the server (check with -gridstatus).\nConfirm? ");
|
|
my $xmlcontent=invokeRMI({action=>'purge',group=>$group,name=>$name});
|
|
my $message=$xmlcontent->{Success}->{Message};
|
|
print "Success. Message from server: $message\n";
|
|
}
|
|
|
|
|
|
|
|
|
|
########################################
|
|
# Handle stop action
|
|
|
|
|
|
sub handleStop {
|
|
|
|
if($into || $name) {
|
|
print STDERR "Action --stop makes no sense in combination with --into or --name.\n";
|
|
}
|
|
|
|
confirmOrDie("The operation will IRREVERSIBLY stop the WU. Results can still be retrieved. Confirm? ");
|
|
my $xmlcontent=invokeRMI({action=>'stop',group=>$group});
|
|
my $message=$xmlcontent->{Success}->{Message};
|
|
print "Success. Message from server: $message\n";
|
|
}
|
|
|
|
|
|
|
|
|
|
########################################
|
|
# Handle status action
|
|
|
|
|
|
sub handleStatus {
|
|
|
|
if($into || $name) {
|
|
print STDERR "Action --status makes no sense in combination with --into or --name.\n";
|
|
}
|
|
|
|
my $xmlcontent=invokeRMI({action=>'status',group=>$group});
|
|
my $message=$xmlcontent->{Success}->{Message};
|
|
print "Success. Message from server: $message\n";
|
|
|
|
my $steps=$xmlcontent->{Success}->{StepList};
|
|
my %st=%$steps;
|
|
foreach my $n (keys %st) {
|
|
my $nn=$n; # strip "bin_"
|
|
$nn=~s/^Bin_//;
|
|
print "$nn\t".$st{$n}."\n";
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
########################################
|
|
# Handle status action
|
|
|
|
|
|
sub handleGridStatus {
|
|
|
|
if($into || $name) {
|
|
print STDERR "Action --gridstatus makes no sense in combination with --into or --name.\n";
|
|
}
|
|
|
|
my $xmlcontent=invokeRMI({action=>'gridstatus'});
|
|
my $message=$xmlcontent->{Success}->{Message};
|
|
print "Success. Message from server: $message\n";
|
|
|
|
my $list=$xmlcontent->{Success}->{content};
|
|
print "$list\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
########################################
|
|
# Handle retrieve action
|
|
|
|
# TODO -intotree, rename
|
|
|
|
|
|
sub handleRetrieve {
|
|
|
|
# ----------------
|
|
# Change dir and fail early
|
|
if($into) {
|
|
chdir $into or do {
|
|
print STDERR "Cannot chdir to -into directory `$into': $!\n";
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
# ----------------
|
|
# Invoke RMI
|
|
my $xmlcontent=invokeRMI({action=>'retrieve',group=>$group,name=>$name});
|
|
|
|
# ----------------
|
|
# Check outcome
|
|
my $rfilelist=$xmlcontent->{FileList}->{File};
|
|
if(!$rfilelist) {
|
|
die "No files ready for retrieval.\n";
|
|
}
|
|
|
|
my $dav_dir=$xmlcontent->{Success}->{Directory};
|
|
if(!$dav_dir) {
|
|
die "Error requesting download location";
|
|
}
|
|
|
|
my $aliasTable=$xmlcontent->{AliasTable};
|
|
my $finalOutputs=$xmlcontent->{Success}->{FinalOutputs};
|
|
my $nMeta=$xmlcontent->{Success}->{MetadataFileCount};
|
|
|
|
|
|
# ----------------
|
|
# Download
|
|
logInfo("Requesting the DAV address");
|
|
my $dav_url=getDavUrl($cgi_url);
|
|
|
|
logInfo("Connecting to DAV server");
|
|
my $dav = new HTTP::DAV;
|
|
$dav->open( -url=> $dav_url )
|
|
or die("Couldn't open $dav_url: " .$dav->message . "\n");
|
|
$dav->cwd($dav_dir)
|
|
or die("Couldn't set remote directory $dav_dir: " .$dav->message . "\n");
|
|
|
|
my $ndone=0;
|
|
my $nskip=0;
|
|
my @skiplist=();
|
|
my $nexpected=scalar @$rfilelist;
|
|
foreach my $fn (@$rfilelist) {
|
|
my $targetfn=$fn;
|
|
if($intotree) {
|
|
my $tmp=parseResultName($fn);
|
|
my $name=$tmp->{name};
|
|
mkdir $name; # may well fail if dir exists
|
|
$targetfn=File::Spec->catfile($name,$fn); # OS indepen
|
|
}
|
|
if( fileOrAliasExists($targetfn,$aliasTable) ) {
|
|
if(!$quiet && $nskip==0) {
|
|
print "Warning: some files are present locally and will not be overwritten.\n";
|
|
}
|
|
$nskip++;
|
|
push @skiplist,$fn;
|
|
} else {
|
|
$targetfn = $targetfn . aliasExtension($fn,$aliasTable);
|
|
$dav->get(-url => $fn,
|
|
-to => $targetfn) and
|
|
$ndone++;
|
|
}
|
|
if(!$quiet) {
|
|
local $|=1;
|
|
print sprintf("Retrieved $ndone, already present $nskip, out of $nexpected (% 3d%%)\r",100.*($ndone+$nskip)/$nexpected);
|
|
}
|
|
}
|
|
|
|
if($verbose) {
|
|
print "The following files were not overwritten: @skiplist\n";
|
|
}
|
|
|
|
print "Successfully retrieved $ndone, already present $nskip, out of $nexpected ($nMeta metadata).\n";
|
|
|
|
|
|
|
|
# ----------------
|
|
# Request deletion of remote files
|
|
|
|
if(! $keep) {
|
|
logInfo("Going to remove retrieved files");
|
|
try {
|
|
$xmlcontent=invokeRMI({action=>'remove',dir=>$dav_dir});
|
|
|
|
my $nremoved=$xmlcontent->{Success}->{NumberRemoved};
|
|
my $nkept= $xmlcontent->{Success}->{NumberKept} ;
|
|
|
|
if(!$quiet) {
|
|
print "Removed $nremoved server files, $nkept were too recent to remove.\n";
|
|
}
|
|
} catch Error with {
|
|
my $ex=shift;
|
|
my $mess=$ex->text();
|
|
print STDERR "Error requesting removal of remote files: $mess; continuing.\n";
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# ----------------
|
|
# Finalize
|
|
|
|
logInfo("Removing retrieval directory");
|
|
foreach my $fn (@$rfilelist) {
|
|
$dav->delete(-url => $fn);
|
|
}
|
|
$dav->cwd("..");
|
|
$dav->delete($dav_dir);
|
|
|
|
}
|
|
|
|
|
|
|
|
# Return the first extension defined in the alias table
|
|
|
|
sub aliasExtension {
|
|
my $fn=shift;
|
|
my $at=shift;
|
|
|
|
# Extract trailing number
|
|
$fn=~/_([0-9]+)$/;
|
|
my $fileExt=$1;
|
|
|
|
my $newExt="";
|
|
|
|
# iterate over the alias list
|
|
my $rAliasList=$at->{File};
|
|
foreach my $curAlias ( @$rAliasList ) {
|
|
if($curAlias->{Extension} eq "_$fileExt") {
|
|
# if alias for current extension, return first Alias
|
|
$newExt=$curAlias->{Alias}->[0];
|
|
logInfo("Found suffix $fileExt -> $newExt");
|
|
last;
|
|
}
|
|
}
|
|
return $newExt;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Check if file exists in current directory (1st arg), checking both
|
|
# the literal file name and its aliases, appending extensions
|
|
# described in the alias list (2nd arg)
|
|
|
|
sub fileOrAliasExists {
|
|
my $fn=shift;
|
|
my $at=shift;
|
|
|
|
logInfo("Testing existence of $fn...");
|
|
if(-e $fn) {
|
|
logInfo("...is there");
|
|
return 1;
|
|
}
|
|
|
|
# Extract trailing number
|
|
$fn=~/_([0-9]+)$/;
|
|
my $fileExt=$1;
|
|
|
|
# iterate over the alias list
|
|
my $rAliasList=$at->{File};
|
|
foreach my $curAlias ( @$rAliasList ) {
|
|
if($curAlias->{Extension} eq "_$fileExt") {
|
|
# if alias for current extension, check appending all extensions
|
|
my $rExtList=$curAlias->{Alias};
|
|
foreach my $extToTest (@$rExtList) {
|
|
logInfo("Testing existence of $fn + $extToTest...");
|
|
if(-e $fn.$extToTest) {
|
|
logInfo("...is there");
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ########################################
|
|
# Misc. utility functions, shared by all handlers
|
|
|
|
|
|
|
|
# ----------------
|
|
# Ask for positive confirmation, or throw exception.
|
|
|
|
sub confirmOrDie {
|
|
my $answer=promptUser(shift,"N");
|
|
die "Operation aborted." if($answer !~ /^[yY]/ );
|
|
}
|
|
|
|
|
|
|
|
|
|
# ----------------
|
|
# Performs the remote method invokation, die-ing on failure
|
|
# Returns a parsed XML structure or throws an exception.
|
|
# Will use the $user global variable
|
|
|
|
sub invokeRMI {
|
|
my $params=shift;
|
|
|
|
logInfo("Invoking CGI");
|
|
|
|
$params->{loginname}=$user;
|
|
|
|
my $xmlcontent;
|
|
my $ua = new LWP::UserAgent;
|
|
my $response = $ua->post( $cgi_url,
|
|
$params );
|
|
if($response->is_success) {
|
|
my $content = $response->content;
|
|
if($verbose) {
|
|
print "Response received:\n";
|
|
print $content;
|
|
}
|
|
$xmlcontent=XMLin($content,ForceArray=>["File","Alias"]);
|
|
} else {
|
|
my $reason=$response->status_line;
|
|
die "Error in POST from remote: $reason. Server may be down.\n";
|
|
}
|
|
|
|
if($xmlcontent->{Failure}) {
|
|
die "Server error message: $xmlcontent->{Failure}->{Reason}";
|
|
} elsif(!$xmlcontent->{Success}) {
|
|
die "Undefined state returned. This is a bug.";
|
|
}
|
|
|
|
return($xmlcontent);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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('$'."$f")) {
|
|
print STDERR "Missing mandatory argument: $f. See -help.\n";
|
|
return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__END__
|
|
|
|
=pod
|
|
=head1 NAME
|
|
|
|
boinc_retrieve - Retrieving and administering remote boinc jobs
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
boinc_retrieve [options]
|
|
|
|
|
|
=head1 OPTIONS
|
|
|
|
=head2 Mandatory parameters
|
|
|
|
=begin text
|
|
|
|
-group GROUP The simulation group to be retrieved
|
|
|
|
=end text
|
|
|
|
=head2 Modifiers
|
|
|
|
=begin text
|
|
|
|
-name NAME Retrieve only a specific job and its metadata
|
|
-into DIR Put files into specified directory (default ".")
|
|
-intotree DIR Put files into specified directory, with NAMEs in subdirs
|
|
-verbose Be verbose
|
|
-quiet Hide download progress indicator
|
|
-keep Do not remove retrieved files from server
|
|
-purge Completely remove GROUP from server (if finished)
|
|
-stop Prevent more work to be spawned for a given group
|
|
-status Show step numbers for given group
|
|
-gridstatus Show resources consumed and statuses for all groups
|
|
-help This message
|
|
|
|
=end text
|
|
|
|
=head2 Authentication
|
|
|
|
=begin text
|
|
|
|
-url URL RBoinc URL contact point (*)
|
|
-user NAME Override username [$user]
|
|
-email ADDRESS (Not implemented)
|
|
-authenticator ID (Not implemented)
|
|
|
|
(*) You can also use the RBOINC_URL environment variable
|
|
For example: http://www.ps3grid.net:8383/rboinc_cgi
|
|
|
|
=end text
|
|
|
|
|
|
|
|
=head1 SEE ALSO
|
|
|
|
L<boinc_submit>
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
Toni Giorgino
|
|
|
|
=cut
|
|
|