ctdb-tools: Use a clear and readable if-statement
[metze/samba/wip.git] / ctdb / tools / onnode
1 #!/bin/bash
2
3 # Run commands on CTDB nodes.
4
5 # See http://ctdb.samba.org/ for more information about CTDB.
6
7 # Copyright (C) Martin Schwenke  2008
8
9 # Based on an earlier script by Andrew Tridgell and Ronnie Sahlberg.
10
11 # Copyright (C) Andrew Tridgell  2007
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 3 of the License, or
16 # (at your option) any later version.
17    
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22    
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, see <http://www.gnu.org/licenses/>.
25
26 prog=$(basename "$0")
27
28 usage ()
29 {
30     cat >&2 <<EOF
31 Usage: onnode [OPTION] ... <NODES> <COMMAND> ...
32   options:
33     -c          Run in current working directory on specified nodes.
34     -f          Specify nodes file, overrides CTDB_NODES_FILE.
35     -i          Keep standard input open - the default is to close it.
36     -n          Allow nodes to be specified by name.
37     -o <prefix> Save standard output from each node to file <prefix>.<ip>
38     -p          Run command in parallel on specified nodes.
39     -P          Push given files to nodes instead of running commands.
40     -q          Do not print node addresses (overrides -v).
41     -v          Print node address even for a single node.
42   <NODES>       "all", "any", "ok" (or "healthy"), "con" (or "connected") ; or
43                 a node number (0 base); or
44                 a hostname (if -n is specified); or
45                 list (comma separated) of <NODES>; or
46                 range (hyphen separated) of node numbers.
47 EOF
48     exit 1
49
50 }
51
52 invalid_nodespec ()
53 {
54     echo "Invalid <nodespec>" >&2 ; echo >&2
55     usage
56 }
57
58 # Defaults.
59 current=false
60 parallel=false
61 verbose=false
62 quiet=false
63 prefix=""
64 names_ok=false
65 push=false
66 stdin=false
67
68 if [ -z "$CTDB_BASE" ] ; then
69     CTDB_BASE="/usr/local/etc/ctdb"
70 fi
71
72 . "${CTDB_BASE}/functions"
73 loadconfig "ctdb"
74
75 parse_options ()
76 {
77     # $POSIXLY_CORRECT means that the command passed to onnode can
78     # take options and getopt won't reorder things to make them
79     # options ot onnode.
80     local temp
81     # Not on the previous line - local returns 0!
82     temp=$(POSIXLY_CORRECT=1 getopt -n "$prog" -o "cf:hno:pqvPi" -l help -- "$@")
83
84     [ $? != 0 ] && usage
85
86     eval set -- "$temp"
87
88     while true ; do
89         case "$1" in
90             -c) current=true ; shift ;;
91             -f) CTDB_NODES_FILE="$2" ; shift 2 ;;
92             -n) names_ok=true ; shift ;;
93             -o) prefix="$2" ; shift 2 ;;
94             -p) parallel=true ; shift ;;
95             -q) quiet=true ; shift ;;
96             -v) verbose=true ; shift ;;
97             -P) push=true ; shift ;;
98             -i) stdin=true ; shift ;;
99             --) shift ; break ;;
100             -h|--help|*) usage ;; # Shouldn't happen, so this is reasonable.
101         esac
102     done
103
104     [ $# -lt 2 ] && usage
105
106     nodespec="$1" ; shift
107     command="$*"
108 }
109
110 echo_nth ()
111 {
112     local n="$1" ; shift
113
114     shift "$n"
115     local node="$1"
116
117     if [ -n "$node" -a "$node" != "#DEAD" ] ; then
118         echo "$node"
119     else
120         echo "${prog}: \"node ${n}\" does not exist" >&2
121         exit 1
122     fi
123 }
124
125 parse_nodespec ()
126 {
127     # Subshell avoids hacks to restore $IFS.
128     (
129         IFS=","
130         for i in $1 ; do
131             case "$i" in
132                 *-*) seq "${i%-*}" "${i#*-}" 2>/dev/null || invalid_nodespec ;;
133                 all|any|ok|healthy|con|connected) echo "$i" ;;
134                 *)
135                     [ "$i" -gt -1 ] 2>/dev/null || $names_ok || invalid_nodespec
136                     echo "$i"
137             esac
138         done
139     )
140 }
141
142 ctdb_status_output="" # cache
143 get_nodes_with_status ()
144 {
145     local all_nodes="$1"
146     local status="$2"
147
148     if [ -z "$ctdb_status_output" ] ; then
149         ctdb_status_output=$(ctdb -X status 2>&1)
150         if [ $? -ne 0 ] ; then
151             echo "${prog}: unable to get status of CTDB nodes" >&2
152             echo "$ctdb_status_output" >&2
153             exit 1
154         fi
155         local nl="
156 "
157         ctdb_status_output="${ctdb_status_output#*${nl}}"
158     fi
159
160     (
161         local i
162         IFS="${IFS}|"
163         while IFS="" read i ; do
164
165             # Intentional word splitting
166             # shellcheck disable=SC2086
167             set -- $i # split line on colons
168             shift     # line starts with : so 1st field is empty
169             local pnn="$1" ; shift
170             shift # ignore IP address but need status bits below
171
172             case "$status" in
173                 healthy)
174                     # If any bit is 1, don't match this address.
175                     local s
176                     for s ; do
177                         [ "$s" != "1" ] || continue 2
178                     done
179                     ;;
180                 connected)
181                     # If disconnected bit is not 0, don't match this address.
182                     [ "$1" = "0" ] || continue
183                     ;;
184                 *)
185                     invalid_nodespec
186             esac
187
188             # Intentional multi-word expansion
189             # shellcheck disable=SC2086
190             echo_nth "$pnn" $all_nodes
191         done <<<"$ctdb_status_output"
192     )
193 }
194
195 get_any_available_node ()
196 {
197     local all_nodes="$1"
198
199     # We do a recursive onnode to find which nodes are up and running.
200     local out line
201     out=$("$0" -pq all ctdb pnn 2>&1)
202     while read line ; do
203         if [[ "$line" =~ ^[0-9]+$ ]] ; then
204             local pnn="$line"
205             # Intentional multi-word expansion
206             # shellcheck disable=SC2086
207             echo_nth "$pnn" $all_nodes
208             return 0
209         fi
210         # Else must be an error message from a down node.
211     done <<<"$out"
212     return 1
213 }
214
215 get_nodes ()
216 {
217     local all_nodes
218
219     if [ -n "$CTDB_NODES_SOCKETS" ] ; then 
220         all_nodes="$CTDB_NODES_SOCKETS"
221     else
222         local f="${CTDB_BASE}/nodes"
223         if [ -n "$CTDB_NODES_FILE" ] ; then
224             f="$CTDB_NODES_FILE"
225             if [ ! -e "$f" -a "${f#/}" = "$f" ] ; then
226                 # $f is relative, try in $CTDB_BASE
227                 f="${CTDB_BASE}/${f}"
228             fi
229         elif [ -n "$CTDB_NODES" ] ; then
230             f="$CTDB_NODES"
231         fi
232
233         if [ ! -r "$f" ] ; then
234             echo "${prog}: unable to open nodes file  \"${f}\"" >&2
235             exit 1
236         fi
237
238         all_nodes=$(sed -e 's@#.*@@g' -e 's@ *@@g' -e 's@^$@#DEAD@' "$f")
239     fi
240
241     local nodes=""
242     local n
243     for n in $(parse_nodespec "$1") ; do
244         [ $? != 0 ] && exit 1  # Required to catch exit in above subshell.
245         case "$n" in
246             all)
247                 echo "${all_nodes//#DEAD/}"
248                 ;;
249             any)
250                 get_any_available_node "$all_nodes" || exit 1
251                 ;;
252             ok|healthy) 
253                 get_nodes_with_status "$all_nodes" "healthy" || exit 1
254                 ;;
255             con|connected) 
256                 get_nodes_with_status "$all_nodes" "connected" || exit 1
257                 ;;
258             [0-9]|[0-9][0-9]|[0-9][0-9][0-9])
259                 # Intentional multi-word expansion
260                 # shellcheck disable=SC2086
261                 echo_nth "$n" $all_nodes
262                 ;;
263             *)
264                 $names_ok || invalid_nodespec
265                 echo "$n"
266         esac
267     done
268 }
269
270 push()
271 {
272     local host="$1"
273     local files="$2"
274
275     local f
276     for f in $files ; do
277         $verbose && echo "Pushing $f"
278         case "$f" in
279             /*) rsync "$f" "[${host}]:${f}" ;;
280             *)  rsync "${PWD}/${f}" "[${host}]:${PWD}/${f}" ;;
281         esac
282     done
283 }
284
285 fakessh ()
286 {
287     CTDB_SOCKET="$1" sh -c "$2" 3>/dev/null
288 }
289
290 stdout_filter ()
291 {
292     if [ -n "$prefix" ] ; then
293         cat >"${prefix}.${n//\//_}"
294     elif $verbose && $parallel ; then
295         sed -e "s@^@[$n] @"
296     else
297         cat
298     fi
299 }
300
301 stderr_filter ()
302 {
303     if $verbose && $parallel ; then
304         sed -e "s@^@[$n] @"
305     else
306         cat
307     fi
308 }
309
310 ######################################################################
311
312 parse_options "$@"
313
314 ssh_opts=
315 if $push ; then
316     SSH=push
317     EXTRA_SSH_OPTS=""
318 else
319     $current && command="cd $PWD && $command"
320
321     if [ -n "$CTDB_NODES_SOCKETS" ] ; then
322         SSH=fakessh
323         EXTRA_SSH_OPTS=""
324     else 
325         # Could "2>/dev/null || true" but want to see errors from typos in file.
326         [ -r "${CTDB_BASE}/onnode.conf" ] && . "${CTDB_BASE}/onnode.conf"
327         [ -n "$SSH" ] || SSH=ssh
328         if [ "$SSH" = "ssh" ] ; then
329             if $parallel || ! $stdin ; then
330                 ssh_opts="-n"
331             fi
332         else
333             : # rsh? All bets are off!
334         fi
335     fi
336 fi
337
338 ######################################################################
339
340 nodes=$(get_nodes "$nodespec")
341 [ $? != 0 ] && exit 1   # Required to catch exit in above subshell.
342
343 if $quiet ; then
344     verbose=false
345 else
346     # If $nodes contains a space or a newline then assume multiple nodes.
347     nl="
348 "
349     [ "$nodes" != "${nodes%[ ${nl}]*}" ] && verbose=true
350 fi
351
352 pids=""
353 # Intentional multi-word expansion
354 # shellcheck disable=SC2086
355 trap 'kill -TERM $pids 2>/dev/null' INT TERM
356 # There's a small race here where the kill can fail if no processes
357 # have been added to $pids and the script is interrupted.  However,
358 # the part of the window where it matter is very small.
359 retcode=0
360 for n in $nodes ; do
361     set -o pipefail 2>/dev/null
362
363     # The following code applies stdout_filter and stderr_filter to
364     # the relevant streams.  Both filters are at the end of pipes so
365     # they read from stdin and (by default) write to stdout.  To allow
366     # the filters to operate independently, the output of
367     # stdout_filter is sent to a temporary file descriptor (3), which
368     # is redirected back to stdout at the outermost level.
369     if $parallel ; then
370         {
371             exec 3>&1
372             {
373                 $SSH $ssh_opts $EXTRA_SSH_OPTS "$n" "$command" |
374                     stdout_filter >&3
375             } 2>&1 | stderr_filter
376         } &
377         pids="${pids} $!"
378     else
379         if $verbose ; then
380             echo >&2 ; echo ">> NODE: $n <<" >&2
381         fi
382
383         {
384             exec 3>&1
385             {
386                 $SSH $ssh_opts $EXTRA_SSH_OPTS "$n" "$command" |
387                     stdout_filter >&3
388             } 2>&1 | stderr_filter
389         }
390         [ $? = 0 ] || retcode=$?
391     fi
392 done
393
394 if $parallel ; then
395     for p in $pids; do
396         wait "$p"
397         [ $? = 0 ] || retcode=$?
398     done
399 fi
400
401 exit $retcode