ctdb: Remove an unnecessary cast
[vlendec/samba-autobuild/.git] / ctdb / tools / onnode
index c60127ce090c81c163ae8dd50ae74c24a0ad8b84..f04d33f9c3f66181a8d1b141491921b61ec2e63d 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 
 # Run commands on CTDB nodes.
 
 # it under the terms of the GNU General Public License as published by
 # the Free Software Foundation; either version 3 of the License, or
 # (at your option) any later version.
-   
+
 # This program 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 General Public License for more details.
-   
+
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
-prog=$(basename $0)
+prog=$(basename "$0")
 
 usage ()
 {
     cat >&2 <<EOF
 Usage: onnode [OPTION] ... <NODES> <COMMAND> ...
   options:
-    -c         Run in current working directory on specified nodes.
-    -p         Run command in parallel on specified nodes.
-    -q         Do not print node addresses (overrides -v).
-    -v         Print node address even for a single node.
-  <NODES>      "all", "healthy", "connected";
-               or a node number (0 base); or
-               list (comma separated) of <NODES>; or
-               range (hyphen separated) of node numbers.
+    -c          Run in current working directory on specified nodes.
+    -f          Specify nodes file, overriding default.
+    -i          Keep standard input open - the default is to close it.
+    -n          Allow nodes to be specified by name.
+    -p          Run command in parallel on specified nodes.
+    -P          Push given files to nodes instead of running commands.
+    -q          Do not print node addresses (overrides -v).
+    -v          Print node address even for a single node.
+  <NODES>       "all", "any", "ok" (or "healthy"), "con" (or "connected") ; or
+                a node number (0 base); or
+                a hostname (if -n is specified); or
+                list (comma separated) of <NODES>; or
+                range (hyphen separated) of node numbers.
 EOF
     exit 1
 
@@ -51,52 +56,62 @@ invalid_nodespec ()
 
 # Defaults.
 current=false
+ctdb_nodes_file=""
 parallel=false
 verbose=false
 quiet=false
+names_ok=false
+push=false
+stdin=false
+
+if [ -z "$CTDB_BASE" ] ; then
+    CTDB_BASE="/usr/local/etc/ctdb"
+fi
 
 parse_options ()
 {
-    # $POSIXLY_CORRECT means that the command passed to onnode can
-    # take options and getopt won't reorder things to make them
-    # options ot onnode.
-    local temp=$(POSIXLY_CORRECT=1 getopt -n "$prog" -o "chpqv" -l help -- "$@")
-
-    [ $? != 0 ] && usage
-
-    eval set -- "$temp"
-
-    while true ; do
-       case "$1" in
-           -c) current=true ; shift ;;
-           -p) parallel=true ; shift ;;
-           -q) quiet=true ; shift ;;
-           -v) verbose=true ; shift ;;
-           --) shift ; break ;;
-           -h|--help|*) usage ;; # Shouldn't happen, so this is reasonable.
-       esac
-    done
-
-    [ $# -lt 2 ] && usage
-
-    nodespec="$1" ; shift
-    command="$@"
+       local opt
+
+       while getopts "cf:hnpqvPi?" opt ; do
+               case "$opt" in
+               c) current=true ;;
+               f) ctdb_nodes_file="$OPTARG" ;;
+               n) names_ok=true ;;
+               p) parallel=true ;;
+               q) quiet=true ;;
+               v) verbose=true ;;
+               P) push=true ;;
+               i) stdin=true ;;
+               \?|h) usage ;;
+               esac
+       done
+       shift $((OPTIND - 1))
+
+       if [ $# -lt 2 ] ; then
+               usage
+       fi
+
+       nodespec="$1" ; shift
+       command="$*"
 }
 
-# Can probably be avoided if we use bash?
-get_nth ()
+echo_nth ()
 {
     local n="$1" ; shift
 
-    local c=0
-    local j
-    for j ; do
-       if [ $n -eq $c ] ; then
-           echo $j
-           break
-       fi
-       c=$(($c + 1))
-    done
+    # Note that this is 0-based
+    local node=""
+    if [ "$n" -le $# ] ; then
+           shift "$n"
+           node="$1"
+    fi
+
+    if [ -n "$node" ] && [ "$node" != "#DEAD" ] ; then
+       echo "$node"
+    else
+       echo "${prog}: \"node ${n}\" does not exist" >&2
+       exit 1
+    fi
 }
 
 parse_nodespec ()
@@ -107,105 +122,180 @@ parse_nodespec ()
        for i in $1 ; do
            case "$i" in
                *-*) seq "${i%-*}" "${i#*-}" 2>/dev/null || invalid_nodespec ;;
-               all|healthy|connected) echo "$i" ;;
+               all|any|ok|healthy|con|connected) echo "$i" ;;
                *)
-                   [ $i -gt -1 ] 2>/dev/null || invalid_nodespec
-                   echo $i
+                   [ "$i" -gt -1 ] 2>/dev/null || $names_ok || invalid_nodespec
+                   echo "$i"
            esac
        done
     )
 }
 
