mirror of https://github.com/BOINC/boinc.git
739 lines
16 KiB
Perl
Executable File
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;
|
|
}
|
|
|