11ad70869321ca404c13b07148077bc2191b13da
[ctdb.git] / 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     -o <prefix> Save standard output from each node to file <prefix>.<ip>
35     -p          Run command in parallel on specified nodes.
36     -q          Do not print node addresses (overrides -v).
37     -n          Allow nodes to be specified by name.
38     -f          Specify nodes file, overrides CTDB_NODES_FILE.
39     -v          Print node address even for a single node.
40     -P          Push given files to nodes instead of running commands.
41   <NODES>       "all", "any", "ok" (or "healthy"), "con" (or "connected"),
42                 "rm" (or "recmaster"), "lvs" (or "lvsmaster"),
43                 "natgw" (or "natgwlist"); or
44                 a node number (0 base); or
45                 a hostname (if -n is specified); or
46                 list (comma separated) of <NODES>; or
47                 range (hyphen separated) of node numbers.
48 EOF
49     exit 1
50
51 }
52
53 invalid_nodespec ()
54 {
55     echo "Invalid <nodespec>" >&2 ; echo >&2
56     usage
57 }
58
59 # Defaults.
60 current=false
61 parallel=false
62 verbose=false
63 quiet=false
64 prefix=""
65 names_ok=false
66 push=false
67
68 ctdb_base="${CTDB_BASE:-/etc/ctdb}"
69
70 parse_options ()
71 {
72     # $POSIXLY_CORRECT means that the command passed to onnode can
73     # take options and getopt won't reorder things to make them
74     # options ot onnode.
75     local temp
76     # Not on the previous line - local returns 0!
77     temp=$(POSIXLY_CORRECT=1 getopt -n "$prog" -o "cf:hno:pqvP" -l help -- "$@")
78
79     [ $? != 0 ] && usage
80
81     eval set -- "$temp"
82
83     while true ; do
84         case "$1" in
85             -c) current=true ; shift ;;
86             -f) CTDB_NODES_FILE="$2" ; shift 2 ;;
87             -n) names_ok=true ; shift ;;
88             -o) prefix="$2" ; shift 2 ;;
89             -p) parallel=true ; shift ;;
90             -q) quiet=true ; shift ;;
91             -v) verbose=true ; shift ;;
92             -P) push=true ; shift ;;
93             --) shift ; break ;;
94             -h|--help|*) usage ;; # Shouldn't happen, so this is reasonable.
95         esac
96     done
97
98     [ $# -lt 2 ] && usage
99
100     nodespec="$1" ; shift
101     command="$@"
102 }
103
104 echo_nth ()
105 {
106     local n="$1" ; shift
107
108     shift $n
109     local node="$1"
110
111     if [ -n "$node" -a "$node" != "#DEAD" ] ; then
112         echo $node
113     else
114         echo "${prog}: \"node ${n}\" does not exist" >&2
115         exit 1
116     fi
117 }
118
119 parse_nodespec ()
120 {
121     # Subshell avoids hacks to restore $IFS.
122     (
123         IFS=","
124         for i in $1 ; do
125             case "$i" in
126                 *-*) seq "${i%-*}" "${i#*-}" 2>/dev/null || invalid_nodespec ;;
127                 # Separate lines for readability.
128                 all|any|ok|healthy|con|connected) echo "$i" ;;
129                 rm|recmaster|lvs|lvsmaster|natgw|natgwlist) echo "$i" ;;
130                 *)
131                     [ $i -gt -1 ] 2>/dev/null || $names_ok || invalid_nodespec
132                     echo $i
133             esac
134         done
135     )
136 }
137
138 ctdb_status_output="" # cache
139 get_nodes_with_status ()
140 {
141     local all_nodes="$1"
142     local status="$2"
143
144     if [ -z "$ctdb_status_output" ] ; then
145         ctdb_status_output=$(ctdb -Y status 2>&1)
146         if [ $? -ne 0 ] ; then
147             echo "${prog}: unable to get status of CTDB nodes" >&2
148             echo "$ctdb_status_output" >&2
149             exit 1
150         fi
151         local nl="
152 "
153         ctdb_status_output="${ctdb_status_output#*${nl}}"
154     fi
155
156     (
157         local i
158         IFS="${IFS}:"
159         while IFS="" read i ; do
160
161             set -- $i # split line on colons
162             shift     # line starts with : so 1st field is empty
163             local pnn="$1" ; shift
164             local ip="$1" ; shift
165
166             case "$status" in
167                 healthy)
168                     # If any bit is not 0, don't match this address.
169                     local s
170                     for s ; do
171                         [ "$s" = "0" ] || continue 2
172                     done
173                     ;;
174                 connected)
175                     # If disconnected bit is not 0, don't match this address.
176                     [ "$1" = "0" ] || continue
177                     ;;
178                 *)
179                     invalid_nodespec
180             esac
181
182             echo_nth "$pnn" $all_nodes
183         done <<<"$ctdb_status_output"
184     )
185 }
186
187 ctdb_props="" # cache
188 get_node_with_property ()
189 {
190     local all_nodes="$1"
191     local prop="$2"
192
193     local prop_node=""
194     if [ "${ctdb_props##:${prop}:}" = "$ctdb_props" ] ; then
195         # Not in cache.
196         prop_node=$(ctdb "$prop" -Y 2>/dev/null)
197         if [ $? -eq 0 ] ; then
198             if [ "$prop" = "natgwlist" ] ; then
199                 prop_node="${prop_node%% *}" # 1st word
200                 if [ "$prop_node" = "-1" ] ; then
201                     # This works around natgwlist returning 0 even
202                     # when there's no natgw.
203                     prop_node=""
204                 fi
205             else
206                 # We only want the first line.
207                 local nl="
208 "
209                 prop_node="${prop_node%%${nl}*}"
210             fi
211         else
212             prop_node=""
213         fi
214
215         if [ -n "$prop_node" ] ; then
216             # Add to cache.
217             ctdb_props="${ctdb_props}${ctdb_props:+ }:${prop}:${prop_node}"
218         fi
219     else
220         # Get from cache.
221         prop_node="${ctdb_props##:${prop}:}"
222         prop_node="${prop_node%% *}"
223     fi
224
225     if [ -n "$prop_node" ] ; then
226         echo_nth "$prop_node" $all_nodes
227     else
228         echo "${prog}: No ${prop} available" >&2
229         exit 1
230     fi
231 }
232
233 get_any_available_node ()
234 {
235     local all_nodes="$1"
236
237     # We do a recursive onnode to find which nodes are up and running.
238     local out=$($0 -pq all ctdb pnn 2>&1)
239     local line
240     while read line ; do 
241         local pnn="${line#PNN:}"
242         if [ "$pnn" != "$line" ] ; then
243             echo_nth "$pnn" $all_nodes
244             return 0
245         fi
246         # Else must be an error message from a down node.
247     done <<<"$out"
248     return 1
249 }
250
251 get_nodes ()
252 {
253     local all_nodes
254
255     if [ -n "$CTDB_NODES_SOCKETS" ] ; then 
256         all_nodes="$CTDB_NODES_SOCKETS"
257     else
258         local f="${ctdb_base}/nodes"
259         if [ -n "$CTDB_NODES_FILE" ] ; then
260             f="$CTDB_NODES_FILE"
261             if [ ! -e "$f" -a "${f#/}" = "$f" ] ; then
262                 # $f is relative, try in $ctdb_base
263                 f="${ctdb_base}/${f}"
264             fi
265         fi
266
267         if [ ! -r "$f" ] ; then
268             echo "${prog}: unable to open nodes file  \"${f}\"" >&2
269             exit 1
270         fi
271
272         all_nodes=$(sed -e 's@#.*@@g' -e 's@ *@@g' -e 's@^$@#DEAD@' "$f")
273     fi
274
275     local nodes=""
276     local n
277     for n in $(parse_nodespec "$1") ; do
278         [ $? != 0 ] && exit 1  # Required to catch exit in above subshell.
279         case "$n" in
280             all)
281                 echo "${all_nodes//#DEAD/}"
282                 ;;
283             any)
284                 get_any_available_node "$all_nodes" || exit 1
285                 ;;
286             ok|healthy) 
287                 get_nodes_with_status "$all_nodes" "healthy" || exit 1
288                 ;;
289             con|connected) 
290                 get_nodes_with_status "$all_nodes" "connected" || exit 1
291                 ;;
292             rm|recmaster)
293                 get_node_with_property "$all_nodes" "recmaster" || exit 1
294                 ;;
295             lvs|lvsmaster)
296                 get_node_with_property "$all_nodes" "lvsmaster" || exit 1
297                 ;;
298             natgw|natgwlist)
299                 get_node_with_property "$all_nodes" "natgwlist" || exit 1
300                 ;;
301             [0-9]|[0-9][0-9]|[0-9][0-9][0-9])
302                 echo_nth $n $all_nodes
303                 ;;
304             *)
305                 $names_ok || invalid_nodespec
306                 echo $n
307         esac
308     done
309 }
310
311 push()
312 {
313     local host="$1"
314     local files="$2"
315
316     local f
317     for f in $files ; do
318         $verbose && echo "Pushing $f"
319         case "$f" in
320             /*) rsync "$f" "${host}:${f}" ;;
321             *)  rsync "${PWD}/${f}" "${host}:${PWD}/${f}" ;;
322         esac
323     done
324 }
325
326 fakessh ()
327 {
328     CTDB_SOCKET="$1" sh -c "$2" 3>/dev/null
329 }
330
331 stdout_filter ()
332 {
333     if [ -n "$prefix" ] ; then
334         cat >"${prefix}.${n//\//_}"
335     elif $verbose && $parallel ; then
336         sed -e "s@^@[$n] @"
337     else
338         cat
339     fi
340 }
341
342 stderr_filter ()
343 {
344     if $verbose && $parallel ; then
345         sed -e "s@^@[$n] @"
346     else
347         cat
348     fi
349 }
350
351 ######################################################################
352
353 parse_options "$@"
354
355 ssh_opts=
356 if $push ; then
357     SSH=push
358     EXTRA_SSH_OPTS=""
359 else
360     $current && command="cd $PWD && $command"
361
362     if [ -n "$CTDB_NODES_SOCKETS" ] ; then
363         SSH=fakessh
364         EXTRA_SSH_OPTS=""
365     else 
366         # Could "2>/dev/null || true" but want to see errors from typos in file.
367         [ -r "${ctdb_base}/onnode.conf" ] && . "${ctdb_base}/onnode.conf"
368         [ -n "$SSH" ] || SSH=ssh
369         if [ "$SSH" = "ssh" ] ; then
370             ssh_opts="-n"
371         else
372             : # rsh? All bets are off!
373         fi
374     fi
375 fi
376
377 ######################################################################
378
379 nodes=$(get_nodes "$nodespec")
380 [ $? != 0 ] && exit 1   # Required to catch exit in above subshell.
381
382 if $quiet ; then
383     verbose=false
384 else
385     # If $nodes contains a space or a newline then assume multiple nodes.
386     nl="
387 "
388     [ "$nodes" != "${nodes%[ ${nl}]*}" ] && verbose=true
389 fi
390
391 pids=""
392 trap 'kill -TERM $pids 2>/dev/null' INT TERM
393 # There's a small race here where the kill can fail if no processes
394 # have been added to $pids and the script is interrupted.  However,
395 # the part of the window where it matter is very small.
396 retcode=0
397 for n in $nodes ; do
398     set -o pipefail 2>/dev/null
399     if $parallel ; then
400         { exec 3>&1 ; { $SSH $ssh_opts $EXTRA_SSH_OPTS $n "$command" | stdout_filter >&3 ; } 2>&1 | stderr_filter ; } &
401         pids="${pids} $!"
402     else
403         if $verbose ; then
404             echo >&2 ; echo ">> NODE: $n <<" >&2
405         fi
406
407         { exec 3>&1 ; { $SSH $ssh_opts $EXTRA_SSH_OPTS $n "$command" | stdout_filter >&3 ; } 2>&1 | stderr_filter ; }
408         [ $? = 0 ] || retcode=$?
409     fi
410 done
411
412 $parallel && {
413     for p in $pids; do
414         wait $p
415         [ $? = 0 ] || retcode=$?
416     done
417 }
418
419 exit $retcode