-# Cache
-ctdb_status_output=""
+ctdb_status_output="" # cache
 get_nodes_with_status ()
 {
     local all_nodes="$1"
     local status="$2"
 
-    local bits
-    case "$status" in
-       healthy)
-           bits="0:0:0:0"
-           ;;
-       connected)
-           bits="0:[0-1]:[0-1]:[0-1]"
-           ;;
-       *)
-           invalid_nodespec
-    esac
-
     if [ -z "$ctdb_status_output" ] ; then
-       ctdb_status_output=$(ctdb -Y status)
-       ctdb_status_output="${ctdb_status_output#* }"
+       ctdb_status_output=$(ctdb -X status 2>&1)
+       # No! Checking the exit code afterwards is actually clearer...
+       # shellcheck disable=SC2181
+       if [ $? -ne 0 ] ; then
+           echo "${prog}: unable to get status of CTDB nodes" >&2
+           echo "$ctdb_status_output" >&2
+           exit 1
+       fi
+       local nl="
+"
+       ctdb_status_output="${ctdb_status_output#*"${nl}"}"
     fi
 
-    local nodes=""
-    local i
-    for i in $ctdb_status_output ; do
-       # Try removing bits from end.
-       local t="${i%:${bits}:}"
-       if [ "$t" != "$i" ] ; then
-           # Succeeded.  Get address.  NOTE: this is an optimisation.
-           # It might be better to get the node number and then use
-           # get_nth() to get the address.  This would make things
-           # more consistent if /etc/ctdb/nodes actually contained
-           # hostnames.
-           nodes="${nodes} ${t##*:}"
-       fi
-    done
+    (
+       local i
+       IFS="${IFS}|"
+       while IFS="" read -r i ; do
+
+           # Intentional word splitting
+           # shellcheck disable=SC2086
+           set -- $i # split line on colons
+           shift     # line starts with : so 1st field is empty
+           local pnn="$1" ; shift
+           shift # ignore IP address but need status bits below
+
+           case "$status" in
+               healthy)
+                   # If any bit is 1, don't match this address.
+                   local s
+                   for s ; do
+                       [ "$s" != "1" ] || continue 2
+                   done
+                   ;;
+               connected)
+                   # If disconnected bit is not 0, don't match this address.
+                   [ "$1" = "0" ] || continue
+                   ;;
+               *)
+                   invalid_nodespec
+           esac
 
-    echo $nodes
+           # Intentional multi-word expansion
+           # shellcheck disable=SC2086
+           echo_nth "$pnn" $all_nodes
+       done <<<"$ctdb_status_output"
+    )
+}
+
+get_any_available_node ()
+{
+    local all_nodes="$1"
+
+    # We do a recursive onnode to find which nodes are up and running.
+    local out line
+    out=$("$0" -pq all ctdb pnn 2>&1)
+    while read -r line ; do
+       if [[ "$line" =~ ^[0-9]+$ ]] ; then
+           local pnn="$line"
+           # Intentional multi-word expansion
+           # shellcheck disable=SC2086
+           echo_nth "$pnn" $all_nodes
+           return 0
+       fi
+       # Else must be an error message from a down node.
+    done <<<"$out"
+    return 1
 }
 
 get_nodes ()
 {
-    [ -f "$CTDB_NODES_FILE" ] || CTDB_NODES_FILE=/etc/ctdb/nodes
-    local all_nodes=$(egrep '^[[:alnum:]]' $CTDB_NODES_FILE)
-
-    local nodes=""
-    local n
-    for n in $(parse_nodespec "$1") ; do
-       [ $? != 0 ] && exit 1  # Required to catch exit in above subshell.
-       case "$n" in
-           all)
-               echo $all_nodes ;;
-           healthy|connected) 
-               get_nodes_with_status "$all_nodes" "$n"
-               ;;
-           *)
-               local node=$(get_nth $n $all_nodes)
-               if [ -n "$node" ] ; then
-                   echo $node
-               else
-                   echo "${prog}: \"node ${n}\" does not exist" >&2
-                   exit 1
+       local all_nodes
+
+       local f="${CTDB_BASE}/nodes"
+       if [ -n "$ctdb_nodes_file" ] ; then
+               f="$ctdb_nodes_file"
+               if [ ! -e "$f" ] && [ "${f#/}" = "$f" ] ; then
+                       # $f is relative, try in $CTDB_BASE
+                       f="${CTDB_BASE}/${f}"
                fi
