boinc/rboinc/server/boinc_retrieve_server.pl

739 lines
16 KiB
Perl
Executable File

#!/usr/bin/perl -w
# $Id$
# The remote-boinc server-side perl script. Should run as a CGI in an
# Apache2 instance.
=head1 NAME
boinc_submit_server.pl - CGI for handling incoming remote
boinc submissions
=head1 SYNOPSIS
To be installed in Apache's cgi-bin directory
=head1 DESCRIPTION
This documentation is provisional.
Currently receives GET requests with two arguments, "action" and
"group". The script authenticates the user, goes in the workflow
directory, checks if the user owns the dir named "group". If so,
generates a random sting, creates a dir with the said name in the DAV
area, and therein creates links to all the files which can be
downloaded. The CGI returns the random id name.
=head1 AUTHOR
Toni Giorgino
=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
# ----------------
# Prepend non-standard location for modules
use lib qw(/home/boinc/toni/perl/lib);
# Prepend location for support modules
#use lib qw(/home/toni/work/RemoteBoincProject/server);
# ----------------
use strict;
use CGI::Lite; # parsing requests
use CGI::Carp; # for error messages
use XML::Simple; # to process xml easily
use File::Copy;
use File::Path;
use File::Basename;
use Cwd;
use Error qw(:try);
require qw(boinc_authentication.pl);
# ----------------
# System-derived variables and constants
my $response_header="Content-type: text/plain\n\n";
my $cgidir=dirname($ENV{SCRIPT_FILENAME});
my $xmlroot = "RetrieveStatus";
my $xml_description_file="description_file.xml";
# ----------------
# Provisional variables: will soon be made global. Underscores are
# important because a user could otherwise submit a job named `pool'
my $pool_dirname="pool";
my $delete_dirname="rboinc_trash_dir";
my $process_stop="force-process-stop";
my $process_vars="process.vars";
my $safepurge=0;
# ----------------
# Provisional variables: will soon be replaced by true variables
# passed by the client via the description mechanism
my $temporary_boinc_email='nobody@nil.es';
# -----------------
# Set configuration variables
carp "------------------------------------------------------";
our $config=do "$cgidir/boinc_configuration.pl";
if(!$config) {
die "Can't open configuration: $!";
}
my $remove_min_age=$config->{REMOVE_MIN_AGE} || 1.0;
# -----------------
# Begin the actual program
# Prepare the CGI machinery for parsing input
my $cgi = new CGI::Lite;
$cgi->add_timestamp(0);
my $action;
# Receive the call and get the arguments
do { # ???
try {
my $form = $cgi->parse_form_data();
die ("Error receiving data: ".$cgi->get_error_message)
if $cgi->is_error;
$action=$form->{action};
if(! defined $action || $action eq '') {
# Simple answer if no random-id provided (direct call)
voidAnswer();
} elsif ($action eq 'get_dav_url') {
handleGetDavUrl();
} elsif ($action eq 'get_wu_template') {
# action slightly misnomed - will get both
handleGetTemplate($form->{application});
} elsif ($action eq 'retrieve') {
my $form_group=$form->{group};
my $form_name=$form->{name};
my $form_loginname=$form->{loginname};
my $userpfx=(uc "$form_loginname")."_";
handleRetrieve($userpfx.$form_group,$form_name);
} elsif ($action eq 'remove') {
handleRemove($form->{dir});
} elsif ($action eq 'purge') {
my $form_group=$form->{group};
my $form_loginname=$form->{loginname};
my $userpfx=(uc "$form_loginname")."_";
handlePurge($userpfx.$form_group,$form->{name});
} elsif ($action eq 'stop') {
my $form_group=$form->{group};
my $form_loginname=$form->{loginname};
my $userpfx=(uc "$form_loginname")."_";
handleStop($userpfx.$form_group);
} elsif ($action eq 'status') {
my $form_group=$form->{group};
my $form_loginname=$form->{loginname};
my $userpfx=(uc "$form_loginname")."_";
handleStatus($userpfx.$form_group);
} elsif ($action eq 'gridstatus') {
my $form_loginname=$form->{loginname};
handleGridStatus($form_loginname);
} else {
returnFailure($xmlroot,"Unsupported action $action");
}
} catch Error with {
my $ex=shift;
my $mess=$ex->text();
carp "Exception: ($action error) $mess";
returnFailure($xmlroot,"($action error) $mess");
}
};
exit(0);
##################################################
sub handleGetDavUrl {
my $oh={};
$oh->{DavUrl}=$config->{DAV_URL};
$oh->{ServerRevision}='$Revision: 736 $';
my $xr=XMLout($oh, RootName => $xmlroot, AttrIndent => 1);
print $response_header;
print $xr;
}
##################################################
# Get wu and result templates
sub handleGetTemplate {
my $app=shift;
my $thash=parse_wu_template($app);
my $rhash=parse_result_template($app);
my $oh={
WuTemplate => $thash,
ResultTemplate => $rhash,
ServerRevision => '$Revision: 736 $'
};
my $xr=XMLout($oh, RootName => $xmlroot, AttrIndent => 1);
print $response_header;
print $xr;
}
##################################################
# Prevent the WU from spawning more work
sub handleStop {
my $group=shift;
my $wd=$config->{WORKFLOW_DIR};
my $groupdir="$wd/$group";
die "invalid tag supplied" if(! isTagValid($group) );
checkPoolDir($groupdir);
chdir($groupdir);
die "Already stopped" if(-e $process_stop);
system("touch $process_stop");
sendSuccess("Group successfully marked as stopped, can be purged after assimilation.");
}
##################################################
# Remove the WU and its administrative files.
sub handlePurge {
my $group=shift;
my $name=shift;
my $owd=cwd;
my $wd=$config->{WORKFLOW_DIR};
my $groupdir="$wd/$group";
my $trashdir="$wd/$delete_dirname";
die "invalid tag supplied" if(! isTagValid($group) ||
($name && isNameReserved($name)) ||
($name && !isTagValid($name) )
);
checkPoolDir($groupdir);
my $size='';
if(!$name) {
# Purge whole GROUP
chdir($groupdir);
if($safepurge) {
my @inprog=glob("*/in_progress");
die "there are jobs in progress: @inprog" if(@inprog);
}
chdir($wd);
$size=`du -h -s $group`;
safeRemove($groupdir) or die "move failed with $!";
} else {
# Purge GROUP/NAME
die "job $name in group $group does not exist"
if(! -d "$groupdir/$name");
die "job $name in group $group still in progress"
if(-e "$groupdir/$name/in_progress" && $safepurge);
chdir($groupdir);
$size=`du -h -s $name`;
safeRemove($name) or die "move failed with $!";
}
sendSuccess("Freed $size on server");
}
##################################################
# Remove the files from the server. This means that links in DAV will
# become broken. In this case, we also have to remove them from DAV,
# because apache DAV won't remove broken symlinks. Metadata files are
# never removed.
sub handleRemove {
my $dir=shift;
my $retrdir=$config->{DAV_DIR}."/$dir/";
# List of symlinks to original files
my @llist=glob("$retrdir/*");
my $nremoved=0;
my $nkept=0;
my $keepalive_every=20;
print $response_header;
foreach my $f (@llist) {
next if($f =~ /metadata_file/);
print "\n" if( ($nremoved+$nkept) % $keepalive_every == 0);
my $ldest=readlink($f);
if(canRemove($ldest) ) {
safeRemove($ldest);
unlink($f); # remove now-broken symlink
$nremoved++;
} else {
$nkept++; # the client will remove it
}
}
sendRemoveSuccess($nremoved,$nkept);
}
# Return true if conditions are met for the given file name to be
# removed
sub canRemove {
my $fn=shift;
my $removable=laterStepsExist($fn) ||
isLastStep($fn);
return($removable);
}
# True if given file name is newer than $remove_min_age
sub isOldAge {
my $fn=shift;
my $age=(-M $fn); # days
return ($age > $remove_min_age);
}
# True if given file name is at its latest step
sub isLastStep {
my $fn=shift;
my $rc=0;
if (stepOf($fn)==maxStepOf($fn)-1) {
$rc=1;
}
return $rc;
}
# Return true if there are more recent steps in the series of
# the given file name
sub laterStepsExist {
my $fn=shift;
my $s=stepOf($fn);
my $gl=mkStepGlob($fn);
my @ex=glob($gl); # list of files in the same dependency
# series as the removee
my $canremove=0;
foreach my $i (@ex) {
if(stepOf($i)>$s) {
$canremove=1;
last;
}
}
return $canremove;
}
# Make a glob to match all the results of this step.
# GPUGRID-specific.
# Ex. /path/N2-TONI_TONIR6-5-10-RND7389_0 -> /path/N2-TONI_TONIR6-*-10-RND7389_0
sub mkStepGlob {
my $fn=shift;
my $path=dirname($fn);
my @p=split(/-/,basename($fn));
$p[2]="*";
my $gl=join '-',@p;
return "$path/$gl";
}
# This one does not send the header, because it must
# precede the keepalives.
sub sendRemoveSuccess {
my $nr=shift;
my $nk=shift;
my $r={};
$r->{Success}->{NumberRemoved}=$nr;
$r->{Success}->{NumberKept}=$nk;
$r->{ServerRevision}='$Revision: 736 $';
my $xr=XMLout($r, RootName => $xmlroot, AttrIndent => 1);
print $xr;
}
##################################################
sub handleRetrieve {
my $group=shift;
my $name=shift;
# Check tag validity
my $fail='';
if(! isTagValid($group)) {
$fail="Invalid group supplied";
} elsif($name && isNameReserved($name)) {
$fail="$name is a reserved name";
} elsif($name && !isTagValid($name)) {
$fail="Invalid name supplied";
}
die("$fail") if($fail);
# -----------------
# Authenticate, TODO
# -----------------
# Check if the TAG dir exists (dies on failure)
# TODO fix expected output counts
# http://www.pageresource.com/cgirec/ptut18.htm
my $groupdir=$config->{WORKFLOW_DIR}."/$group";
# disabled machinery for computing expected outputs
my $expouts=-1;
my $metadata=0;
# will die on error
checkPoolDir($groupdir);
# -----------------
# Make a random id and corresponding dl dir
my $random_id=sprintf('retr%06d',int(rand(999999)));
my $retrdir=$config->{DAV_DIR} . "/$random_id/";
mkdir $retrdir,0777 or die "Can't make retrieve dir: $!";
# -----------------
# TODO: find files to be downloaded
my @flist;
my $aliastable;
if($name) { # name given
@flist=glob("$groupdir/$name-$group-*");
$aliastable=getAliasTable($name,$group);
if(-r "$groupdir/$name/metadata_file") {
push @flist,"$groupdir/$name/metadata_file";
$metadata++;
}
} else { # name not given
@flist=glob("$groupdir/*-$group-*");
my $tmp=parseResultName(basename($flist[0])); # NAME of first retrieved item
$aliastable=getAliasTable($tmp->{name},$group);
}
if(! scalar @flist) {
rmdir($retrdir);
returnFailure($xmlroot,"No files ready for retrieval");
return;
}
my @blist=(); # basenames
foreach my $f (@flist) {
my $bn=basename($f);
push @blist,$bn;
symlink $f,"$retrdir/$bn";
}
# Send response
my $r={};
$r->{Success}->{Directory}=$random_id;
$r->{Success}->{FinalOutputs}=$expouts;
$r->{Success}->{MetadataFileCount}=$metadata;
$r->{AliasTable}={File=>$aliastable};
$r->{FileList}={File=>\@blist};
$r->{ServerRevision}='$Revision: 736 $';
my $xr=XMLout($r, RootName => $xmlroot, AttrIndent => 1);
print $response_header;
print $xr;
}
# Make alias table for given group, name (from template)
sub getAliasTable {
my $name=shift;
my $group=shift;
my $groupdir=$config->{WORKFLOW_DIR}."/$group";
my $desc=XMLin("$groupdir/$name/$xml_description_file");
my $app=$desc->{Template};
my $tpl=parse_result_template($app);
my @ata=();
my $i=0;
while ($tpl->{file_info}->[$i]) {
my $ext="_$i";
my $aliases=$tpl->{file_info}->[$i]->{rboinc}->{aliases};
my @alist=split(' ',$aliases);
push(@ata, { Extension => $ext,
Alias => \@alist } );
$i++;
}
return \@ata;
}
##################################################
sub handleStatus {
my $group=shift; # the group name
my $r=currentSteps($group);
die "invalid tag supplied" if(! isTagValid($group));
sendSuccess("Returning count of max-steps computed",{Success=>{StepList=>$r}});
}
# Return the list of bins in this group (based on those who
# have a process.vars)
sub currentNamesInGroup {
my $group=shift; # the group name
my $gd=$config->{WORKFLOW_DIR}."/$group";
checkPoolDir($gd);
my @plist=glob("$gd/*/$process_vars");
my @nlist=();
# for each process_vars, take the last element of the path
foreach my $i (@plist) {
push @nlist,basename(dirname($i));
}
return @nlist;
}
# existence of step 0 returned as 1
sub maxStepNameGroup {
my $group=shift;
my $name=shift;
my $gd=$config->{WORKFLOW_DIR}."/$group";
my $gl="$gd/$name-$group-*-*-*_0";
my @l=glob($gl);
my $mx=0;
foreach my $i (@l) {
my $n=stepOf($i)+1;
if($n>$mx) {
$mx=$n;
}
}
return $mx;
}
# Using "bin_" to workaround attribute name limitation
sub currentSteps {
my $group=shift;
my @nl=currentNamesInGroup($group);
my $r={};
foreach my $i (@nl) {
$r->{"Bin_$i"}=maxStepNameGroup($group,$i);
}
return $r;
}
##################################################
# Misc utility functions
sub handleGridStatus {
my $user=shift; # the group name
die "invalid user supplied" if(! isUserValid($user));
my $cmd=<<"EOL";
echo "call mon_status('$user')" | mysql -t -pc0c4c0la LUNA
EOL
my $list=`$cmd`;
sendSuccess("Returning grid status",{Success=>{content=>$list}});
}
##################################################
# Misc utility functions
# Assert that the TAG dir exists (dies on failure). Will return a
# void.
sub checkPoolDir {
my $wd=shift;
my $res=opendir(WD, $wd);
closedir WD;
# No special cleanup required
die("Group does not exist") if(!$res);
my $pfn=$wd."/$pool_dirname";
die("Not a remotely-submitted job") if(! -d $pfn );
}
# Extract the step num from a given result filename
# GPUGRID-specific.
# Ex. /path/N2-TONI_TONIR6-5-10-RND7389_0 -> 5
sub stepOf {
my $fn=basename(shift);
my @p=split(/-/,$fn);
return $p[2];
}
# /path/N2-TONI_TONIR6-5-10-RND7389_0 -> 10
sub maxStepOf {
my @p=split(/-/,basename(shift));
return $p[3];
}
# Remove a dir or file, moving it into the trash dir (renamed with an
# unique suffix). I use "system mv" because move() has quirks on
# handling directory moves. E.g. move("/dir1/dir2/dir3","/dir4")
# fails unless "dir3" eq "dir4". Should be changed in a two-step
# move()
sub safeRemove {
my $fn=shift;
my $wd=$config->{WORKFLOW_DIR};
my $t=time(); # TODO improve
my $trashdir="$wd/$delete_dirname/$t";
mkpath($trashdir);
system("/bin/mv $fn $trashdir") == 0
or die "Error moving: $!";
}
# Generic "success" function - mandatory message, optional
# hash of additional stuff to be transferred
sub sendSuccess {
my $m=shift;
my $r=shift || {};
$r->{Success}->{Message}=$m;
$r->{ServerRevision}='$Revision: 736 $';
my $xr=XMLout($r, RootName => $xmlroot, AttrIndent => 1);
print $response_header;
print $xr;
}