ctdb-tests: Make integration.bash pass shellcheck
[vlendec/samba-autobuild/.git] / ctdb / tests / scripts / integration.bash
1 # Hey Emacs, this is a -*- shell-script -*- !!!  :-)
2
3 . "${TEST_SCRIPTS_DIR}/common.sh"
4
5 ######################################################################
6
7 export CTDB_TIMEOUT=60
8
9 if [ -n "$CTDB_TEST_REMOTE_DIR" ] ; then
10         CTDB_TEST_WRAPPER="${CTDB_TEST_REMOTE_DIR}/test_wrap"
11 else
12         _d=$(cd "$TEST_SCRIPTS_DIR" &&  echo "$PWD")
13         CTDB_TEST_WRAPPER="$_d/test_wrap"
14 fi
15 export CTDB_TEST_WRAPPER
16
17 # If $VALGRIND is set then use it whenever ctdb is called, but only if
18 # $CTDB is not already set.
19 [ -n "$CTDB" ] || export CTDB="${VALGRIND}${VALGRIND:+ }ctdb"
20
21 # why???
22 PATH="${TEST_SCRIPTS_DIR}:${PATH}"
23
24 ######################################################################
25
26 ctdb_test_on_cluster ()
27 {
28         [ -z "$CTDB_TEST_LOCAL_DAEMONS" ]
29 }
30
31 ctdb_test_exit ()
32 {
33     local status=$?
34
35     trap - 0
36
37     # run_tests.sh pipes stdout into tee.  If the tee process is
38     # killed then any attempt to write to stdout (e.g. echo) will
39     # result in SIGPIPE, terminating the caller.  Ignore SIGPIPE to
40     # ensure that all clean-up is run.
41     trap '' PIPE
42
43     # Avoid making a test fail from this point onwards.  The test is
44     # now complete.
45     set +e
46
47     echo "*** TEST COMPLETED (RC=$status) AT $(date '+%F %T'), CLEANING UP..."
48
49     eval "$ctdb_test_exit_hook" || true
50     unset ctdb_test_exit_hook
51
52     echo "Stopping cluster..."
53     ctdb_nodes_stop || ctdb_test_error "Cluster shutdown failed"
54
55     exit $status
56 }
57
58 ctdb_test_exit_hook_add ()
59 {
60     ctdb_test_exit_hook="${ctdb_test_exit_hook}${ctdb_test_exit_hook:+ ; }$*"
61 }
62
63 # Setting cleanup_pid to <pid>@<node> will cause <pid> to be killed on
64 # <node> when the test completes.  To cancel, just unset cleanup_pid.
65 ctdb_test_cleanup_pid=""
66 ctdb_test_cleanup_pid_exit_hook ()
67 {
68         if [ -n "$ctdb_test_cleanup_pid" ] ; then
69                 local pid="${ctdb_test_cleanup_pid%@*}"
70                 local node="${ctdb_test_cleanup_pid#*@}"
71
72                 try_command_on_node "$node" "kill ${pid}"
73         fi
74 }
75
76 ctdb_test_exit_hook_add ctdb_test_cleanup_pid_exit_hook
77
78 ctdb_test_cleanup_pid_set ()
79 {
80         local node="$1"
81         local pid="$2"
82
83         ctdb_test_cleanup_pid="${pid}@${node}"
84 }
85
86 ctdb_test_cleanup_pid_clear ()
87 {
88         ctdb_test_cleanup_pid=""
89 }
90
91 ctdb_test_init ()
92 {
93         trap "ctdb_test_exit" 0
94
95         ctdb_nodes_stop >/dev/null 2>&1 || true
96
97         echo "Configuring cluster..."
98         setup_ctdb "$@" || ctdb_test_error "Cluster configuration failed"
99
100         echo "Starting cluster..."
101         ctdb_init || ctdb_test_error "Cluster startup failed"
102
103         echo  "*** SETUP COMPLETE AT $(date '+%F %T'), RUNNING TEST..."
104 }
105
106 ctdb_test_skip_on_cluster ()
107 {
108         if ctdb_test_on_cluster ; then
109                 ctdb_test_skip \
110                         "SKIPPING this test - only runs against local daemons"
111         fi
112 }
113
114
115 ctdb_nodes_restart ()
116 {
117         ctdb_nodes_stop "$@"
118         ctdb_nodes_start "$@"
119 }
120
121 ########################################
122
123 # Sets: $out, $outfile
124 # * The first 1KB of output is put into $out
125 # * Tests should use $outfile for handling large output
126 # * $outfile is removed after each test
127 out=""
128 outfile="${CTDB_TEST_TMP_DIR}/try_command_on_node.out"
129
130 outfile_cleanup ()
131 {
132         rm -f "$outfile"
133 }
134
135 ctdb_test_exit_hook_add outfile_cleanup
136
137 try_command_on_node ()
138 {
139     local nodespec="$1" ; shift
140
141     local verbose=false
142     local onnode_opts=""
143
144     while [ "${nodespec#-}" != "$nodespec" ] ; do
145         if [ "$nodespec" = "-v" ] ; then
146             verbose=true
147         else
148             onnode_opts="${onnode_opts}${onnode_opts:+ }${nodespec}"
149         fi
150         nodespec="$1" ; shift
151     done
152
153     local cmd="$*"
154
155     local status=0
156     # Intentionally unquoted - might be empty
157     # shellcheck disable=SC2086
158     onnode -q $onnode_opts "$nodespec" "$cmd" >"$outfile" 2>&1 || status=$?
159     out=$(dd if="$outfile" bs=1k count=1 2>/dev/null)
160
161     if [ $status -ne 0 ] ; then
162         echo "Failed to execute \"$cmd\" on node(s) \"$nodespec\""
163         cat "$outfile"
164         return $status
165     fi
166
167     if $verbose ; then
168         echo "Output of \"$cmd\":"
169         cat "$outfile"
170     fi
171 }
172
173 _run_onnode ()
174 {
175         local thing="$1"
176         shift
177
178         local options nodespec
179
180         while : ; do
181                 case "$1" in
182                 -*)
183                         options="${options}${options:+ }${1}"
184                         shift
185                         ;;
186                 *)
187                         nodespec="$1"
188                         shift
189                         break
190                 esac
191         done
192
193         # shellcheck disable=SC2086
194         # $options can be multi-word
195         try_command_on_node $options "$nodespec" "${thing} $*"
196 }
197
198 ctdb_onnode ()
199 {
200         _run_onnode "$CTDB" "$@"
201 }
202
203 testprog_onnode ()
204 {
205         _run_onnode "${CTDB_TEST_WRAPPER} ${VALGRIND}" "$@"
206 }
207
208 function_onnode ()
209 {
210         _run_onnode "${CTDB_TEST_WRAPPER}" "$@"
211 }
212
213 sanity_check_output ()
214 {
215     local min_lines="$1"
216     local regexp="$2" # Should be anchored as necessary.
217
218     local ret=0
219
220     local num_lines
221     num_lines=$(wc -l <"$outfile" | tr -d '[:space:]')
222     echo "There are $num_lines lines of output"
223     if [ "$num_lines" -lt "$min_lines" ] ; then
224         ctdb_test_fail "BAD: that's less than the required number (${min_lines})"
225     fi
226
227     local status=0
228     local unexpected # local doesn't pass through status of command on RHS.
229     unexpected=$(grep -Ev "$regexp" "$outfile") || status=$?
230
231     # Note that this is reversed.
232     if [ $status -eq 0 ] ; then
233         echo "BAD: unexpected lines in output:"
234         echo "$unexpected" | cat -A
235         ret=1
236     else
237         echo "Output lines look OK"
238     fi
239
240     return $ret
241 }
242
243 select_test_node ()
244 {
245         try_command_on_node any ctdb pnn || return 1
246
247         test_node="$out"
248         echo "Selected node ${test_node}"
249 }
250
251 # This returns a list of "ip node" lines in $outfile
252 all_ips_on_node()
253 {
254     local node="$1"
255     try_command_on_node "$node" \
256         "$CTDB ip -X | awk -F'|' 'NR > 1 { print \$2, \$3 }'"
257 }
258
259 _select_test_node_and_ips ()
260 {
261     try_command_on_node any \
262         "$CTDB ip -X all | awk -F'|' 'NR > 1 { print \$2, \$3 }'"
263
264     test_node=""  # this matches no PNN
265     test_node_ips=""
266     local ip pnn
267     while read -r ip pnn ; do
268         if [ -z "$test_node" ] && [ "$pnn" != "-1" ] ; then
269             test_node="$pnn"
270         fi
271         if [ "$pnn" = "$test_node" ] ; then
272             test_node_ips="${test_node_ips}${test_node_ips:+ }${ip}"
273         fi
274     done <"$outfile"
275
276     echo "Selected node ${test_node} with IPs: ${test_node_ips}."
277     test_ip="${test_node_ips%% *}"
278
279     # test_prefix used by caller
280     # shellcheck disable=SC2034
281     case "$test_ip" in
282         *:*) test_prefix="${test_ip}/128" ;;
283         *)   test_prefix="${test_ip}/32"  ;;
284     esac
285
286     [ -n "$test_node" ] || return 1
287 }
288
289 select_test_node_and_ips ()
290 {
291     local timeout=10
292     while ! _select_test_node_and_ips ; do
293         echo "Unable to find a test node with IPs assigned"
294         if [ $timeout -le 0 ] ; then
295             ctdb_test_error "BAD: Too many attempts"
296             return 1
297         fi
298         sleep_for 1
299         timeout=$((timeout - 1))
300     done
301
302     return 0
303 }
304
305 # Sets: mask, iface
306 get_test_ip_mask_and_iface ()
307 {
308     # Find the interface
309     ctdb_onnode "$test_node" "ip -v -X"
310     iface=$(awk -F'|' -v ip="$test_ip" '$2 == ip { print $4 }' "$outfile")
311
312     if ctdb_test_on_cluster ; then
313         # Find the netmask
314         try_command_on_node "$test_node" ip addr show to "$test_ip"
315         mask="${out##*/}"
316         mask="${mask%% *}"
317     else
318         mask="24"
319     fi
320
321     echo "$test_ip/$mask is on $iface"
322 }
323
324 ctdb_get_all_pnns ()
325 {
326     try_command_on_node -q all "$CTDB pnn"
327     all_pnns="$out"
328 }
329
330 # The subtlety is that "ctdb delip" will fail if the IP address isn't
331 # configured on a node...
332 delete_ip_from_all_nodes ()
333 {
334     _ip="$1"
335
336     ctdb_get_all_pnns
337
338     _nodes=""
339
340     for _pnn in $all_pnns ; do
341         all_ips_on_node "$_pnn"
342         while read -r _i _ ; do
343             if [ "$_ip" = "$_i" ] ; then
344                 _nodes="${_nodes}${_nodes:+,}${_pnn}"
345             fi
346         done <"$outfile"
347     done
348
349     try_command_on_node -pq "$_nodes" "$CTDB delip $_ip"
350 }
351
352 #######################################
353
354 sleep_for ()
355 {
356     echo -n "=${1}|"
357     for i in $(seq 1 "$1") ; do
358         echo -n '.'
359         sleep 1
360     done
361     echo '|'
362 }
363
364 _cluster_is_healthy ()
365 {
366     $CTDB nodestatus all >/dev/null
367 }
368
369 _cluster_is_recovered ()
370 {
371     node_has_status 0 recovered
372 }
373
374 _cluster_is_ready ()
375 {
376     _cluster_is_healthy && _cluster_is_recovered
377 }
378
379 cluster_is_healthy ()
380 {
381         if onnode 0 "$CTDB_TEST_WRAPPER" _cluster_is_healthy ; then
382                 echo "Cluster is HEALTHY"
383                 if ! onnode 0 "$CTDB_TEST_WRAPPER" _cluster_is_recovered ; then
384                         echo "WARNING: cluster in recovery mode!"
385                 fi
386                 return 0
387         fi
388
389         echo "Cluster is UNHEALTHY"
390
391         echo "DEBUG AT $(date '+%F %T'):"
392         local i
393         for i in "onnode -q 0 $CTDB status" \
394                          "onnode -q 0 onnode all $CTDB scriptstatus" ; do
395                 echo "$i"
396                 $i || true
397         done
398
399         return 1
400 }
401
402 wait_until_ready ()
403 {
404     local timeout="${1:-120}"
405
406     echo "Waiting for cluster to become ready..."
407
408     wait_until "$timeout" onnode -q any "$CTDB_TEST_WRAPPER" _cluster_is_ready
409 }
410
411 # This function is becoming nicely overloaded.  Soon it will collapse!  :-)
412 node_has_status ()
413 {
414         local pnn="$1"
415         local status="$2"
416
417         case "$status" in
418         recovered)
419                 ! $CTDB status -n "$pnn" | \
420                         grep -Eq '^Recovery mode:RECOVERY \(1\)$'
421                 return
422                 ;;
423         notlmaster)
424                 ! $CTDB status | grep -Eq "^hash:.* lmaster:${pnn}\$"
425                 return
426                 ;;
427         esac
428
429         local bits
430         case "$status" in
431         unhealthy)    bits="?|?|?|1|*" ;;
432         healthy)      bits="?|?|?|0|*" ;;
433         disconnected) bits="1|*" ;;
434         connected)    bits="0|*" ;;
435         banned)       bits="?|1|*" ;;
436         unbanned)     bits="?|0|*" ;;
437         disabled)     bits="?|?|1|*" ;;
438         enabled)      bits="?|?|0|*" ;;
439         stopped)      bits="?|?|?|?|1|*" ;;
440         notstopped)   bits="?|?|?|?|0|*" ;;
441         *)
442                 echo "node_has_status: unknown status \"$status\""
443                 return 1
444         esac
445         local out _ line
446
447         out=$($CTDB -X status 2>&1) || return 1
448
449         {
450                 read -r _
451                 while read -r line ; do
452                         # This needs to be done in 2 steps to
453                         # avoid false matches.
454                         local line_bits="${line#|${pnn}|*|}"
455                         [ "$line_bits" = "$line" ] && continue
456                         [ "${line_bits#${bits}}" != "$line_bits" ] && \
457                                 return 0
458                 done
459                 return 1
460         } <<<"$out" # Yay bash!
461 }
462
463 wait_until_node_has_status ()
464 {
465     local pnn="$1"
466     local status="$2"
467     local timeout="${3:-30}"
468     local proxy_pnn="${4:-any}"
469
470     echo "Waiting until node $pnn has status \"$status\"..."
471
472     if ! wait_until "$timeout" onnode "$proxy_pnn" \
473          "$CTDB_TEST_WRAPPER" node_has_status "$pnn" "$status" ; then
474
475         for i in "onnode -q any $CTDB status" "onnode -q any onnode all $CTDB scriptstatus" ; do
476             echo "$i"
477             $i || true
478         done
479
480         return 1
481     fi
482
483 }
484
485 # Useful for superficially testing IP failover.
486 # IPs must be on the given node.
487 # If the first argument is '!' then the IPs must not be on the given node.
488 ips_are_on_node ()
489 {
490     local negating=false
491     if [ "$1" = "!" ] ; then
492         negating=true ; shift
493     fi
494     local node="$1" ; shift
495     local ips="$*"
496
497     local out
498
499     all_ips_on_node "$node"
500
501     local check
502     for check in $ips ; do
503         local ip pnn
504         while read -r ip pnn ; do
505             if [ "$check" = "$ip" ] ; then
506                 if [ "$pnn" = "$node" ] ; then
507                     if $negating ; then return 1 ; fi
508                 else
509                     if ! $negating ; then return 1 ; fi
510                 fi
511                 ips="${ips/${ip}}" # Remove from list
512                 break
513             fi
514             # If we're negating and we didn't see the address then it
515             # isn't hosted by anyone!
516             if $negating ; then
517                 ips="${ips/${check}}"
518             fi
519         done <"$outfile"
520     done
521
522     ips="${ips// }" # Remove any spaces.
523     [ -z "$ips" ]
524 }
525
526 wait_until_ips_are_on_node ()
527 {
528     # Go to some trouble to print a use description of what is happening
529     local not=""
530     if [ "$1" == "!" ] ; then
531         not="no longer "
532     fi
533     local node=""
534     local ips=""
535     local i
536     for i ; do
537         [ "$i" != "!" ] || continue
538         if [ -z "$node" ] ; then
539             node="$i"
540             continue
541         fi
542         ips="${ips}${ips:+, }${i}"
543     done
544     echo "Waiting for ${ips} to ${not}be assigned to node ${node}"
545
546     wait_until 60 ips_are_on_node "$@"
547 }
548
549 node_has_some_ips ()
550 {
551     local node="$1"
552
553     local out
554
555     all_ips_on_node "$node"
556
557     while read -r ip pnn ; do
558         if [ "$node" = "$pnn" ] ; then
559             return 0
560         fi
561     done <"$outfile"
562
563     return 1
564 }
565
566 wait_until_node_has_some_ips ()
567 {
568     echo "Waiting for some IPs to be assigned to node ${test_node}"
569
570     wait_until 60 node_has_some_ips "$@"
571 }
572
573 wait_until_node_has_no_ips ()
574 {
575     echo "Waiting until no IPs are assigned to node ${test_node}"
576
577     wait_until 60 ! node_has_some_ips "$@"
578 }
579
580 #######################################
581
582 ctdb_init ()
583 {
584         ctdb_nodes_stop >/dev/null 2>&1 || :
585
586         ctdb_nodes_start || ctdb_test_error "Cluster start failed"
587
588         wait_until_ready 120 || ctdb_test_error "Cluster didn't become ready"
589
590         echo "Setting RerecoveryTimeout to 1"
591         onnode -pq all "$CTDB setvar RerecoveryTimeout 1"
592
593         echo "Forcing a recovery..."
594         onnode -q 0 "$CTDB recover"
595         sleep_for 2
596
597         if ! onnode -q all "$CTDB_TEST_WRAPPER _cluster_is_recovered" ; then
598                 echo "Cluster has gone into recovery again, waiting..."
599                 wait_until 30/2 onnode -q all \
600                            "$CTDB_TEST_WRAPPER _cluster_is_recovered" || \
601                         ctdb_test_error "Cluster did not come out of recovery"
602         fi
603
604         if ! onnode 0 "$CTDB_TEST_WRAPPER _cluster_is_healthy" ; then
605                 ctdb_test_error "Cluster became UNHEALTHY again [$(date)]"
606         fi
607
608         echo "Doing a sync..."
609         onnode -q 0 "$CTDB sync"
610
611         echo "ctdb is ready"
612         return 0
613 }
614
615 ctdb_base_show ()
616 {
617         echo "${CTDB_BASE:-${CTDB_SCRIPTS_BASE}}"
618 }
619
620 #######################################
621
622 wait_for_monitor_event ()
623 {
624     local pnn="$1"
625     local timeout=120
626
627     echo "Waiting for a monitor event on node ${pnn}..."
628
629     ctdb_onnode "$pnn" scriptstatus || {
630         echo "Unable to get scriptstatus from node $pnn"
631         return 1
632     }
633
634     mv "$outfile" "${outfile}.orig"
635
636     wait_until 120 _ctdb_scriptstatus_changed
637 }
638
639 _ctdb_scriptstatus_changed ()
640 {
641     ctdb_onnode "$pnn" scriptstatus || {
642         echo "Unable to get scriptstatus from node $pnn"
643         return 1
644     }
645
646     ! diff "$outfile" "${outfile}.orig" >/dev/null
647 }
648
649 #######################################
650
651 # If the given IP is hosted then print 2 items: maskbits and iface
652 ip_maskbits_iface ()
653 {
654     _addr="$1"
655
656     case "$_addr" in
657         *:*) _family="inet6" ; _bits=128 ;;
658         *)   _family="inet"  ; _bits=32  ;;
659     esac
660
661     # Literal backslashes in awk script
662     # shellcheck disable=SC1004
663     ip addr show to "${_addr}/${_bits}" 2>/dev/null | \
664         awk -v family="${_family}" \
665             'NR == 1 { iface = $2; sub(":$", "", iface) } \
666              $1 ~ /inet/ { mask = $2; sub(".*/", "", mask); \
667                            print mask, iface, family }'
668 }
669
670 drop_ip ()
671 {
672     _addr="${1%/*}"  # Remove optional maskbits
673
674     # Intentional word splitting
675     # shellcheck disable=SC2046,SC2086
676     set -- $(ip_maskbits_iface $_addr)
677     if [ -n "$1" ] ; then
678         _maskbits="$1"
679         _iface="$2"
680         echo "Removing public address $_addr/$_maskbits from device $_iface"
681         ip addr del "$_ip/$_maskbits" dev "$_iface" >/dev/null 2>&1 || true
682     fi
683 }
684
685 drop_ips ()
686 {
687     for _ip ; do
688         drop_ip "$_ip"
689     done
690 }
691
692 #######################################
693
694 # $1: pnn, $2: DB name
695 db_get_path ()
696 {
697         ctdb_onnode -v "$1" "getdbstatus $2" | sed -n -e "s@^path: @@p"
698 }
699
700 # $1: pnn, $2: DB name
701 db_ctdb_cattdb_count_records ()
702 {
703         # Count the number of keys, excluding any that begin with '_'.
704         # This excludes at least the sequence number record in
705         # persistent/replicated databases.  The trailing "|| :" forces
706         # the command to succeed when no records are matched.
707         ctdb_onnode "$1" "cattdb $2 | grep -c '^key([0-9][0-9]*) = \"[^_]' || :"
708         echo "$out"
709 }
710
711 # $1: pnn, $2: DB name, $3: key string, $4: value string, $5: RSN (default 7)
712 db_ctdb_tstore ()
713 {
714     _tdb=$(db_get_path "$1" "$2")
715     _rsn="${5:-7}"
716     ctdb_onnode "$1" tstore "$_tdb" "$3" "$4" "$_rsn"
717 }
718
719 # $1: pnn, $2: DB name, $3: dbseqnum (must be < 255!!!!!)
720 db_ctdb_tstore_dbseqnum ()
721 {
722     # "__db_sequence_number__" + trailing 0x00
723     _key='0x5f5f64625f73657175656e63655f6e756d6265725f5f00'
724
725     # Construct 8 byte (unit64_t) database sequence number.  This
726     # probably breaks if $3 > 255
727     _value=$(printf "0x%02x%014x" "$3" 0)
728
729     db_ctdb_tstore "$1" "$2" "$_key" "$_value"
730 }
731
732 ########################################
733
734 # Make sure that $CTDB is set.
735 if [ -z "$CTDB" ] ; then
736         CTDB="ctdb"
737 fi
738
739 if ctdb_test_on_cluster ; then
740         . "${TEST_SCRIPTS_DIR}/integration_real_cluster.bash"
741 else
742         . "${TEST_SCRIPTS_DIR}/integration_local_daemons.bash"
743 fi
744
745
746 local="${CTDB_TEST_SUITE_DIR}/scripts/local.bash"
747 if [ -r "$local" ] ; then
748     . "$local"
749 fi