-       esac
-       
-    done
+       fi
+
+       if [ ! -r "$f" ] ; then
+               echo "${prog}: unable to open nodes file  \"${f}\"" >&2
+               exit 1
+       fi
+
+       all_nodes=$(sed -e 's@#.*@@g' -e 's@ *@@g' -e 's@^$@#DEAD@' "$f")
+
+       local n nodes
+       nodes=$(parse_nodespec "$1") || exit $?
+       for n in $nodes ; do
+               case "$n" in
+               all)
+                       echo "${all_nodes//#DEAD/}"
+                       ;;
+               any)
+                       get_any_available_node "$all_nodes" || exit 1
+                       ;;
+               ok|healthy)
+                       get_nodes_with_status "$all_nodes" "healthy" || exit 1
+                       ;;
+               con|connected)
+                       get_nodes_with_status "$all_nodes" "connected" || exit 1
+                       ;;
+               [0-9]|[0-9][0-9]|[0-9][0-9][0-9])
+                       # Intentional multi-word expansion
+                       # shellcheck disable=SC2086
+                       echo_nth "$n" $all_nodes
+                       ;;
+               *)
+                       $names_ok || invalid_nodespec
+                       echo "$n"
+               esac
+       done
+}
+
+# shellcheck disable=SC2317
+# push() called indirectly via $ONNODE_SSH
+push ()
+{
+       local host="$1"
+       local files="$2"
+
+       local f
+       for f in $files ; do
+               $verbose && echo "Pushing $f"
+               case "$f" in
+               /*) rsync "$f" "[${host}]:${f}" ;;
+               *)  rsync "${PWD}/${f}" "[${host}]:${PWD}/${f}" ;;
+               esac
+       done
 }
 
 ######################################################################
 
 parse_options "$@"
 
-$current && command="cd $PWD && $command"
-
-SSH_OPTS=
-# Could "2>/dev/null || true" but want to see errors from typos in file.
-[ -r /etc/ctdb/onnode.conf ] && . /etc/ctdb/onnode.conf
-[ -n "$SSH" ] || SSH=ssh
-if [ "$SSH" = "ssh" ] ; then
-    ssh_opts="-n"
+ssh_opts=
+if $push ; then
+       if [ -n "$ONNODE_SSH" ] ; then
+               export RSYNC_RSH="$ONNODE_SSH"
+       fi
+       ONNODE_SSH=push
 else
-    : # rsh? All bets are off!
+       $current && command="cd $PWD && $command"
+
+       # Could "2>/dev/null || true" but want to see errors from typos in file.
+       [ -r "${CTDB_BASE}/onnode.conf" ] && . "${CTDB_BASE}/onnode.conf"
+       [ -n "$ONNODE_SSH" ] || ONNODE_SSH=ssh
+       # $ONNODE_SSH must accept the -n option - it can be ignored!
+       if $parallel || ! $stdin ; then
+               ssh_opts="-n"
+       fi
 fi
 
 ######################################################################
 
-nodes=$(get_nodes "$nodespec")
-[ $? != 0 ] && exit 1   # Required to catch exit in above subshell.
+nodes=$(get_nodes "$nodespec") || exit $?
 
 if $quiet ; then
     verbose=false
@@ -213,40 +303,42 @@ else
     # If $nodes contains a space or a newline then assume multiple nodes.
     nl="
 "
-    [ "$nodes" != "${nodes%[ ${nl}]*}" ] && verbose=true
+    [ "$nodes" != "${nodes%[ "${nl}"]*}" ] && verbose=true
 fi
 
 pids=""
+# Intentional multi-word expansion
+# shellcheck disable=SC2086
 trap 'kill -TERM $pids 2>/dev/null' INT TERM
 # There's a small race here where the kill can fail if no processes
 # have been added to $pids and the script is interrupted.  However,
 # the part of the window where it matter is very small.
 retcode=0
 for n in $nodes ; do
-    if $parallel ; then
-       if $verbose ; then
-           # pipefail is a bashism - is there some way to do this with plain sh?
-           set -o pipefail 2>/dev/null
-           ($SSH $ssh_opts $EXTRA_SSH_OPTS $n "$command" 2>&1 | sed -e "s@^@[$n] @" )&
+       set -o pipefail 2>/dev/null
+
+       ssh_cmd="$ONNODE_SSH $ssh_opts"
+       if $parallel ; then
+               if $verbose ; then
+                       $ssh_cmd "$n" "$command" 2>&1 | sed -e "s@^@[$n] @"
+               else
+                       $ssh_cmd "$n" "$command"
+               fi &
+               pids="${pids} $!"
        else
-           $SSH $ssh_opts $EXTRA_SSH_OPTS $n "$command" &
-       fi
-       pids="${pids} $!"
-    else
-       if $verbose ; then
-           echo >&2 ; echo ">> NODE: $n <<" >&2
+               if $verbose ; then
+                       echo >&2 ; echo ">> NODE: $n <<" >&2
+               fi
+               {
+                       $ssh_cmd "$n" "$command"
+               } || retcode=$?
        fi
-
-       $SSH $ssh_opts $EXTRA_SSH_OPTS $n "$command" 
-       [ $? = 0 ] || retcode=$?
-    fi
 done
 
-$parallel && {
-    for p in $pids; do
-       wait $p
-       [ $? = 0 ] || retcode=$?
-    done
-}
+if $parallel ; then
+       for p in $pids; do
+               wait "$p" || retcode=$?
+       done
+fi
 
 exit $retcode