ctdb-common: Rename system utility files
[bbaumbach/samba-autobuild/.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, overriding default.
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 ctdb_nodes_file=""
61 parallel=false
62 verbose=false
63 quiet=false
64 prefix=""
65 names_ok=false
66 push=false
67 stdin=false
68
69 if [ -z "$CTDB_BASE" ] ; then
70     CTDB_BASE="/usr/local/etc/ctdb"
71 fi
72
73 parse_options ()
74 {
75     # $POSIXLY_CORRECT means that the command passed to onnode can
76     # take options and getopt won't reorder things to make them
77     # options ot onnode.
78     local temp
79     # Not on the previous line - local returns 0!
80     temp=$(POSIXLY_CORRECT=1 getopt -n "$prog" -o "cf:hno:pqvPi" -l help -- "$@")
81
82     # No! Checking the exit code afterwards is actually clearer...
83     # shellcheck disable=SC2181
84     [ $? -eq 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         # No! Checking the exit code afterwards is actually clearer...
151         # shellcheck disable=SC2181
152         if [ $? -ne 0 ] ; then
153             echo "${prog}: unable to get status of CTDB nodes" >&2
154             echo "$ctdb_status_output" >&2
155             exit 1
156         fi
157         local nl="
158 "
159         ctdb_status_output="${ctdb_status_output#*${nl}}"
160     fi
161
162     (
163         local i
164         IFS="${IFS}|"
165         while IFS="" read i ; do
166
167             # Intentional word splitting
168             # shellcheck disable=SC2086
169             set -- $i # split line on colons
170             shift     # line starts with : so 1st field is empty
171             local pnn="$1" ; shift
172             shift # ignore IP address but need status bits below
173
174             case "$status" in
175                 healthy)
176                     # If any bit is 1, don't match this address.
177                     local s
178                     for s ; do
179                         [ "$s" != "1" ] || continue 2
180                     done
181                     ;;
182                 connected)
183                     # If disconnected bit is not 0, don't match this address.
184                     [ "$1" = "0" ] || continue
185                     ;;
186                 *)
187                     invalid_nodespec
188             esac
189
190             # Intentional multi-word expansion
191             # shellcheck disable=SC2086
192             echo_nth "$pnn" $all_nodes
193         done <<<"$ctdb_status_output"
194     )
195 }
196
197 get_any_available_node ()
198 {
199     local all_nodes="$1"
200
201     # We do a recursive onnode to find which nodes are up and running.
202     local out line
203     out=$("$0" -pq all ctdb pnn 2>&1)
204     while read line ; do
205         if [[ "$line" =~ ^[0-9]+$ ]] ; then
206             local pnn="$line"
207             # Intentional multi-word expansion
208             # shellcheck disable=SC2086
209             echo_nth "$pnn" $all_nodes
210             return 0
211         fi
212         # Else must be an error message from a down node.
213     done <<<"$out"
214     return 1
215 }
216
217 get_nodes ()
218 {
219         local all_nodes
220
221         local f="${CTDB_BASE}/nodes"
222         if [ -n "$ctdb_nodes_file" ] ; then
223                 f="$ctdb_nodes_file"
224                 if [ ! -e "$f" -a "${f#/}" = "$f" ] ; then
225                         # $f is relative, try in $CTDB_BASE
226                         f="${CTDB_BASE}/${f}"
227                 fi
228         fi
229
230         if [ ! -r "$f" ] ; then
231                 echo "${prog}: unable to open nodes file  \"${f}\"" >&2
232                 exit 1
233         fi
234
235         all_nodes=$(sed -e 's@#.*@@g' -e 's@ *@@g' -e 's@^$@#DEAD@' "$f")
236
237         local n nodes
238         nodes=$(parse_nodespec "$1") || exit $?
239         for n in $nodes ; do
240                 case "$n" in
241                 all)
242                         echo "${all_nodes//#DEAD/}"
243                         ;;
244                 any)
245                         get_any_available_node "$all_nodes" || exit 1
246                         ;;
247                 ok|healthy)
248                         get_nodes_with_status "$all_nodes" "healthy" || exit 1
249                         ;;
250                 con|connected)
251                         get_nodes_with_status "$all_nodes" "connected" || exit 1
252                         ;;
253                 [0-9]|[0-9][0-9]|[0-9][0-9][0-9])
254                         # Intentional multi-word expansion
255                         # shellcheck disable=SC2086
256                         echo_nth "$n" $all_nodes
257                         ;;
258                 *)
259                         $names_ok || invalid_nodespec
260                         echo "$n"
261                 esac
262         done
263 }
264
265 push()
266 {
267     local host="$1"
268     local files="$2"
269
270     local f
271     for f in $files ; do
272         $verbose && echo "Pushing $f"
273         case "$f" in
274             /*) rsync "$f" "[${host}]:${f}" ;;
275             *)  rsync "${PWD}/${f}" "[${host}]:${PWD}/${f}" ;;
276         esac
277     done
278 }
279
280 stdout_filter ()
281 {
282     if [ -n "$prefix" ] ; then
283         cat >"${prefix}.${n//\//_}"
284     elif $verbose && $parallel ; then
285         sed -e "s@^@[$n] @"
286     else
287         cat
288     fi
289 }
290
291 stderr_filter ()
292 {
293     if $verbose && $parallel ; then
294         sed -e "s@^@[$n] @"
295     else
296         cat
297     fi
298 }
299
300 ######################################################################
301
302 parse_options "$@"
303
304 ssh_opts=
305 if $push ; then
306         ONNODE_SSH=push
307         ONNODE_SSH_OPTS=""
308 else
309         $current && command="cd $PWD && $command"
310
311         # Could "2>/dev/null || true" but want to see errors from typos in file.
312         [ -r "${CTDB_BASE}/onnode.conf" ] && . "${CTDB_BASE}/onnode.conf"
313         [ -n "$ONNODE_SSH" ] || ONNODE_SSH=ssh
314         if [ "$ONNODE_SSH" = "ssh" ] ; then
315                 if $parallel || ! $stdin ; then
316                         ssh_opts="-n"
317                 fi
318         else
319                 : # rsh? All bets are off!
320         fi
321 fi
322
323 ######################################################################
324
325 nodes=$(get_nodes "$nodespec") || exit $?
326
327 if $quiet ; then
328     verbose=false
329 else
330     # If $nodes contains a space or a newline then assume multiple nodes.
331     nl="
332 "
333     [ "$nodes" != "${nodes%[ ${nl}]*}" ] && verbose=true
334 fi
335
336 pids=""
337 # Intentional multi-word expansion
338 # shellcheck disable=SC2086
339 trap 'kill -TERM $pids 2>/dev/null' INT TERM
340 # There's a small race here where the kill can fail if no processes
341 # have been added to $pids and the script is interrupted.  However,
342 # the part of the window where it matter is very small.
343 retcode=0
344 for n in $nodes ; do
345         set -o pipefail 2>/dev/null
346
347         # The following code applies stdout_filter and stderr_filter to
348         # the relevant streams.  Both filters are at the end of pipes so
349         # they read from stdin and (by default) write to stdout.  To allow
350         # the filters to operate independently, the output of
351         # stdout_filter is sent to a temporary file descriptor (3), which
352         # is redirected back to stdout at the outermost level.
353         ssh_cmd="$ONNODE_SSH $ssh_opts $ONNODE_SSH_OPTS"
354         if $parallel ; then
355                 {
356                         exec 3>&1
357                         {
358                                 $ssh_cmd "$n" "$command" 3>&- |
359                                         stdout_filter >&3
360                         } 2>&1 | stderr_filter
361                 } &
362                 pids="${pids} $!"
363         else
364                 if $verbose ; then
365                         echo >&2 ; echo ">> NODE: $n <<" >&2
366                 fi
367                 {
368                         $ssh_cmd "$n" "$command" | stdout_filter
369                 } || retcode=$?
370         fi
371 done
372
373 if $parallel ; then
374         for p in $pids; do
375                 wait "$p" || retcode=$?
376         done
377 fi
378
379 exit $retcode