Advertisement
devinteske

bs_parallel.sh

Jan 8th, 2020
634
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 10.77 KB | None | 0 0
  1. #!/bin/sh
  2. ############################################################ IDENT(1)
  3. #
  4. # $Title: Script for bootstrapping multiple hosts in parallel $
  5. # $Copyright: 2017-2020 Devin Teske. All rights reserved. $
  6. #
  7. ############################################################ INFORMATION
  8. #
  9. # Sometimes you just want to bootstrap a bunch of machines. This script helps
  10. # you bootstrap them in parallel to minimize the amount of time it takes to get
  11. # a large amount of machines bootstrapped.
  12. #
  13. ############################################################ CONFIGURATION
  14.  
  15. #
  16. # Bootstrap script
  17. # NB: Exported for parallelized sh
  18. #
  19. export BSCMD="./bs.sh" # -b path
  20.  
  21. #
  22. # Bootstrap script arguments (these go after the host name)
  23. # NB: Exported for parallelized sh
  24. #
  25. export BSARGS=
  26.  
  27. #
  28. # Bootstrap script options (these go before the host name)
  29. #
  30. export BSOPTS=
  31.  
  32. #
  33. # Directory where logs are to be stored
  34. # NB: Exported for parallelized sh
  35. #
  36. export LOGDIR="$HOME/logs" # -l path
  37.  
  38. #
  39. # Maximum concurrent bootstrap scripts that can run in-parallel
  40. #
  41. MAXPARALLEL=9 # -n num
  42.  
  43. #
  44. # Maximum seconds to randomly sleep before each bootstrap starts
  45. # NB: Exported for parallelized sh
  46. #
  47. export MAXSLEEP=30 # -s num
  48.  
  49. #
  50. # Test mode. If enabled (non-NULL value), don't actually do anything
  51. # NB: Exported for parallelized sh
  52. #
  53. export TEST= # -t
  54.  
  55. ############################################################ ENVIRONMENT
  56.  
  57. SUDO_PROMPT="[sudo] Password:"
  58. SUDO_PROMPT_RE='\[sudo\] Password:'
  59. export SUDO_PROMPT SUDO_PROMPT_RE
  60.  
  61. # OPTIONAL: Ansible become-user support
  62. #?BECOME_PROMPT="SUDO password: "
  63. #?BECOME_PROMPT_RE="SUDO password: "
  64. #?export BECOME_PROMPT BECOME_PROMPT_RE
  65.  
  66. # OPTIONAL: Ansible vault support
  67. #?VAULT_PROMPT="Vault password: "
  68. #?VAULT_PROMPT_RE="$VAULT_PROMPT"
  69. #?export VAULT_PROMPT VAULT_PROMPT_RE
  70.  
  71. # OPTIONAL: Kerberos support
  72. #?KINIT_PROMPT="Password for admin@domain: "
  73. #?KINIT_PROMPT_RE="$KINIT_PROMPT"
  74. #?export KINIT_PROMPT KINIT_PROMPT_RE
  75.  
  76. ############################################################ GLOBALS
  77.  
  78. pgm="${0##*/}" # program basename
  79.  
  80. #
  81. # Global exit status
  82. #
  83. SUCCESS=0
  84. FAILURE=1
  85.  
  86. #
  87. # Command-line options
  88. #
  89. OPT_EXPECT_SUDO=        # -S
  90.  
  91. #
  92. # Terminal Detection
  93. #
  94. stty size > /dev/null 2>&1 && TTY=1
  95.  
  96. #
  97. # Inline routine for calculating time elapsed from $estart
  98. #
  99. export ELAPSED_DURATION='
  100.     estop=$( date +%s )
  101.     elapsed=$(( $estop - $estart ))
  102.     hour=$(( $elapsed / 3600 ))
  103.     min=$(( ($elapsed - $hour * 3600) / 60 ))
  104.     sec=$(( $elapsed - $hour * 3600 - $min * 60 ))
  105.     [ $hour -lt 10 ] && hour=0$hour
  106.     [ $min -lt 10 ] && min=0$min
  107.     [ $sec -lt 10 ] && sec=0$sec
  108.     if [ $hour -gt 0 ]; then
  109.         duration=$hour:$min:$sec
  110.     else
  111.         duration=$min:$sec
  112.     fi
  113. ' # END-QUOTE
  114.  
  115. ############################################################ FUNCTIONS
  116.  
  117. die()
  118. {
  119.     [ "$*" ] && fail "$*"
  120.     exit $FAILURE
  121. }
  122.  
  123. exec 3<&1
  124. eval2(){ printf '\e[2m%s\e[m\n' "$*" >&3; eval "$@"; }
  125. fail(){ printf "\e[1m+ \e[31mFAIL\e[m %s\n\n" "$*"; }
  126. pass(){ printf "\e[1m+ \e[32mPASS\e[m %s\n\n" "$*"; }
  127. step(){ printf "\e[32;1m==>\e[39m %s\e[m\n" "$*"; }
  128. warn(){ printf '\e[33;1m!WARNING!\e[39m %s\e[m\n' "$*"; }
  129.  
  130. usage()
  131. {
  132.     local fmt="\t%-9s %s\n"
  133.     exec >&2
  134.     [ "$*" ] && echo "$pgm:" "$@"
  135.     printf "Usage: %s [OPTIONS] host ...\n" "$pgm"
  136.     printf "OPTIONS:\n"
  137.     printf "$fmt" "-a args" \
  138.         "Bootstrap command arguments to use after each host name."
  139.     printf "$fmt" "-b path" \
  140.         "Bootstrap command (default \`$BSCMD')."
  141.     printf "$fmt" "-l path" \
  142.         "Path to log directory (default \`$LOGDIR')."
  143.     printf "$fmt" "-n num" \
  144.         "Maximum number of parallel instances (default $MAXPARALLEL)."
  145.     printf "$fmt" "-o opts" \
  146.         "Bootstrap command options to use before each host name."
  147.     printf "$fmt" "-S" \
  148.         "Use tcl/expect to handle sudo prompts automatically."
  149.     printf "$fmt" "-s num" \
  150.         "Maximum seconds to randomly sleep (default $MAXSLEEP)."
  151.     printf "$fmt" "-t" \
  152.         "Test mode. Don't actually do anything but pretend."
  153.     die
  154. }
  155.  
  156. isset()
  157. {
  158.     eval [ \"\${$1+set}\" ]
  159. }
  160.  
  161. sread()
  162. {
  163.     local OPTIND=1 OPTARG flag
  164.     local prompt= retval
  165.  
  166.     while getopts p: flag; do
  167.         case "$flag" in
  168.         p) prompt="$OPTARG" ;;
  169.         esac
  170.     done
  171.     shift $(( OPTIND - 1 ))
  172.  
  173.     ! isset "$1" || return $SUCCESS
  174.     ! [ "$prompt" ] || printf "%s" "$prompt"
  175.  
  176.     trap 'stty echo' EXIT
  177.     stty -echo
  178.     retval=$?
  179.     read -r $1
  180.     stty echo
  181.     trap - EXIT
  182.  
  183.     echo
  184.     export $1
  185.     return $retval
  186. }
  187.  
  188. make_expect()
  189. {
  190.     local var=$1
  191.     shift 1 # var
  192.     exec 9<<-EOF
  193.     set timeout 86400
  194.     fconfigure stdout -buffering none
  195.     #log_user 0
  196.     spawn {*}\$argv
  197.     #log_user 1
  198.     while (1) { expect {$(
  199.         for key_value in "$@"; do
  200.             printf '\n\t\t'
  201.             printf '%s $::env(%s) { send "$::env(%s)\\n" }' \
  202.                 -re ${key_value%%=*} ${key_value#*=}
  203.         done )
  204.         timeout { Time_Out; break }
  205.         eof { break }
  206.     } }
  207.     lassign [wait] pid spawnid os_error_flag value
  208.     exit \$value
  209.     EOF
  210.     eval $var='$( cat <&9 )'
  211. }
  212.  
  213. ############################################################ MAIN
  214.  
  215. #
  216. # Command-line options
  217. #
  218. while getopts a:b:l:n:o:Ss:t flag; do
  219.     case "$flag" in
  220.     a) BSARGS="$OPTARG" ;;
  221.     b) BSCMD="$OPTARG" ;;
  222.     l) LOGDIR="$OPTARG" ;;
  223.     n) MAXPARALLEL="$OPTARG" ;;
  224.     o) BSOPTS="$OPTARG" ;;
  225.     S) OPT_EXPECT_SUDO=1 ;;
  226.     s) MAXSLEEP="$OPTARG" ;;
  227.     t) TEST=1 ;;
  228.     *) usage # NOTREACHED
  229.     esac
  230. done
  231. shift $(( $OPTIND - 1 ))
  232.  
  233. #
  234. # Validate `-b path' option
  235. #
  236. [ -e "$BSCMD" ] || die "$BSCMD: No such file or directory" # NOTREACHED
  237. [ -d "$BSCMD" ] && die "$BSCMD: Is a directory" # NOTREACHED
  238. [ -x "$BSCMD" ] || die "$BSCMD: Permission denied" # NOTREACHED
  239.  
  240. #
  241. # Validate `-l path' option
  242. #
  243. [ -e "$LOGDIR" ] || mkdir -p "$LOGDIR" || die # NOTREACHED
  244. [ -d "${LOGDIR%/}/" ] || die "$LOGDIR: Not a directory" # NOTREACHED
  245.  
  246. #
  247. # Validate `-n num' option
  248. #
  249. case "$MAXPARALLEL" in
  250. "") usage "-n flag requires an argument" ;; # NOTREACHED
  251. *[!0-9]*) usage "$MAXPARALLEL: -n argument must be a number" ;; # NOTREACHED
  252. esac
  253.  
  254. #
  255. # Validate `-s num' option
  256. #
  257. case "$MAXSLEEP" in
  258. "") usage "-s flag requires an argument" ;; # NOTREACHED
  259. *[!0-9]*) usage "$MAXSLEEP: -s argument must be a number" ;; # NOTREACHED
  260. esac
  261.  
  262. #
  263. # Validate number of command-line arguments
  264. #
  265. [ $# -gt 0 ] || usage "missing host argument(s)" # NOTREACHED
  266.  
  267. #
  268. # Provide some basic information
  269. #
  270. step "Run Details"
  271. echo "Maximum parallel instances: $MAXPARALLEL"
  272. echo "Maximum random sleep: $MAXSLEEP"
  273. echo "Hosts: $*"
  274. set -- $* || die
  275. echo "Total Hosts: $#"
  276. echo "Running (for each host): $BSCMD ${BSOPTS:+$BSOPTS }<host> $BSARGS"
  277. echo "Logs go to: ${LOGDIR%/}/<host>.log"
  278. if [ "$TEST" ]; then
  279.     printf "${TTY:+\e[33m}INFO:${TTY:+\e[0m} %s\n" \
  280.         "TEST MODE enabled (bootstrap actions will not be performed)"
  281. else
  282.     printf "${TTY:+\e[35m}%s${TTY:+\e[0m} " \
  283.         "< Press ENTER to proceed or Ctrl-C to cancel >"
  284.     read IGNORED
  285. fi
  286.  
  287. #
  288. # Tcl/Expect
  289. #
  290. if [ "$OPT_EXPECT_SUDO" ]; then
  291.     errexit=
  292.     case "$-" in
  293.     *e*) errexit=1 ;;
  294.     esac
  295.     set -e # errexit
  296.     step "Test sudo credentials"
  297.     sread -p "$SUDO_PROMPT" _SP || die
  298.     make_expect EXPECT_SUDO SUDO_PROMPT_RE=_SP
  299.     echo "$EXPECT_SUDO" | eval2 expect -f- sudo /bin/true || die
  300.     pass
  301.     # OPTIONAL: Kerberos support
  302.     #?step "Test kerberos credentials"
  303.     #?sread -p "$KINIT_PROMPT" _KP || die
  304.     #?make_expect EXPECT_KINIT KINIT_PROMPT_RE=_KP
  305.     #?if ! eval2 klist \| grep -q admin; then
  306.     #?  eval2 kdestroy
  307.     #?  eval2 sleep 3
  308.     #?fi
  309.     #?echo "$EXPECT_KINIT" | eval2 expect -f- kinit admin || die
  310.     #?pass
  311.     # OPTIONAL: Ansible support
  312.     #?if ! isset _VP; then
  313.     #?  echo
  314.     #?  warn "Using kerberos credentials for ansible vault"
  315.     #?  echo
  316.     #?  _VP="$_KP"
  317.     #?  export _VP
  318.     #?fi
  319.     #?make_expect EXPECT_SUDO_OR_VAULT \
  320.     #?  SUDO_PROMPT_RE=_SP BECOME_PROMPT_RE=_SP VAULT_PROMPT_RE=_VP
  321.     [ "$errexit" ] || set +e
  322. fi
  323.  
  324. #
  325. # Configure EXIT trap to provide ending information
  326. #
  327. trap '
  328.     echo "End parallel bootstrap: $( date )"
  329.     eval "$ELAPSED_DURATION"
  330.     echo "Elapsed parallel time: $duration"
  331. ' EXIT
  332.  
  333. #
  334. # Parallel sh code
  335. # NB: The first argument ($1) represents the host to be bootstrapped
  336. #
  337. export OPT_EXPECT_SUDO EXPECT_SUDO EXPECT_SUDO_OR_VAULT
  338. exec 9<<'EOF'
  339.     log="${LOGDIR%/}/$1.log"
  340.     exec > "$log" 2>&1
  341.  
  342.     sleep=$(( $RANDOM * $MAXSLEEP / 32768 ))
  343.     echo "Sleeping $sleep second(s) before starting bootstrap..."
  344.     sleep $sleep
  345.  
  346.     echo "Bootstrap started: $( date )"
  347.     estart=$( date +%s )
  348.  
  349.     echo "Running: $BSCMD ${BSOPTS:+$BSOPTS }$1 $BSARGS"
  350.     trap '
  351.         echo "Exit code: $?"
  352.         echo "Bootstrap ended: $( date )"
  353.         eval "$ELAPSED_DURATION"
  354.         echo "Elapsed time: $duration"
  355.     ' EXIT
  356.     if [ "$TEST" ]; then
  357.         echo "INFO: TEST MODE enabled (nothing done)"
  358.     elif [ "$OPT_EXPECT_SUDO" ]; then
  359.         # OPTIONAL: Ansible vault support
  360.         #?echo "$EXPECT_SUDO_OR_VAULT" |
  361.         echo "$EXPECT_SUDO" |
  362.             expect -f- $BSCMD $BSOPTS $1 $BSARGS
  363.     else
  364.         $BSCMD $BSOPTS $1 $BSARGS
  365.     fi
  366. EOF
  367. BS_PARALLEL_WORKER=$( cat <&9 )
  368.  
  369. #
  370. # Run parallel instances of sh code, one instance per host to be bootstrapped
  371. # NB: Worker code evaluated to prevent raw code appearing in ps(1)
  372. #
  373. step "Run"
  374. echo "Start parallel bootstrap: $( date )"
  375. export BS_PARALLEL_WORKER
  376. estart=$( date +%s )
  377. exec 3<&1 4<&2
  378. ( exec 5<&1 >&3 2>&4 3<&5 4>&- 5>&- # fd0-2 to TTY, fd3 to pipe
  379.     ################################################## INFORMATION
  380.     #
  381.     # Shell co-routine for sending work to xargs and displaying status
  382.     #
  383.     ################################################## MAIN
  384.  
  385.     #
  386.     # Send list of hosts to xargs
  387.     #
  388.     n=0
  389.     for host in $*; do
  390.         n=$(( $n + 1 ))
  391.  
  392.         log="${LOGDIR%/}/$host.log"
  393.         rm -f "$log"
  394.  
  395.         unset done$n
  396.         eval log$n=\"\$log\"
  397.  
  398.         echo $host >&3
  399.     done
  400.  
  401.     #
  402.     # Display status and elapsed time for completed host(s)
  403.     #
  404.     remainder=$n
  405.     printf "%4s %6s %9s %s\n" "#" STATUS ELAPSED HOSTNAME
  406.     while [ $remainder -gt 0 ]; do
  407.         n=0
  408.         alldone=1
  409.         for host in $*; do
  410.             n=$(( $n + 1 ))
  411.             eval done=\$done$n
  412.             [ "$done" ] && continue
  413.  
  414.             # This host is not done yet, check its status
  415.             alldone=
  416.             eval log=\"\$log$n\"
  417.             status=$( awk '
  418.                 /^Exit code:/, ++fs { status = $NF }
  419.                 /^Elapsed time:/, ++fe { elapsed = $NF }
  420.                 fs && fe { exit found = 1 }
  421.                 END { print status, elapsed; exit !found }
  422.             ' "$log" 2> /dev/null ) || continue
  423.  
  424.             # This host has recently completed, display status
  425.             eval done$n=1
  426.             if [ "$status" != "${status#"$SUCCESS "}" ]; then
  427.                 msg="[${TTY:+\e[32;1m} OK ${TTY:+\e[0m}]"
  428.             else
  429.                 msg="[${TTY:+\e[31;1m}FAIL${TTY:+\e[0m}]"
  430.             fi
  431.             elapsed="${status#*[$IFS]}"
  432.             printf "%4s $msg %9s %s\n" \
  433.                 "$remainder" "$elapsed" "$host"
  434.  
  435.             remainder=$(( $remainder - 1 ))
  436.         done
  437.         [ "$alldone" ] && break
  438.         sleep 1
  439.     done
  440.  
  441.     ######################################################################
  442. ) | xargs -rn1 -P$MAXPARALLEL sh -c 'eval "$BS_PARALLEL_WORKER"' /bin/sh
  443.  
  444. ################################################################################
  445. # END
  446. ################################################################################
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement