]> git.uio.no Git - u/mrichter/AliRoot.git/blob - PWGPP/benchmark/alirelval
827384d69ce6d0f550560a24b6fe6a0b433a6f57
[u/mrichter/AliRoot.git] / PWGPP / benchmark / alirelval
1 #!/bin/bash
2
3 #
4 # launch-relval.sh -- by Dario Berzano <dario.berzano@cern.ch>
5 #
6 # Controls the release validation submission by managing the validation virtual
7 # cluster.
8 #
9
10 #
11 # Variables
12 #
13
14 # error codes
15 errCfg=1
16 errMissingCmd=2
17 errEc2Auth=3
18 errInvalidOpt=4
19 errSessionDir=5
20 errCreateKey=6
21 errRunVm=7
22 errLaunchValidation=8
23 errSshNotReady=9
24 errStatusUnavailable=10
25 errPickSession=11
26 errCopyKey=12
27 errAttachScreen=13
28
29 # error codes not treated as errors (100 to 140)
30 errStatusRunning=100
31 errStatusNotRunning=101
32 errStatusDoneOk=102
33 errStatusDoneFail=103
34
35 # thresholds
36 maxVmLaunchAttempts=10
37 maxSshConnectAttempts=400
38 maxVmAddressWait=120
39
40 # working directory prefix
41 sessionPrefix="$HOME/.alice-release-validation"
42
43 # screen name for the validation
44 screenName='AliceReleaseValidation'
45
46 # program name
47 Prog=$(basename "$0")
48
49 #
50 # Functions
51 #
52
53 # Pretty print
54 function pr() {
55   local nl
56   if [ "$1" == '-n' ] ; then
57     nl="-n"
58     shift
59   fi
60   echo $nl -e "\033[1m$@\033[m" >&2
61 }
62
63 # Nice date in UTC
64 function ndate() {
65   date -u +%Y%m%d-%H%M%S-utc
66 }
67
68 # Temporary file
69 function tmpf() {
70   mktemp /tmp/alirelval-XXXX
71 }
72
73 # Swallow output. Show only if something goes wrong
74 function swallow() {
75   local tout ret
76   tout=$(tmpf)
77   "$@" > "$tout" 2>&1
78   ret=$?
79   if [ $ret != 0 ] ; then
80     pr "Command failed (exit status: $ret): $@"
81     cat "$tout" >&2
82   fi
83   rm -f "$tout"
84   return $ret
85 }
86
87 # Launch a VM. Create the keypair if the given keyfile does not exist. Syntax:
88 #
89 #   RunVM <image_id> <profile> <user_data> <key_name> <key_file>
90 #
91 # Returns 0 on success, nonzero on failure. IP address is returned on stdout.
92 function RunVM() {
93   local imageId profile userData keyName
94   imageId="$1"
95   profile="$2"
96   userData="$3"
97   keyName="$4"
98   keyFile="$5"
99   local raw iip iid ret attempt createdKeypair error
100
101   # keypair part: if file does not exist, create keypair
102   if [ ! -e "$keyFile" ] ; then
103     pr "Creating a new keypair: $keyName (private key: $keyFile)"
104     swallow euca-create-keypair -f "$keyFile" "$keyName"
105     if [ $? != 0 ] ; then
106       pr 'Problems creating the keypair'
107       return $errCreateKey
108     fi
109     createdKeypair=1
110   fi
111
112   attempt=0
113   pr 'Attempting to run virtual machine'
114
115   # resubmit loop
116   while true ; do
117
118     if [ $((++attempt)) -gt $maxVmLaunchAttempts ] ; then
119       pr " * Reached maximum number of attempts, giving up"
120       if [ "$createdKeypair" == 1 ] ; then
121         ( euca-delete-keypair "$keyName" ; rm -f "$keyFile" ) > /dev/null 2>&1
122       fi
123       return $errRunVm
124     fi
125
126     pr -n " * Launching VM (attempt #$attempt/$maxVmLaunchAttempts)..."
127     error=0
128
129     raw=$( euca-run-instances "$imageId" -t "$profile" -d "$userData" -k "$keyName" 2>&1 )
130     ret=$?
131     iid=$( echo "$raw" | egrep '^INSTANCE' | head -n1 | awk '{ print $2 }' )
132     if [ $ret != 0 ] || [ "$iid" == '' ] ; then
133       # 'hard' error, but can be temporary
134       pr 'error: message follows'
135       echo "$raw" >&2
136       sleep 1
137       continue
138     else
139       pr 'ok'
140     fi
141
142     pr " * VM has instance ID $iid"
143     pr -n " * Waiting for IP address..."
144
145     # wait for address loop
146     iip=''
147     for ((i=0; i<$maxVmAddressWait; i++)) ; do
148       sleep 1
149       raw=$( euca-describe-instances 2>&1 | grep -E '^INSTANCE' | grep "$iid" | head -n1 )
150
151       # error state?
152       echo "$raw" | grep -i error -q
153       if [ $? == 0 ] ; then
154         pr ; pr " * VM went to error state"
155         error=1
156         break
157       fi
158
159       # no error: try to parse address (NOTE: only IPv4 for the moment)
160       iip=$( echo "$raw" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' )
161       if [ "$iip" != '' ] ; then
162         pr
163         break
164       fi
165
166       # no address
167       pr -n '.'
168
169     done
170
171     # do we have address?
172     if [ "$iip" != '' ] ; then
173       pr " * VM has address $iip"
174       break
175     fi
176
177     # we don't: terminate (timeout)
178     [ "$error" != 1 ] && pr 'timeout'
179     pr " * Terminating instance $iid"
180     euca-terminate-instances "$iid" > /dev/null 2>&1
181
182   done
183
184   # success
185   [ "$createdKeypair" == 1 ] && euca-delete-keypair "$keyName" > /dev/null 2>&1
186   echo "$iid $iip" # must be parsed
187   return 0
188
189 }
190
191 # Prepare the validation session directory. Syntax:
192 #
193 #   PrepareSession <aliroot_tag>
194 #
195 # Returns 0 on success, nonzero on failure. Session tag returned on stdout.
196 function PrepareSession() {
197   local aliRootTag sessionTag sessionDir
198   aliRootTag="$1"
199   shift
200   sessionTag="${aliRootTag}_$(ndate)"
201   sessionDir="$sessionPrefix/$sessionTag"
202
203   # session directory already exists? abort
204   if [ -d "$sessionDir" ] ; then
205     pr "Session directory already exists, aborting"
206     return $errSessionDir
207   fi
208
209   # create working directory
210   mkdir -p "$sessionDir"
211   if [ $? != 0 ] ; then
212     pr "Fatal: cannot create session directory $sessionDir"
213     return $errSessionDir
214   fi
215
216   # aliroot version written to a file
217   echo "$aliRootTag" > "$sessionDir/aliroot-version.txt"
218
219   # benchmark script, configuration and file list
220   cp benchmark.sh benchmark.config files.list "$sessionDir/"
221   if [ $? != 0 ] ; then
222     pr "Cannot copy benchmark configuration and script to $sessionDir"
223     return $errSessionDir
224   fi
225
226   # append local files to the configuration
227   for f in benchmark.config.d/*.config ; do
228     [ ! -e "$f" ] && continue
229     ( echo ''
230       echo "### from $f ###"
231       cat $f
232       echo ''
233     ) >> "$sessionDir/benchmark.config"
234   done
235
236   # command-line options override the configuration
237   if [ $# != 0 ] ; then
238     pr "Note: the following command-line options will override the corresponding ones in the config files:"
239     ( echo ''
240       echo "### from the command line ###"
241       while [ $# -gt 0 ] ; do
242         extraName="${1%%=*}"
243         extraVal="${1#*=}"
244         if [ "$extraName" != "$1" ] ; then
245           pr " * $extraName = $extraVal"
246           echo "$1"
247         fi
248         shift
249       done
250       echo ''
251     ) >> "$sessionDir/benchmark.config"
252   fi
253
254   # success: return the session tag and move to the session directory
255   pr "*** Creating new working session: $sessionTag ***"
256   pr "*** Use this name for future session operations ***"
257   echo "$sessionTag"
258   return 0
259 }
260
261 # Undo the previous action
262 function PrepareSession_Undo() {
263   rm -rf "$sessionPrefix/$1"
264 }
265
266 # Move into the session tag directory. Usage:
267 #
268 #   MoveToSessionDir <session_tag>
269 #
270 # Returns 0 on success, nonzero on error.
271 function MoveToSessionDir() {
272   originalWorkDir="$PWD"
273   cd "$sessionPrefix/$sessionTag" || return $errSessionDir
274   return 0
275 }
276
277 # Undo the previous action
278 function MoveToSessionDir_Undo() {
279   cd "$originalWorkDir"
280 }
281
282 # Load the benchmark configuration
283 function LoadConfig() {
284   source benchmark.config > /dev/null 2>&1
285   if [ $? != 0 ] ; then
286     pr "Cannot load benchmark configuration"
287     return $errCfg
288   fi
289   return 0
290 }
291
292 # Instantiate the validation VM
293 function InstantiateValidationVM() {
294   local sessionTag instanceId instanceIp ret raw
295   sessionTag="$1"
296
297   # check if we already have a vm
298   instanceId="$(cat instance-id.txt 2> /dev/null)"
299   if [ "$instanceId" != '' ] ; then
300     pr "Virtual machine $instanceId is already running"
301     return 0 # consider it a success
302   else
303     rm -f instance-id.txt instance-address.txt
304   fi
305
306   # do we need to create a keypair?
307   if [ "$cloudKeyName" == '' ] ; then
308     pr "Note: temporary SSH keys will be created for this VM"
309     cloudKeyName="$sessionTag"
310     cloudKeyFile="$PWD/key.pem"
311     rm -f "$cloudKeyFile"
312   elif [ -e "$cloudKeyFile" ] ; then
313     # copy key to session dir
314     pr -n "Copying private key $cloudKeyFile to session directory..."
315     cp "$cloudKeyFile" 'key.pem' 2> /dev/null
316     if [ $? != 0 ] ; then
317       pr 'error'
318       return $errCopyKey
319     else
320       pr 'ok'
321     fi
322     cloudKeyFile="$PWD/key.pem"
323   else
324     pr "Cannot find private key to access virtual machines: $cloudKeyFile"
325     return $errCopyKey
326   fi
327
328   # launch virtual machine and get its address
329   raw=$( RunVM "$cloudImageId" "$cloudProfile" "$cloudUserData" "$cloudKeyName" "$cloudKeyFile" )
330   ret=$?
331
332   if [ $ret == 0 ] ; then
333     instanceId=$( echo $raw | cut -d' ' -f1 )
334     instanceIp=$( echo $raw | cut -d' ' -f2 )
335
336     # write both parameters to files
337     echo $instanceId > 'instance-id.txt'
338     echo $instanceIp > 'instance-address.txt'
339   fi
340
341   return $ret
342 }
343
344 # Undo the previous action
345 function InstantiateValidationVM_Undo() {
346   local sessionTag
347   sessionTag="$1"
348   if [ -e 'instance-id.txt' ] ; then
349     swallow euca-terminate-instances $(cat instance-id.txt)
350     if [ $? == 0 ] ; then
351       rm -f instance-id.txt instance-address.txt key.pem
352     fi
353   fi
354 }
355
356 # Generic SSH function to the VM
357 function VMSSH() {
358   local instanceIp sshParams ret
359   instanceIp=$(cat instance-address.txt 2> /dev/null)
360   sshParams="-oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no -oPasswordAuthentication=no -i $PWD/key.pem"
361
362   if [ "$1" == '--rsync-cmd' ] ; then
363     shift
364     echo ssh $sshParams "$@"
365     ret=0
366   else
367     ssh $sshParams "$cloudUserName"@"$instanceIp" "$@"
368     ret=$?
369   fi
370   return $ret
371 }
372
373 # Opens a shell to the remote VM
374 function Shell() {
375   local sessionTag
376   sessionTag="$1"
377   VMSSH
378 }
379
380 # Checks status of the validation
381 function Status() {
382   local raw ret screen exitcode
383   raw=$( VMSSH -t "screen -ls 2> /dev/null | grep -q .$screenName && echo -n 'screen_yes ' || echo -n 'screen_no ' ; cat alirelval/validation.done 2> /dev/null || echo 'not_done' ; true" 2> /dev/null )
384   raw=$( echo "$raw" | tr -cd '[:alnum:]_ ' ) # garbage removal
385   ret=$?
386
387   if [ "$ret" != 0 ] ; then
388     pr "Cannot get status"
389     return $errStatusUnavailable
390   fi
391
392   screen="${raw%% *}"
393   exitcode="${raw#* }"
394
395   if [ "$screen" == 'screen_yes' ] ; then
396     pr 'Status: validation still running'
397     return $errStatusRunning
398   else
399     if [ "$exitcode" == 'not_done' ] ; then
400       pr 'Status: validation not running'
401       return $errStatusNotRunning
402     elif [ "$exitcode" == 0 ] ; then
403       pr 'Status: validation completed successfully'
404       return $errStatusDoneOk
405     else
406       pr "Status: validation finished with errors (exitcode: $exitcode)"
407       return $errStatusDoneFail
408     fi
409   fi
410
411 }
412
413 # Wait for host to be ready
414 function WaitSsh() {
415   local attempt error
416   attempt=0
417   pr -n 'Waiting for the VM to accept SSH connections...'
418
419   while ! VMSSH -Tq true > /dev/null 2>&1 ; do
420     if [ $((++attempt)) -gt $maxSshConnectAttempts ] ; then
421       pr 'timeout'
422       error=1
423       break
424     fi
425     pr -n '.'
426     sleep 3
427   done
428
429   [ "$error" == 1 ] && return $errSshNotReady
430   pr 'ok'
431   return 0
432 }
433
434 # Run the validation
435 function Validate() {
436   local instanceIp sshParams sessionTag
437   sessionTag="$1"
438   instanceIp=$(cat instance-address.txt 2> /dev/null)
439   sshParams="-oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no -oPasswordAuthentication=no -i $PWD/key.pem"
440
441   # create helper script to launch benchmark
442   cat > run-benchmark.sh <<_EoF_
443 #!/bin/bash
444 cd \$(dirname "\$0")
445 v=validation.done
446 rm -f \$v
447 env ALIROOT_VERSION=$(cat aliroot-version.txt) ./benchmark.sh run $sessionTag files.list benchmark.config
448 #sleep 1000
449 ret=\$?
450 echo \$ret > \$v
451 echo ; echo ; echo
452 echo "*** Validation finished with exitcode \$ret ***"
453 echo ; echo ; echo
454 read -p 'Press ENTER to dismiss: automatic dismiss in 60 seconds...' -t 60
455 _EoF_
456   chmod +x run-benchmark.sh
457
458   # transfer files
459   pr 'Transferring files to the VM'
460   rsync -av -e "$(VMSSH --rsync-cmd)" $PWD/ $cloudUserName@$instanceIp:alirelval/ || return $errLaunchValidation
461
462   # open a screen that does something; note that the command is not executed if
463   # the screen already exists, which is what we want
464   # note: sleep necessary to avoid "dead" screens
465   VMSSH -t "screen -wipe > /dev/null 2>&1 ; if screen -ls | grep -q $screenName ; then ret=42 ; else screen -dmS AliceReleaseValidation alirelval/run-benchmark.sh ; ret=0 ; sleep 3 ; fi ; exit \$ret"
466   ret=$?
467
468   # message
469   if [ $ret == 42 ] ; then
470     pr 'Validation already running inside a screen.'
471   else
472     pr 'Validation launched inside a screen.'
473   fi
474
475   pr
476   pr 'Check the progress status with:'
477   pr "  $Prog --session $sessionTag --status"
478   pr 'Attach to the screen for debug:'
479   pr "  $Prog --session $sessionTag --attach"
480   pr 'Open a shell to the virtual machine:'
481   pr "  $Prog --session $sessionTag --shell"
482   pr
483
484   # ignore ssh errors
485   return 0
486 }
487
488 # Attach current validation screen, if possible
489 function Attach() {
490   local sessionTag
491   sessionTag="$1"
492
493   VMSSH -t "( screen -wipe ; screen -rx $screenName ) > /dev/null 2>&1"
494
495   if [ $? != 0 ] ; then
496     pr "Cannot attach screen: check if validation is running with:"
497     pr "  $Prog --session $sessionTag --status"
498     pr "or connect manually to the VM for debug:"
499     pr "  $Prog --session $sessionTag --attach"
500     return $errAttachScreen
501   fi
502
503   return 0
504 }
505
506 # Pick session interactively
507 function PickSession() {
508   local sessionTag sess listSessions
509   listSessions=()
510   mkdir -p "$sessionPrefix"
511
512   while read sess ; do
513     [ ! -d "$sessionPrefix/$sess" ] && continue
514     listSessions+=( $sess )
515   done < <( cd $sessionPrefix ; ls -1t )
516
517   if [ ${#listSessions[@]} == 0 ] ; then
518     pr "No session available in session directory $sessionPrefix"
519     return $errPickSession
520   fi
521
522   pr 'Available sessions (most recent first):'
523   for ((i=0; i<${#listSessions[@]}; i++)) ; do
524     pr "$( printf "  % 2d. ${listSessions[$i]}" $((i+1)) )"
525   done
526   pr -n 'Pick one: '
527   read i
528
529   let i--
530   if [ "$i" -lt 0 ] || [ "${listSessions[$i]}" == '' ] ; then
531     pr 'Invalid session'
532     return $errPickSession
533   fi
534
535   sess="${listSessions[$i]}"
536   pr "You chose session $sess"
537   echo $sess
538   return 0
539 }
540
541 # Run an action
542 function RunAction() {
543   local ret
544   type "$1" > /dev/null 2>&1
545   if [ $? == 0 ] ; then
546     #pr "--> $1 (wd: $PWD)"
547     eval "$@"
548     ret=$?
549     #pr "<-- $1 (ret: $ret, wd: $PWD)"
550     return $ret
551   fi
552   return 0
553 }
554
555 # Print help screen
556 function Help() {
557   pr "$Prog -- by Dario Berzano <dario.berzano@cern.ch>"
558   pr 'Controls the Release Validation workflow on the cloud for AliRoot.'
559   pr
560   pr "Usage 1: $Prog [--prepare|--launch] --aliroot <aliroot_tag> [-- arbitraryOpt1=value [arbitraryOpt2=value2...]]"
561   pr
562   pr 'A new session is created to validate the specified AliRoot tag.'
563   pr
564   pr '  --prepare  : prepares the session directory containing the files needed'
565   pr '              for the validation'
566   pr '  --launch   : launches the full validation process: prepares session,'
567   pr '              runs the virtual machine, launches the validation program'
568   pr '  --aliroot  : the AliRoot tag to validate, in the form "vAN-20140610"'
569   pr
570   pr 'Arbitrary options (in the form variable=value) can be specified after the'
571   pr 'double dash and will override the corresponding options in any of the'
572   pr 'configuration files.'
573   pr ; pr
574   pr "Usage 2: $Prog [--runvm|--validate|--shell|--status] --session <session_tag>"
575   pr
576   pr 'Runs the validation step by step after a session is created with'
577   pr '--prepare, and runs other actions on a certain session.'
578   pr
579   pr '  --session  : session identifier, e.g. vAN-20140610_20140612-123047-utc:'
580   pr '               if no session is specified an interactive prompt is'
581   pr '               presented'
582   pr '  --runvm    : instantiates the head node of the validation cluster on'
583   pr '               the cloud' 
584   pr '  --validate : runs the validation script on the head node for the'
585   pr '               current session. Head node must be already up, or it'
586   pr '               should be created with --runvm. If validation is running'
587   pr '               already, connects to the existing validation shell'
588   pr '  --attach   : attach a currently running validation screen; remember to'
589   pr '               detach with Ctrl+A+D (and *not* Ctrl-C)'
590   pr '  --shell    : does SSH on the head node'
591   pr '  --status   : returns the status of the validation'
592   pr ; pr
593   pr 'Example 1: run the validation of AliRoot tag vAN-20140610:'
594   pr
595   pr "  $Prog --aliroot vAN-20140610 --launch"
596   pr
597   pr 'Example 2: do the same thing step-by-step:'
598   pr
599   pr "  $Prog --aliroot vAN-20140610 --prepare"
600   pr "  $Prog --runvm"
601   pr "  $Prog --validate"
602   pr
603 }
604
605 # The main function
606 function Main() {
607
608   # local variables
609   local Args aliRootTag EnterShell Actions sessionTag
610   Actions=()
611
612   # parse command line options
613   while [ $# -gt 0 ] ; do
614     case "$1" in
615
616       # options
617       --aliroot|-a)
618         aliRootTag="$2"
619         shift 2
620       ;;
621       --session)
622         sessionTag="$2"
623         shift 2
624       ;;
625
626       # actions
627       --launch)
628         # all actions
629         Actions=( PrepareSession MoveToSessionDir LoadConfig InstantiateValidationVM WaitSsh Validate )
630         shift
631       ;;
632       --prepare)
633         Actions=( PrepareSession MoveToSessionDir )
634         shift
635       ;;
636       --runvm)
637         Actions=( MoveToSessionDir LoadConfig InstantiateValidationVM )
638         shift
639       ;;
640       --validate)
641         Actions=( MoveToSessionDir LoadConfig WaitSsh Validate )
642         shift
643       ;;
644       --attach)
645         Actions=( MoveToSessionDir LoadConfig WaitSsh Attach )
646         shift
647       ;;
648
649       # extra actions
650       --shell)
651         Actions=( MoveToSessionDir LoadConfig WaitSsh Shell )
652         shift
653       ;;
654       --status)
655         Actions=( MoveToSessionDir LoadConfig WaitSsh Status )
656         shift
657       ;;
658       --help)
659         Help
660         exit 0
661       ;;
662
663       # end of options
664       --)
665         shift
666         break
667       ;;
668
669       *)
670         pr "Invalid option: $1. Use --help for assistance."
671         return $errInvalidOpt
672       ;;
673     esac
674   done
675
676   # check for the presence of the required tools in the $PATH
677   for T in euca-describe-instances euca-describe-regions euca-run-instances euca-create-keypair euca-delete-keypair rsync ; do
678     which "$T" > /dev/null 2>&1
679     if [ $? != 0 ] ; then
680       pr "Cannot find one of the required commands: $T"
681       return $errMissingCmd
682     fi
683   done
684
685   # test EC2 credentials
686   # euca-describe-regions > /dev/null 2>&1
687   # if [ $? != 0 ] ; then
688   #   pr 'Cannot authenticate to EC2.'
689   #   pr 'Note: you must have at least the following variables properly set in your environment:'
690   #   pr "  * EC2_URL (current value: ${EC2_URL-<not set>})"
691   #   pr "  * EC2_ACCESS_KEY (current value: ${EC2_ACCESS_KEY-<not set>})"
692   #   pr "  * EC2_SECRET_KEY (current value: ${EC2_SECRET_KEY-<not set>})"
693   #   return $errEc2Auth
694   # fi
695
696   # what to do?
697   if [ ${#Actions[@]} == 0 ] ; then
698     pr 'Nothing to do. Use --help for assistance.'
699     return $errInvalidOpt
700   fi
701
702   # run actions
703   for ((i=0; i<${#Actions[@]}; i++)) ; do
704
705     A=${Actions[$i]}
706
707     if [ "$A" == 'PrepareSession' ] ; then
708       # special action returning the session tag
709       if [ "$aliRootTag" == '' ] ; then
710         pr 'Specify an AliRoot version with --aliroot <tag>'
711         return $errInvalidOpt
712       fi
713       if [ "$sessionTag" != '' ] ; then
714         pr 'Cannot use --session with --prepare. Use --help for assistance.'
715         return $errInvalidOpt
716       fi
717       sessionTag=$( RunAction "$A" "$aliRootTag" "$@" )
718       ret=$?
719     else
720       if [ "$sessionTag" == '' ] ; then
721         sessionTag=$( PickSession )
722         ret=$?
723         [ $ret != 0 ] && return $ret
724       fi
725       RunAction "$A" "$sessionTag"
726       ret=$?
727     fi
728
729     # 100 to 140 --> not errors
730     ( [ $ret != 0 ] && ( [ $ret -ge 100 ] || [ $ret -le 140 ] ) ) && break
731
732   done
733
734   # undo actions
735   let i--
736   if [ $ret != 0 ] && ( [ $ret -ge 100 ] || [ $ret -le 140 ] ) ; then
737     for ((; i>=0; i--)) ; do
738       RunAction "${Actions[$i]}_Undo" "$sessionTag"
739     done
740   fi
741
742   # return last value
743   return $ret
744
745 }
746
747 #
748 # Entry point
749 #
750
751 Main "$@" || exit $?