#!/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 . =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; }