When testing make the time taken for some operations more obvious.
[sahlberg/ctdb.git] / tests / scripts / ctdb_test_functions.bash
1 # Hey Emacs, this is a -*- shell-script -*- !!!  :-)
2
3 fail ()
4 {
5     echo "$*"
6     exit 1
7 }
8
9 ######################################################################
10
11 ctdb_test_begin ()
12 {
13     local name="$1"
14
15     teststarttime=$(date '+%s')
16     testduration=0
17
18     echo "--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--"
19     echo "Running test $name ($(date '+%T'))"
20     echo "--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--"
21 }
22
23 ctdb_test_end ()
24 {
25     local name="$1" ; shift
26     local status="$1" ; shift
27     # "$@" is command-line
28
29     local interp="SKIPPED"
30     local statstr=" (reason $*)"
31     if [ -n "$status" ] ; then
32         if [ $status -eq 0 ] ; then
33             interp="PASSED"
34             statstr=""
35             echo "ALL OK: $*"
36         else
37             interp="FAILED"
38             statstr=" (status $status)"
39             testfailures=$(($testfailures+1))
40         fi
41     fi
42
43     testduration=$(($(date +%s)-$teststarttime))
44
45     echo "=========================================================================="
46     echo "TEST ${interp}: ${name}${statstr} (duration: ${testduration}s)"
47     echo "=========================================================================="
48
49 }
50
51 test_exit ()
52 {
53     exit $(($testfailures+0))
54 }
55
56 ctdb_test_exit ()
57 {
58     local status=$?
59
60     trap - 0
61
62     [ $(($testfailures+0)) -eq 0 -a $status -ne 0 ] && testfailures=$status
63     status=$(($testfailures+0))
64
65     # Avoid making a test fail from this point onwards.  The test is
66     # now complete.
67     set +e
68
69     echo "*** TEST COMPLETE (RC=$status), CLEANING UP..."
70
71     eval "$ctdb_test_exit_hook" || true
72     unset ctdb_test_exit_hook
73
74     if $ctdb_test_restart_scheduled || \
75         ! onnode 0 CTDB_TEST_CLEANING_UP=1 $CTDB_TEST_WRAPPER cluster_is_healthy ; then
76
77         restart_ctdb
78     else
79         # This could be made unconditional but then we might get
80         # duplication from the recovery in restart_ctdb.  We want to
81         # leave the recovery in restart_ctdb so that future tests that
82         # might do a manual restart mid-test will benefit.
83         echo "Forcing a recovery..."
84         onnode 0 ctdb recover
85     fi
86
87     exit $status
88 }
89
90 ctdb_test_exit_hook_add ()
91 {
92     ctdb_test_exit_hook="${ctdb_test_exit_hook}${ctdb_test_exit_hook:+ ; }$*"
93 }
94
95 ctdb_test_run ()
96 {
97     local name="$1" ; shift
98     
99     [ -n "$1" ] || set -- "$name"
100
101     ctdb_test_begin "$name"
102
103     local status=0
104     "$@" || status=$?
105
106     ctdb_test_end "$name" "$status" "$*"
107     
108     return $status
109 }
110
111 ctdb_test_usage()
112 {
113     local status=${1:-2}
114     
115     cat <<EOF
116 Usage: $0 [option]
117
118 Options:        
119     -h, --help          show this screen.
120     -v, --version       show test case version.
121     --category          show the test category (ACL, CTDB, Samba ...).
122     -d, --description   show test case description.
123     --summary           show short test case summary.
124 EOF
125
126     exit $status
127 }
128
129 ctdb_test_version ()
130 {
131     [ -n "$CTDB_DIR" ] || fail "Can not determine version."
132
133     (cd "$CTDB_DIR" && git describe)
134 }
135
136 ctdb_test_cmd_options()
137 {
138     [ -n "$1" ] || return 0
139
140     case "$1" in
141         -h|--help)        ctdb_test_usage 0   ;;
142         -v|--version)     ctdb_test_version   ;;
143         --category)       echo "CTDB"         ;; 
144         -d|--description) test_info           ;;
145         --summary)        test_info | head -1 ;;
146         *)
147             echo "Error: Unknown parameter = $1"
148             echo
149             ctdb_test_usage 2
150             ;;
151     esac
152
153     exit 0
154 }
155
156 ctdb_test_init () 
157 {
158     scriptname=$(basename "$0")
159     testfailures=0
160     ctdb_test_restart_scheduled=false
161
162     ctdb_test_cmd_options $@
163
164     trap "ctdb_test_exit" 0
165 }
166
167 ctdb_test_check_real_cluster ()
168 {
169     [ -n "$CTDB_TEST_REAL_CLUSTER" ] && return 0
170
171     echo "ERROR: This test must be run on a real/virtual cluster, not local daemons."
172     return 1
173 }
174
175 ########################################
176
177 # Sets: $out
178 try_command_on_node ()
179 {
180     local nodespec="$1" ; shift
181
182     local verbose=false
183     local onnode_opts=""
184
185     while [ "${nodespec#-}" != "$nodespec" ] ; do
186         if [ "$nodespec" = "-v" ] ; then
187             verbose=true
188         else
189             onnode_opts="$nodespec"
190         fi
191         nodespec="$1" ; shift
192     done
193
194     local cmd="$*"
195
196     out=$(onnode -q $onnode_opts "$nodespec" "$cmd" 2>&1) || {
197
198         echo "Failed to execute \"$cmd\" on node(s) \"$nodespec\""
199         echo "$out"
200         return 1
201     }
202
203     if $verbose ; then
204         echo "Output of \"$cmd\":"
205         echo "$out"
206     fi
207 }
208
209 sanity_check_output ()
210 {
211     local min_lines="$1"
212     local regexp="$2" # Should be anchored as necessary.
213     local output="$3"
214
215     local ret=0
216
217     local num_lines=$(echo "$output" | wc -l)
218     echo "There are $num_lines lines of output"
219     if [ $num_lines -lt $min_lines ] ; then
220         echo "BAD: that's less than the required number (${min_lines})"
221         ret=1
222     fi
223
224     local status=0
225     local unexpected # local doesn't pass through status of command on RHS.
226     unexpected=$(echo "$output" | egrep -v "$regexp") || status=$?
227
228     # Note that this is reversed.
229     if [ $status -eq 0 ] ; then
230         echo "BAD: unexpected lines in output:"
231         echo "$unexpected" | cat -A
232         ret=1
233     else
234         echo "Output lines look OK"
235     fi
236
237     return $ret
238 }
239
240 sanity_check_ips ()
241 {
242     local ips="$1" # Output of "ctdb ip -n all"
243
244     echo "Sanity checking IPs..."
245
246     local x ipp prev
247     prev=""
248     while read x ipp ; do
249         [ "$ipp" = "-1" ] && break
250         if [ -n "$prev" -a "$ipp" != "$prev" ] ; then
251             echo "OK"
252             return 0
253         fi
254         prev="$ipp"
255     done <<<"$ips"
256
257     echo "BAD: a node was -1 or IPs are only assigned to one node"
258     echo "Are you running an old version of CTDB?"
259     return 1
260 }
261
262 #######################################
263
264 # Wait until either timeout expires or command succeeds.  The command
265 # will be tried once per second.
266 wait_until ()
267 {
268     local timeout="$1" ; shift # "$@" is the command...
269
270     echo -n "<${timeout}|"
271     local t=$timeout
272     while [ $t -gt 0 ] ; do
273         if "$@" ; then
274             echo "|$(($timeout - $t))|"
275             echo "OK"
276             return 0
277         fi
278         echo -n .
279         t=$(($t - 1))
280         sleep 1
281     done
282     
283     echo "*TIMEOUT*"
284     
285     return 1
286 }
287
288 sleep_for ()
289 {
290     echo -n "=${1}|"
291     for i in $(seq 1 $1) ; do
292         echo -n '.'
293         sleep 1
294     done
295     echo '|'
296 }
297
298 _cluster_is_healthy ()
299 {
300     local out x count line
301
302     out=$(ctdb -Y status 2>&1) || return 1
303
304     {
305         read x
306         count=0
307         while read line ; do
308             count=$(($count + 1))
309             [ "${line#:*:*:}" != "0:0:0:0:" ] && return 1
310         done
311         [ $count -gt 0 ] && return $?
312     } <<<"$out" # Yay bash!
313 }
314
315 cluster_is_healthy ()
316 {
317     if _cluster_is_healthy ; then
318         echo "Cluster is HEALTHY"
319         exit 0
320     else
321         echo "Cluster is UNHEALTHY"
322         if [ -z "$CTDB_TEST_CLEANING_UP" ] ; then
323             echo "DEBUG:"
324             local i
325             for i in "ctdb status" "onnode -q 0 onnode all ctdb scriptstatus" ; do
326                 echo "$i"
327                 $i || true
328             done
329         fi
330         exit 1
331     fi
332 }
333
334 wait_until_healthy ()
335 {
336     local timeout="${1:-120}"
337
338     echo "Waiting for cluster to become healthy..."
339
340     wait_until 120 _cluster_is_healthy
341 }
342
343 # This function is becoming nicely overloaded.  Soon it will collapse!  :-)
344 node_has_status ()
345 {
346     local pnn="$1"
347     local status="$2"
348
349     local bits fpat mpat
350     case "$status" in
351         (unhealthy)    bits="?:?:?:1" ;;
352         (healthy)      bits="?:?:?:0" ;;
353         (disconnected) bits="1:?:?:?" ;;
354         (connected)    bits="0:?:?:?" ;;
355         (banned)       bits="?:1:?:?" ;;
356         (unbanned)     bits="?:0:?:?" ;;
357         (disabled)     bits="?:?:1:?" ;;
358         (enabled)      bits="?:?:0:?" ;;
359         (frozen)       fpat='^[[:space:]]+frozen[[:space:]]+1$' ;;
360         (unfrozen)     fpat='^[[:space:]]+frozen[[:space:]]+0$' ;;
361         (monon)        mpat='^Monitoring mode:ACTIVE \(0\)$' ;;
362         (monoff)       mpat='^Monitoring mode:DISABLED \(1\)$' ;;
363         *)
364             echo "node_has_status: unknown status \"$status\""
365             return 1
366     esac
367
368     if [ -n "$bits" ] ; then
369         local out x line
370
371         out=$(ctdb -Y status 2>&1) || return 1
372
373         {
374             read x
375             while read line ; do
376                 [ "${line#:${pnn}:*:${bits}:}" = "" ] && return 0
377             done
378             return 1
379         } <<<"$out" # Yay bash!
380     elif [ -n "$fpat" ] ; then
381         ctdb statistics -n "$pnn" | egrep -q "$fpat"
382     elif [ -n "$mpat" ] ; then
383         ctdb getmonmode -n "$pnn" | egrep -q "$mpat"
384     else
385         echo 'node_has_status: unknown mode, neither $bits nor $fpat is set'
386         return 1
387     fi
388 }
389
390 wait_until_node_has_status ()
391 {
392     local pnn="$1"
393     local status="$2"
394     local timeout="${3:-30}"
395
396     echo "Waiting until node $pnn has status \"$status\"..."
397
398     wait_until $timeout node_has_status "$pnn" "$status"
399 }
400
401 # Useful for superficially testing IP failover.
402 # IPs must be on nodes matching nodeglob.
403 ips_are_on_nodeglob ()
404 {
405     local nodeglob="$1" ; shift
406     local ips="$*"
407
408     local out
409
410     try_command_on_node 1 ctdb ip -n all
411
412     while read ip pnn ; do
413         for check in $ips ; do
414             if [ "$check" = "$ip" ] ; then
415                 case "$pnn" in
416                     ($nodeglob) : ;;
417                     (*) return 1  ;;
418                 esac
419                 ips="${ips/${ip}}" # Remove from list
420             fi
421         done
422     done <<<"$out" # bashism to avoid problem setting variable in pipeline.
423
424     ips="${ips// }" # Remove any spaces.
425     [ -z "$ips" ]
426 }
427
428 wait_until_ips_are_on_nodeglob ()
429 {
430     echo "Waiting for IPs to fail over..."
431
432     wait_until 60 ips_are_on_nodeglob "$@"
433 }
434
435 get_src_socket ()
436 {
437     local proto="$1"
438     local dst_socket="$2"
439     local pid="$3"
440     local prog="$4"
441
442     local pat="^${proto}[[:space:]]+[[:digit:]]+[[:space:]]+[[:digit:]]+[[:space:]]+[^[:space:]]+[[:space:]]+${dst_socket//./\\.}[[:space:]]+ESTABLISHED[[:space:]]+${pid}/${prog}[[:space:]]*\$"
443     out=$(netstat -tanp |
444         egrep "$pat" |
445         awk '{ print $4 }')
446
447     [ -n "$out" ]
448 }
449
450 wait_until_get_src_socket ()
451 {
452     local proto="$1"
453     local dst_socket="$2"
454     local pid="$3"
455     local prog="$4"
456
457     echo "Waiting for ${prog} to establish connection to ${dst_socket}..."
458
459     wait_until 5 get_src_socket "$@"
460 }
461
462 #######################################
463
464 # filename will be in $tcpdump_filename, pid in $tcpdump_pid
465 tcpdump_start ()
466 {
467     tcpdump_filter="$1" # global
468
469     echo "Running tcpdump..."
470     tcpdump_filename=$(mktemp)
471     ctdb_test_exit_hook_add "rm -f $tcpdump_filename"
472
473     # The only way of being sure that tcpdump is listening is to send
474     # some packets that it will see.  So we use dummy pings - the -U
475     # option to tcpdump ensures that packets are flushed to the file
476     # as they are captured.
477     local dummy_addr="127.3.2.1"
478     local dummy="icmp and dst host ${dummy_addr} and icmp[icmptype] == icmp-echo"
479     tcpdump -n -p -s 0 -e -U -w $tcpdump_filename -i any "($tcpdump_filter) or ($dummy)" &
480     ctdb_test_exit_hook_add "kill $! >/dev/null 2>&1"
481
482     echo "Waiting for tcpdump output file to be ready..."
483     ping -q "$dummy_addr" >/dev/null 2>&1 &
484     ctdb_test_exit_hook_add "kill $! >/dev/null 2>&1"
485
486     tcpdump_listen_for_dummy ()
487     {
488         tcpdump -n -r $tcpdump_filename -c 1 "$dummy" >/dev/null 2>&1
489     }
490
491     wait_until 10 tcpdump_listen_for_dummy
492 }
493
494 # By default, wait for 1 matching packet.
495 tcpdump_wait ()
496 {
497     local count="${1:-1}"
498     local filter="${2:-${tcpdump_filter}}"
499
500     tcpdump_check ()
501     {
502         local found=$(tcpdump -n -r $tcpdump_filename "$filter" 2>/dev/null | wc -l)
503         [ $found -ge $count ]
504     }
505
506     echo "Waiting for tcpdump to capture some packets..."
507     if ! wait_until 30 tcpdump_check ; then
508         echo "DEBUG:"
509         local i
510         for i in "ctdb status" "netstat -tanp" "tcpdump -n -e -r $tcpdump_filename" ; do
511             echo "$i"
512             $i || true
513         done
514         return 1
515     fi
516 }
517
518 tcpdump_show ()
519 {
520     local filter="${1:-${tcpdump_filter}}"
521
522     tcpdump -n -r $tcpdump_filename  "$filter" 2>/dev/null
523 }
524
525 tcptickle_sniff_start ()
526 {
527     local src="$1"
528     local dst="$2"
529
530     local in="src host ${dst%:*} and tcp src port ${dst##*:} and dst host ${src%:*} and tcp dst port ${src##*:}"
531     local out="src host ${src%:*} and tcp src port ${src##*:} and dst host ${dst%:*} and tcp dst port ${dst##*:}"
532     local tickle_ack="${in} and (tcp[tcpflags] & tcp-ack != 0) and (tcp[14] == 4) and (tcp[15] == 210)" # win == 1234
533     local ack_ack="${out} and (tcp[tcpflags] & tcp-ack != 0)"
534     tcptickle_reset="${in} and tcp[tcpflags] & tcp-rst != 0"
535     local filter="(${tickle_ack}) or (${ack_ack}) or (${tcptickle_reset})"
536
537     tcpdump_start "$filter"
538 }
539
540 tcptickle_sniff_wait_show ()
541 {
542     tcpdump_wait 1 "$tcptickle_reset"
543
544     echo "GOOD: here are some TCP tickle packets:"
545     tcpdump_show
546 }
547
548
549 #######################################
550
551 daemons_stop ()
552 {
553     echo "Attempting to politely shutdown daemons..."
554     onnode 1 ctdb shutdown -n all || true
555
556     echo "Sleeping for a while..."
557     sleep_for 1
558
559     if pgrep -f $CTDB_DIR/bin/ctdbd >/dev/null ; then
560         echo "Killing remaining daemons..."
561         pkill -f $CTDB_DIR/bin/ctdbd
562
563         if pgrep -f $CTDB_DIR/bin/ctdbd >/dev/null ; then
564             echo "Once more with feeling.."
565             pkill -9 $CTDB_DIR/bin/ctdbd
566         fi
567     fi
568
569     local var_dir=$CTDB_DIR/tests/var
570     rm -rf $var_dir/test.db
571 }
572
573 daemons_setup ()
574 {
575     local num_nodes="${1:-2}" # default is 2 nodes
576
577     local var_dir=$CTDB_DIR/tests/var
578
579     mkdir -p $var_dir/test.db/persistent
580
581     local nodes=$var_dir/nodes.txt
582     local public_addresses=$var_dir/public_addresses.txt
583     local no_public_addresses=$var_dir/no_public_addresses.txt
584     rm -f $nodes $public_addresses $no_public_addresses
585
586     # If there are (strictly) greater than 2 nodes then we'll randomly
587     # choose a node to have no public addresses.
588     local no_public_ips=-1
589     [ $num_nodes -gt 2 ] && no_public_ips=$(($RANDOM % $num_nodes))
590     echo "$no_public_ips" >$no_public_addresses
591
592     local i
593     for i in $(seq 1 $num_nodes) ; do
594         if [ "${CTDB_USE_IPV6}x" != "x" ]; then
595             echo ::$i >> $nodes
596             ip addr add ::$i/128 dev lo
597         else
598             echo 127.0.0.$i >> $nodes
599             # 2 public addresses on most nodes, just to make things interesting.
600             if [ $(($i - 1)) -ne $no_public_ips ] ; then
601                 echo "192.0.2.$i/24 lo" >> $public_addresses
602                 echo "192.0.2.$(($i + $num_nodes))/24 lo" >> $public_addresses
603             fi
604         fi
605     done
606 }
607
608 daemons_start ()
609 {
610     local num_nodes="${1:-2}" # default is 2 nodes
611     shift # "$@" gets passed to ctdbd
612
613     local var_dir=$CTDB_DIR/tests/var
614
615     local nodes=$var_dir/nodes.txt
616     local public_addresses=$var_dir/public_addresses.txt
617     local no_public_addresses=$var_dir/no_public_addresses.txt
618
619     local no_public_ips=-1
620     [ -r $no_public_addresses ] && read no_public_ips <$no_public_addresses
621
622     local ctdb_options="--reclock=$var_dir/rec.lock --nlist $nodes --nopublicipcheck --event-script-dir=$CTDB_DIR/tests/events.d --logfile=$var_dir/daemons.log -d 0 --dbdir=$var_dir/test.db --dbdir-persistent=$var_dir/test.db/persistent"
623
624     echo "Starting $num_nodes ctdb daemons..."
625     if  [ "$no_public_ips" != -1 ] ; then
626         echo "Node $no_public_ips will have no public IPs."
627     fi
628
629     for i in $(seq 0 $(($num_nodes - 1))) ; do
630         if [ $(id -u) -eq 0 ]; then
631             ctdb_options="$ctdb_options --public-interface=lo"
632         fi
633
634         if [ $i -eq $no_public_ips ] ; then
635             ctdb_options="$ctdb_options --public-addresses=/dev/null"
636         else
637             ctdb_options="$ctdb_options --public-addresses=$public_addresses"
638         fi
639
640         # Need full path so we can use "pkill -f" to kill the daemons.
641         $VALGRIND $CTDB_DIR/bin/ctdbd --socket=$var_dir/sock.$i $ctdb_options "$@" ||return 1
642     done
643
644     if [ -L /tmp/ctdb.socket -o ! -S /tmp/ctdb.socket ] ; then 
645         ln -sf $var_dir/sock.0 /tmp/ctdb.socket || return 1
646     fi
647 }
648
649 #######################################
650
651 _restart_ctdb ()
652 {
653     if [ -e /etc/redhat-release ] ; then
654         service ctdb restart
655     else
656         /etc/init.d/ctdb restart
657     fi
658 }
659
660 setup_ctdb ()
661 {
662     if [ -n "$CTDB_NODES_SOCKETS" ] ; then
663         daemons_setup $CTDB_TEST_NUM_DAEMONS
664     fi
665 }
666
667 restart_ctdb ()
668 {
669     echo -n "Restarting CTDB"
670     if $ctdb_test_restart_scheduled ; then
671         echo -n " (scheduled)"
672     fi
673     echo "..."
674     
675     if [ -n "$CTDB_NODES_SOCKETS" ] ; then
676         daemons_stop
677         daemons_start $CTDB_TEST_NUM_DAEMONS
678     else
679         onnode -pq all $CTDB_TEST_WRAPPER _restart_ctdb 
680     fi || return 1
681         
682     onnode -q 1  $CTDB_TEST_WRAPPER wait_until_healthy || return 1
683
684     echo "Setting RerecoveryTimeout to 1"
685     onnode -pq all "ctdb setvar RerecoveryTimeout 1"
686
687     # In recent versions of CTDB, forcing a recovery like this blocks
688     # until the recovery is complete.  Hopefully this will help the
689     # cluster to stabilise before a subsequent test.
690     echo "Forcing a recovery..."
691     onnode -q 0 ctdb recover
692     sleep_for 1
693     echo "Forcing a recovery..."
694     onnode -q 0 ctdb recover
695
696     echo "ctdb is ready"
697 }
698
699 ctdb_restart_when_done ()
700 {
701     ctdb_test_restart_scheduled=true
702 }
703
704 #######################################
705
706 install_eventscript ()
707 {
708     local script_name="$1"
709     local script_contents="$2"
710
711     if [ -n "$CTDB_TEST_REAL_CLUSTER" ] ; then
712         # The quoting here is *very* fragile.  However, we do
713         # experience the joy of installing a short script using
714         # onnode, and without needing to know the IP addresses of the
715         # nodes.
716         onnode all "f=\"\${CTDB_BASE:-/etc/ctdb}/events.d/${script_name}\" ; echo \"Installing \$f\" ; echo '${script_contents}' > \"\$f\" ; chmod 755 \"\$f\""
717     else
718         f="${CTDB_DIR}/tests/events.d/${script_name}"
719         echo "$script_contents" >"$f"
720         chmod 755 "$f"
721     fi
722 }
723
724 uninstall_eventscript ()
725 {
726     local script_name="$1"
727
728     if [ -n "$CTDB_TEST_REAL_CLUSTER" ] ; then
729         onnode all "rm -vf \"\${CTDB_BASE:-/etc/ctdb}/events.d/${script_name}\""
730     else
731         rm -vf "${CTDB_DIR}/tests/events.d/${script_name}"
732     fi
733 }