#!/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$ 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 =head1 AUTHOR Toni Giorgino =cut