/
githubmirror
/
lvm2
Обзор
Документация
Войти
/
githubmirror
/
lvm2
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
test/cluster/cluster-executor.sh
2 520 строк
92 KB
David Teigland
add test/cluster framework: lvmtest, test scripts, configs, and groups
03 июл 2026, 01:24
03 июл 2026, 01:24
f0a4ff3
Код
Авторство
О чём код?
#!/bin/bash # # cluster-executor.sh - Test execution layer for LVM cluster testing # # This script provides the test execution infrastructure for running commands # and tests on cluster nodes. It implements: # - Single node command execution # - Serial execution across all test nodes # - Parallel execution across all test nodes # - Test variable system ($node1, $node2, ..., $nodes, $nodep) # - Device variable system ($dev1, $dev2, ..., $devN) # # Source the core library for logging and utilities SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "$SCRIPT_DIR/cluster-test-lib.sh" || exit 1 # # cluster_translate_devs - Translate node1 device paths to target node paths # # Device variables ($dev1, $dev2, etc.) are expanded on the host using node1's # device paths before commands reach the dispatch functions. When nodes have # different device names for the same physical device (common — Linux device # enumeration order varies), the literal paths are wrong for non-node1 targets. # # This function rewrites a command string, replacing every node1 device path # with the corresponding path on the target node. Uses a two-phase # placeholder approach to avoid collisions when paths swap (e.g. node1 # /dev/sda,/dev/sdb but node3 /dev/sdb,/dev/sda). # # Requires CLUSTER_DEV_COUNT and CLUSTER_DEV_MAP_<node>_<idx> exported # variables set by cluster_setup_device_vars(). # cluster_translate_devs() { local node_num="$1" local cmd="$2" if [ "$node_num" -le 1 ]; then echo "$cmd" return fi if [ -z "${CLUSTER_DEV_COUNT:-}" ] || [ "$CLUSTER_DEV_COUNT" -eq 0 ]; then echo "$cmd" return fi local result="$cmd" # Phase 1: replace node1 paths with placeholders. # Sort by path length (longest first) so that longer paths like # /dev/mapper/mpathaa are replaced before shorter prefixes like # /dev/mapper/mpatha that would otherwise match as substrings. local -a _dev_order for i in $(seq 0 $((CLUSTER_DEV_COUNT - 1))); do local _varname="CLUSTER_DEV_MAP_1_${i}" local _p="${!_varname}" if [ -n "$_p" ]; then _dev_order+=("${#_p} $i") fi done IFS=$'\n' _dev_order=($(printf '%s\n' "${_dev_order[@]}" | sort -t' ' -k1,1 -rn)); unset IFS for _entry in "${_dev_order[@]}"; do local i="${_entry#* }" local _varname="CLUSTER_DEV_MAP_1_${i}" local node1_path="${!_varname}" result="${result//$node1_path/__CLUSTER_DEV_${i}__}" done # Phase 2: replace placeholders with target node paths for i in $(seq 0 $((CLUSTER_DEV_COUNT - 1))); do local _varname="CLUSTER_DEV_MAP_${node_num}_${i}" local target_path="${!_varname}" if [ -n "$target_path" ]; then result="${result//__CLUSTER_DEV_${i}__/$target_path}" fi done echo "$result" } # # cluster_node_exec - Execute command on a specific node # # Executes a command on the specified cluster node via SSH and returns # the output and exit code. # # Arguments: # $1 - node_num (0 for storage node, 1..N for test nodes) # $2 - command to execute # # Returns: # Exit code from the remote command # # Output: # stdout/stderr from the remote command # cluster_node_exec() { local node_num="$1" local cmd="$2" if [ -z "$node_num" ]; then cluster_error "cluster_node_exec: node_num is required" return 1 fi if [ -z "$cmd" ]; then cluster_error "cluster_node_exec: command is required" return 1 fi # Get the VM name for this node local vm_name=$(cluster_vm_get_name "$CLUSTER_ID" "$node_num") # Get the node IP local node_ip if ! node_ip=$(cluster_vm_get_ip "$vm_name" 2>/dev/null); then cluster_error "Failed to get IP for node $node_num ($vm_name)" return 1 fi # Translate node1 device paths to this node's paths cmd=$(cluster_translate_devs "$node_num" "$cmd") # Execute command via SSH cluster_vm_ssh "$node_ip" "export NODE_NUM=$node_num; source /root/cluster-dev-map.sh 2>/dev/null; source /root/cluster-node-helpers.sh 2>/dev/null; source /root/cluster-test-helpers.sh 2>/dev/null; $cmd" return $? } # # cluster_all_exec - Execute command serially on all test nodes # # Executes a command on all test nodes (1..N) in serial order. # Stops on first failure. # # Arguments: # $1 - command to execute # # Returns: # 0 if all executions succeeded # Exit code of first failed execution # cluster_all_exec() { local cmd="$1" if [ -z "$cmd" ]; then cluster_error "cluster_all_exec: command is required" return 1 fi if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi local num_nodes="${CLUSTER_NUM_NODES}" # Execute on nodes 1..N (skip node 0 which is storage exporter) for node_num in $(seq 1 "$num_nodes"); do cluster_debug "Executing on node $node_num: $cmd" if cluster_node_exec "$node_num" "$cmd"; then echo " node${node_num}: success" else local exit_code=$? echo " node${node_num}: failed" return 1 fi done return 0 } # # cluster_nodes_exec - Execute command in parallel on all test nodes # # Executes a command on all test nodes (1..N) in parallel using background jobs. # Waits for all jobs to complete and collects results. # # Arguments: # $1 - command to execute # # Returns: # 0 if all executions succeeded # 1 if any execution failed # cluster_nodes_exec() { local cmd="$1" if [ -z "$cmd" ]; then cluster_error "cluster_nodes_exec: command is required" return 1 fi if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi local num_nodes="${CLUSTER_NUM_NODES}" local pids=() local results=() local failed=0 # Create temp directory for output files local tmpdir=$(mktemp -d) if [ ! -d "$tmpdir" ]; then echo "ERROR: Failed to create temp directory" >&2 return 1 fi trap "rm -rf '$tmpdir'" RETURN # Pre-resolve all node IPs to avoid function call issues in subshells local node_ips=() for node_num in $(seq 1 "$num_nodes"); do local vm_name=$(cluster_vm_get_name "$CLUSTER_ID" "$node_num") local node_ip if ! node_ip=$(cluster_vm_get_ip "$vm_name" 2>/dev/null); then echo "ERROR: Failed to get IP for node $node_num ($vm_name)" >&2 return 1 fi node_ips[$node_num]="$node_ip" cluster_debug "Node $node_num IP: $node_ip" done # Validate SSH key directory and user are set if [ -z "${CLUSTER_SSH_KEY_DIR:-}" ]; then echo "ERROR: CLUSTER_SSH_KEY_DIR not set" >&2 return 1 fi if [ -z "${CLUSTER_SSH_USER:-}" ]; then echo "ERROR: CLUSTER_SSH_USER not set" >&2 return 1 fi local ssh_key="${CLUSTER_SSH_KEY_DIR}/cluster_test_rsa" if [ ! -f "$ssh_key" ]; then echo "ERROR: SSH key not found: $ssh_key" >&2 return 1 fi cluster_debug "Using SSH key: $ssh_key" cluster_debug "SSH user: $CLUSTER_SSH_USER" # Launch background jobs for each node for node_num in $(seq 1 "$num_nodes"); do local output_file="$tmpdir/node${node_num}.out" local status_file="$tmpdir/node${node_num}.status" local node_ip="${node_ips[$node_num]}" cluster_debug "Starting background job for node $node_num (IP: $node_ip)" local node_cmd node_cmd=$(cluster_translate_devs "$node_num" "$cmd") bash -c " ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -o ConnectTimeout=10 \ -o ServerAliveInterval=5 \ -o ServerAliveCountMax=3 \ -i '$ssh_key' \ '$CLUSTER_SSH_USER@$node_ip' \ 'export NODE_NUM=$node_num; source /root/cluster-dev-map.sh 2>/dev/null; source /root/cluster-node-helpers.sh 2>/dev/null; source /root/cluster-test-helpers.sh 2>/dev/null; $node_cmd' >'$output_file' 2>&1 echo \$? > '$status_file' " & pids+=($!) cluster_debug "Launched parallel execution on node $node_num (PID: ${pids[-1]})" done # Wait for all background jobs cluster_debug "Waiting for ${#pids[@]} parallel executions to complete" for pid in "${pids[@]}"; do wait "$pid" 2>/dev/null || true done # Give background jobs a moment to flush output sleep 0.1 # Collect results - explicitly write to stdout for node_num in $(seq 1 "$num_nodes"); do local output_file="$tmpdir/node${node_num}.out" local status_file="$tmpdir/node${node_num}.status" # Debug: Check if files exist if [ ! -f "$output_file" ]; then echo "ERROR: Output file not created for node $node_num" >&2 echo " Expected: $output_file" >&2 failed=1 continue fi if [ ! -f "$status_file" ]; then echo "ERROR: Status file not created for node $node_num" >&2 echo " Expected: $status_file" >&2 failed=1 continue fi # Get exit status local status=-1 status=$(cat "$status_file" 2>/dev/null || echo "-1") # Print header with status to stdout if [ "$status" -eq 0 ]; then echo " node${node_num}: success" else echo " node${node_num}: failed" failed=1 fi # Print output from this node (both stdout and stderr were captured) if [ -s "$output_file" ]; then cat "$output_file" else echo " (no output)" fi done # Count successes and failures, track node numbers local success_nodes=() local fail_nodes=() for node_num in $(seq 1 "$num_nodes"); do local status_file="$tmpdir/node${node_num}.status" local status=-1 if [ -f "$status_file" ]; then status=$(cat "$status_file" 2>/dev/null || echo "-1") fi if [ "$status" -eq 0 ]; then success_nodes+=($node_num) else fail_nodes+=($node_num) fi done # Export result variables export NODEP_TOTAL_COUNT=$num_nodes export NODEP_SUCCESS_COUNT=${#success_nodes[@]} export NODEP_FAIL_COUNT=${#fail_nodes[@]} export NODEP_SUCCESS_NODES="${success_nodes[*]}" export NODEP_FAIL_NODES="${fail_nodes[*]}" # Special case: exactly one success if [ ${#success_nodes[@]} -eq 1 ]; then export NODEP_SINGLE_SUCCESS_NODE="${success_nodes[0]}" else export NODEP_SINGLE_SUCCESS_NODE="" fi cluster_debug "nodep results: success=$NODEP_SUCCESS_COUNT fail=$NODEP_FAIL_COUNT" if [ -n "$NODEP_SINGLE_SUCCESS_NODE" ]; then cluster_debug " Single success on node: $NODEP_SINGLE_SUCCESS_NODE" fi return $failed } # # cluster_nodes_exec_per_node - Execute different commands on different nodes in parallel # # Similar to cluster_nodes_exec, but allows running different commands on each node. # Sets the same NODEP_* environment variables for use with assert_* functions. # # Arguments: # $@ - node:command pairs in format "1:command1" "2:command2" ... # # Returns: # 0 if all commands succeeded, 1 if any failed # cluster_nodes_exec_per_node() { if [ $# -eq 0 ]; then cluster_error "cluster_nodes_exec_per_node: at least one node:command pair required" return 1 fi if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi # Parse node:command pairs declare -A node_commands declare -a target_nodes for arg in "$@"; do # Parse node:command format if [[ "$arg" =~ ^([0-9]+):(.+)$ ]]; then local node_num="${BASH_REMATCH[1]}" local command="${BASH_REMATCH[2]}" # Validate node number if [ "$node_num" -lt 1 ] || [ "$node_num" -gt "$CLUSTER_NUM_NODES" ]; then echo "ERROR: Invalid node number $node_num (valid: 1-$CLUSTER_NUM_NODES)" >&2 return 1 fi # Check for duplicates if [ -n "${node_commands[$node_num]:-}" ]; then echo "ERROR: Node $node_num specified multiple times" >&2 return 1 fi node_commands[$node_num]="$command" target_nodes+=($node_num) else echo "ERROR: Invalid format '$arg' - expected 'node:command'" >&2 return 1 fi done # Validate SSH key directory and user are set if [ -z "${CLUSTER_SSH_KEY_DIR:-}" ]; then echo "ERROR: CLUSTER_SSH_KEY_DIR not set" >&2 return 1 fi if [ -z "${CLUSTER_SSH_USER:-}" ]; then echo "ERROR: CLUSTER_SSH_USER not set" >&2 return 1 fi local ssh_key="${CLUSTER_SSH_KEY_DIR}/cluster_test_rsa" if [ ! -f "$ssh_key" ]; then echo "ERROR: SSH key not found: $ssh_key" >&2 return 1 fi # Create temp directory for output files local tmpdir=$(mktemp -d) if [ ! -d "$tmpdir" ]; then echo "ERROR: Failed to create temp directory" >&2 return 1 fi trap "rm -rf '$tmpdir'" RETURN # Pre-resolve node IPs for target nodes local node_ips=() for node_num in "${target_nodes[@]}"; do local vm_name=$(cluster_vm_get_name "$CLUSTER_ID" "$node_num") local node_ip if ! node_ip=$(cluster_vm_get_ip "$vm_name" 2>/dev/null); then echo "ERROR: Failed to get IP for node $node_num ($vm_name)" >&2 return 1 fi node_ips[$node_num]="$node_ip" cluster_debug "Node $node_num IP: $node_ip" done # Launch background jobs for each target node local pids=() declare -A pid_to_node for node_num in "${target_nodes[@]}"; do local command="${node_commands[$node_num]}" local output_file="$tmpdir/node${node_num}.out" local status_file="$tmpdir/node${node_num}.status" local node_ip="${node_ips[$node_num]}" cluster_debug "Starting node $node_num: $command" local node_cmd node_cmd=$(cluster_translate_devs "$node_num" "$command") bash -c " ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -o ConnectTimeout=10 \ -o ServerAliveInterval=5 \ -o ServerAliveCountMax=3 \ -i '$ssh_key' \ '$CLUSTER_SSH_USER@$node_ip' \ 'export NODE_NUM=$node_num; source /root/cluster-dev-map.sh 2>/dev/null; source /root/cluster-node-helpers.sh 2>/dev/null; source /root/cluster-test-helpers.sh 2>/dev/null; $node_cmd' >'$output_file' 2>&1 echo \$? > '$status_file' " & local pid=$! pids+=($pid) pid_to_node[$pid]=$node_num cluster_debug "Launched on node $node_num (PID: $pid)" done # Wait for all background jobs cluster_debug "Waiting for ${#pids[@]} parallel executions to complete" for pid in "${pids[@]}"; do wait "$pid" 2>/dev/null || true done # Give background jobs a moment to flush output sleep 0.1 # Collect results for target nodes only local failed=0 for node_num in "${target_nodes[@]}"; do local output_file="$tmpdir/node${node_num}.out" local status_file="$tmpdir/node${node_num}.status" if [ ! -f "$status_file" ]; then echo "ERROR: Status file not created for node $node_num" >&2 failed=1 continue fi # Get exit status local status=$(cat "$status_file" 2>/dev/null || echo "-1") # Print header with status if [ "$status" -eq 0 ]; then echo " node${node_num}: success" else echo " node${node_num}: failed" failed=1 fi # Print output if [ -s "$output_file" ]; then cat "$output_file" else echo " (no output)" fi done # Count successes and failures across ALL nodes (not just target nodes) # Nodes not in target list are counted as "not run" / failed local success_nodes=() local fail_nodes=() for node_num in $(seq 1 "$CLUSTER_NUM_NODES"); do local status_file="$tmpdir/node${node_num}.status" # Check if this node was targeted if [ -n "${node_commands[$node_num]:-}" ]; then # Node was targeted - check its status local status=-1 if [ -f "$status_file" ]; then status=$(cat "$status_file" 2>/dev/null || echo "-1") fi if [ "$status" -eq 0 ]; then success_nodes+=($node_num) else fail_nodes+=($node_num) fi else # Node was not targeted - count as failed fail_nodes+=($node_num) fi done # Export result variables (same as cluster_nodes_exec for compatibility) export NODEP_TOTAL_COUNT=$CLUSTER_NUM_NODES export NODEP_SUCCESS_COUNT=${#success_nodes[@]} export NODEP_FAIL_COUNT=${#fail_nodes[@]} export NODEP_SUCCESS_NODES="${success_nodes[*]}" export NODEP_FAIL_NODES="${fail_nodes[*]}" # Special case: exactly one success if [ ${#success_nodes[@]} -eq 1 ]; then export NODEP_SINGLE_SUCCESS_NODE="${success_nodes[0]}" else export NODEP_SINGLE_SUCCESS_NODE="" fi cluster_debug "nodepp results: success=$NODEP_SUCCESS_COUNT fail=$NODEP_FAIL_COUNT" if [ -n "$NODEP_SINGLE_SUCCESS_NODE" ]; then cluster_debug " Single success on node: $NODEP_SINGLE_SUCCESS_NODE" fi return $failed } # # cluster_setup_test_vars - Set up test variable system # # Creates bash functions for test variables: # $node1, $node2, ..., $nodeN - Execute on specific node # $nodes - Execute serially on all test nodes # $nodep - Execute in parallel on all test nodes (same command) # $nodepp - Execute in parallel on different nodes (different commands) # $success_node - Execute on the node that succeeded (after nodep/nodepp) # $node_rand - Execute on a randomly selected test node # # These functions allow natural syntax in test scripts: # $node1 vgcreate vg /dev/sdb # $nodes vgchange --lock-start vg # $nodep lvs vg # $nodepp 1:"lvs" 2:"vgs" 3:"pvs" # $nodep lvcreate -L 10M -n lv1 testvg || true # $success_node lvchange -ay testvg/lv1 # $node_rand lvchange -ay testvg/lv2 # # Arguments: # None (uses CLUSTER_NUM_NODES from environment) # # Returns: # 0 on success, 1 on failure # cluster_setup_test_vars() { if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi local num_nodes="${CLUSTER_NUM_NODES}" cluster_debug "Setting up test variables for $num_nodes node(s)" # Create node1, node2, ..., nodeN functions for node_num in $(seq 1 "$num_nodes"); do local func_name="node${node_num}" # Create function dynamically eval " $func_name() { echo \"$func_name \$*\" >&2 cluster_node_exec $node_num \"\$*\" } export -f $func_name " cluster_debug "Created test variable: \$$func_name" done # Create $nodes function (serial execution) nodes() { echo "nodes $*" >&2 cluster_all_exec "$*" } export -f nodes cluster_debug "Created test variable: \$nodes (serial execution)" # Create $nodep function (parallel execution) # After parallel execution, sets these variables: # NODEP_TOTAL_COUNT - total number of nodes # NODEP_SUCCESS_COUNT - number of nodes where command succeeded (exit 0) # NODEP_FAIL_COUNT - number of nodes where command failed (exit != 0) # NODEP_SUCCESS_NODES - space-separated list of successful node numbers # NODEP_FAIL_NODES - space-separated list of failed node numbers # NODEP_SINGLE_SUCCESS_NODE - node number if exactly one succeeded, else empty nodep() { echo "nodep $*" >&2 cluster_nodes_exec "$*" } export -f nodep cluster_debug "Created test variable: \$nodep (parallel execution)" # Create $nodepp function (parallel execution with different commands per node) # After parallel execution, sets the same NODEP_* variables as nodep: # NODEP_TOTAL_COUNT - total number of nodes # NODEP_SUCCESS_COUNT - number of nodes where command succeeded (exit 0) # NODEP_FAIL_COUNT - number of nodes where command failed (exit != 0) # NODEP_SUCCESS_NODES - space-separated list of successful node numbers # NODEP_FAIL_NODES - space-separated list of failed node numbers # NODEP_SINGLE_SUCCESS_NODE - node number if exactly one succeeded, else empty nodepp() { echo "nodepp $*" >&2 cluster_nodes_exec_per_node "$@" } export -f nodepp cluster_debug "Created test variable: \$nodepp (parallel per-node execution)" # Create $success_node function - execute on the single success node # This is a convenience wrapper for: eval "node${NODEP_SINGLE_SUCCESS_NODE} <command>" # Must be called after a nodep or nodepp operation that resulted in exactly one success success_node() { if [ -z "${NODEP_SINGLE_SUCCESS_NODE:-}" ]; then echo "ERROR: success_node requires NODEP_SINGLE_SUCCESS_NODE to be set" >&2 echo " This variable is set by nodep/nodepp when exactly one node succeeds" >&2 echo " Current NODEP_SUCCESS_COUNT: ${NODEP_SUCCESS_COUNT:-not set}" >&2 return 1 fi local node_num="$NODEP_SINGLE_SUCCESS_NODE" echo "success_node (node${node_num}) $*" >&2 cluster_node_exec "$node_num" "$*" } export -f success_node cluster_debug "Created test variable: \$success_node (execute on single success node)" # Create $node_rand function - execute on a randomly selected test node # Useful for testing operations that should work on any node node_rand() { # Generate random node number from 1 to CLUSTER_NUM_NODES local random_node=$((1 + RANDOM % CLUSTER_NUM_NODES)) echo "node_rand (selected node${random_node}) $*" >&2 cluster_node_exec "$random_node" "$*" } export -f node_rand cluster_debug "Created test variable: \$node_rand (execute on random node)" # Create $noden function - execute on a specified node number # Usage: noden <node_num> <command> # This is a convenience wrapper for dynamic node selection noden() { local node_num="$1" shift if [ -z "$node_num" ]; then echo "ERROR: noden requires a node number as first argument" >&2 echo " Usage: noden <node_num> <command>" >&2 return 1 fi if ! [[ "$node_num" =~ ^[0-9]+$ ]]; then echo "ERROR: noden node number must be numeric, got: '$node_num'" >&2 return 1 fi if [ "$node_num" -lt 1 ] || [ "$node_num" -gt "$CLUSTER_NUM_NODES" ]; then echo "ERROR: noden node number $node_num out of range (1-$CLUSTER_NUM_NODES)" >&2 return 1 fi echo "noden (node${node_num}) $*" >&2 cluster_node_exec "$node_num" "$*" } export -f noden cluster_debug "Created test variable: \$noden (execute on specified node)" # Also export node0 for storage node (though tests typically don't use it) node0() { echo "node0 $*" >&2 cluster_node_exec 0 "$*" } export -f node0 cluster_debug "Created test variable: \$node0 (storage exporter)" # Stress test support: arrays to hold registered loop definitions STRESS_LOOP_NAMES=() STRESS_LOOP_BODIES=() STRESS_LOOP_NODES=() # stress_loop <name> <body> - register a named loop for stress testing # Loop body is a shell fragment that will be run repeatedly on each node. # Use single quotes for the body so variables like $(hostname) and $dev1 # expand on the remote node, not locally. stress_loop() { local name="$1" local body="$2" if [ -z "$name" ]; then echo "ERROR: stress_loop requires a name" >&2 return 1 fi if [ -z "$body" ]; then echo "ERROR: stress_loop '$name' requires a body" >&2 return 1 fi STRESS_LOOP_NAMES+=("$name") STRESS_LOOP_BODIES+=("$body") STRESS_LOOP_NODES+=("all") echo "stress_loop $name" >&2 } export -f stress_loop # stress_loop_on <nodes> <name> <body> - register a loop for specific nodes only # <nodes> is a space-separated list of node numbers, e.g. "1 3" stress_loop_on() { local nodes="$1" local name="$2" local body="$3" if [ -z "$nodes" ] || [ -z "$name" ] || [ -z "$body" ]; then echo "ERROR: stress_loop_on requires: <nodes> <name> <body>" >&2 return 1 fi STRESS_LOOP_NAMES+=("$name") STRESS_LOOP_BODIES+=("$body") STRESS_LOOP_NODES+=("$nodes") echo "stress_loop_on $nodes $name" >&2 } export -f stress_loop_on # stress <duration> - run all registered stress loops on nodes # duration format: Nmin (minutes) or Nsec (seconds), e.g. "5min" or "30sec" stress() { local duration="$1" if [ -z "$duration" ]; then echo "ERROR: stress requires a duration, e.g. 5min or 30sec" >&2 return 1 fi local duration_secs if [[ "$duration" =~ ^([0-9]+)min$ ]]; then duration_secs=$(( ${BASH_REMATCH[1]} * 60 )) elif [[ "$duration" =~ ^([0-9]+)sec$ ]]; then duration_secs=${BASH_REMATCH[1]} else echo "ERROR: stress duration must be Nmin or Nsec, got: $duration" >&2 return 1 fi if [ "$duration_secs" -lt 1 ]; then echo "ERROR: stress duration must be at least 1sec" >&2 return 1 fi if [ ${#STRESS_LOOP_NAMES[@]} -eq 0 ]; then echo "ERROR: no stress loops defined (use stress_loop first)" >&2 return 1 fi echo "stress $duration" >&2 cluster_stress_run "$duration_secs" local rc=$? # Clear the registry for potential reuse STRESS_LOOP_NAMES=() STRESS_LOOP_BODIES=() STRESS_LOOP_NODES=() return $rc } export -f stress cluster_debug "Created stress test functions: stress_loop, stress_loop_on, stress" # Create safety wrappers for commands to prevent local execution # These will error if someone forgets to use node1, node2, etc. _command_error() { local cmd="$1" shift local caller_line="${BASH_LINENO[0]}" local caller_file="${BASH_SOURCE[1]}" caller_file="$(basename "$caller_file")" echo "" >&2 echo "ERROR: command '$cmd' called without node prefix" >&2 echo " at $caller_file:$caller_line" >&2 echo "" >&2 echo "Commands must be executed on test nodes using:" >&2 echo " node1 $cmd $*" >&2 echo " node2 $cmd $*" >&2 echo " noden N $cmd $* (specific node N)" >&2 echo " nodes $cmd $* (all nodes serially)" >&2 echo " nodep $cmd $* || true (all nodes in parallel)" >&2 echo " node_rand $cmd $* (random node)" >&2 echo "" >&2 exit 1 } # Wrapper functions for LVM commands vgcreate() { _command_error "vgcreate" "$@"; } vgremove() { _command_error "vgremove" "$@"; } vgchange() { _command_error "vgchange" "$@"; } vgrename() { _command_error "vgrename" "$@"; } vgextend() { _command_error "vgextend" "$@"; } vgreduce() { _command_error "vgreduce" "$@"; } vgexport() { _command_error "vgexport" "$@"; } vgimport() { _command_error "vgimport" "$@"; } vgck() { _command_error "vgck" "$@"; } vgcfgbackup() { _command_error "vgcfgbackup" "$@"; } vgcfgrestore() { _command_error "vgcfgrestore" "$@"; } vgimportdevices() { _command_error "vgimportdevices" "$@"; } lvcreate() { _command_error "lvcreate" "$@"; } lvremove() { _command_error "lvremove" "$@"; } lvchange() { _command_error "lvchange" "$@"; } lvrename() { _command_error "lvrename" "$@"; } lvextend() { _command_error "lvextend" "$@"; } lvreduce() { _command_error "lvreduce" "$@"; } lvresize() { _command_error "lvresize" "$@"; } lvconvert() { _command_error "lvconvert" "$@"; } pvcreate() { _command_error "pvcreate" "$@"; } pvremove() { _command_error "pvremove" "$@"; } pvchange() { _command_error "pvchange" "$@"; } pvresize() { _command_error "pvresize" "$@"; } pvmove() { _command_error "pvmove" "$@"; } vgs() { _command_error "vgs" "$@"; } lvs() { _command_error "lvs" "$@"; } pvs() { _command_error "pvs" "$@"; } vgdisplay() { _command_error "vgdisplay" "$@"; } lvdisplay() { _command_error "lvdisplay" "$@"; } pvdisplay() { _command_error "pvdisplay" "$@"; } vgscan() { _command_error "vgscan" "$@"; } lvscan() { _command_error "lvscan" "$@"; } pvscan() { _command_error "pvscan" "$@"; } lvmconfig() { _command_error "lvmconfig" "$@"; } lvmdevices() { _command_error "lvmdevices" "$@"; } lvmlockctl() { _command_error "lvmlockctl" "$@"; } lvmpersist() { _command_error "lvmpersist" "$@"; } # Wrapper functions for non-LVM commands wipefs() { _command_error "wipefs" "$@"; } mdadm() { _command_error "mdadm" "$@"; } cryptsetup() { _command_error "cryptsetup" "$@"; } dmsetup() { _command_error "dmsetup" "$@"; } dd() { _command_error "dd" "$@"; } xxd() { _command_error "xxd" "$@"; } systemctl() { _command_error "systemctl" "$@"; } mkfs() { _command_error "mkfs" "$@"; } mkfs.ext2() { _command_error "mkfs.ext2" "$@"; } mkfs.ext3() { _command_error "mkfs.ext3" "$@"; } mkfs.ext4() { _command_error "mkfs.ext4" "$@"; } mkfs.xfs() { _command_error "mkfs.xfs" "$@"; } mkfs.btrfs() { _command_error "mkfs.btrfs" "$@"; } mount() { _command_error "mount" "$@"; } umount() { _command_error "umount" "$@"; } blockdev() { _command_error "blockdev" "$@"; } blkid() { _command_error "blkid" "$@"; } losetup() { _command_error "losetup" "$@"; } fdisk() { _command_error "fdisk" "$@"; } sanlock() { _command_error "sanlock" "$@"; } modprobe() { _command_error "modprobe" "$@"; } reboot() { _command_error "reboot" "$@"; } nvme() { _command_error "nvme" "$@"; } # sg_* SCSI generic commands local _sg_cmds="sg_persist sg_inq sg_raw sg_dd sgm_dd sgp_dd sgdisk sg_reset sg_format sg_sanitize sg_write_same sg_write_buffer sg_write_x sg_unmap sg_start sg_ses sg_ses_microcode sg_senddiag sg_luns sg_logs sg_modes sg_opcodes sg_readcap sg_reassign sg_verify sg_vpd sg_turs sg_sync sg_compare_and_write sg_xcopy sg_copy_results sg_seek sg_stream_ctl sg_get_config sg_get_elem_status sg_get_lba_status sg_zone sg_z_act_query sg_rep_zones sg_reset_wp sg_rem_rest_elem sg_prevent sg_scan sg_map sg_map26 sg_rbuf sg_read sg_read_attr sg_read_buffer sg_read_long sg_write_long sg_write_attr sg_write_verify sg_wr_mode sg_timestamp sg_sat_identify sg_sat_set_features sg_sat_read_gplog sg_sat_datetime sg_sat_phy_event sg_ident sg_rmsn sg_safte sg_rtpg sg_stpg sg_rdac sg_emc_trespass sg_requests sg_decode_sense sg_test_rwbuf sg_bg_ctl sg_rep_density sg_rep_pip sg_read_block_limits sginfo sgpio sg" for _sg_cmd in $_sg_cmds; do eval "${_sg_cmd}() { _command_error \"${_sg_cmd}\" \"\$@\"; }" export -f "${_sg_cmd}" done export -f _command_error export -f vgcreate vgremove vgchange vgrename vgextend vgreduce export -f vgexport vgimport vgck vgcfgbackup vgcfgrestore vgimportdevices export -f lvcreate lvremove lvchange lvrename lvextend lvreduce lvresize lvconvert export -f pvcreate pvremove pvchange pvresize pvmove export -f vgs lvs pvs vgdisplay lvdisplay pvdisplay export -f vgscan lvscan pvscan export -f lvmconfig lvmdevices lvmlockctl lvmpersist export -f wipefs mdadm cryptsetup dmsetup dd xxd nvme export -f systemctl mkfs mkfs.ext2 mkfs.ext3 mkfs.ext4 mkfs.xfs mkfs.btrfs export -f mount umount blockdev blkid losetup fdisk sanlock modprobe reboot cluster_debug "Created command safety wrappers" cluster_log "Test variables set up successfully" return 0 } # # cluster_discover_devices_on_node - Discover devices on a specific node # # Queries a test node to discover imported storage devices. # # Arguments: # $1 - node_num # # Output: # List of device paths (one per line) # # Returns: # 0 on success, 1 on failure # cluster_discover_devices_on_node() { local node_num="$1" local skip_rescan="${2:-0}" # Optional: skip slow rescans if devices already known if [ -z "$node_num" ]; then cluster_error "cluster_discover_devices_on_node: node_num is required" return 1 fi local num_scsi="${CLUSTER_NUM_SCSI:-0}" local num_nvme="${CLUSTER_NUM_NVME:-0}" local num_multipath="${CLUSTER_NUM_MULTIPATH:-0}" # Build discovery script local discovery_script=" set -e # Fast discovery without rescanning (devices already imported during cluster setup) if [ $skip_rescan -eq 1 ]; then # Direct discovery without slow rescan operations # Discover devices in order: SCSI, NVMe, then multipath # Discover iSCSI SCSI devices (excluding multipath backing devices) if [ $num_scsi -gt 0 ]; then # Get all LIO-ORG SCSI devices scsi_devs=\$(lsblk -d -n -o NAME,VENDOR 2>/dev/null | grep -i 'LIO-ORG' | awk '{print \$1}') # If multipath is enabled, filter out devices that are multipath slaves if [ $num_multipath -gt 0 ] && [ -n \"\$scsi_devs\" ]; then # Get list of multipath slave devices mpath_slaves=\$(multipath -ll 2>/dev/null | grep -oP '[0-9]+:[0-9]+:[0-9]+:[0-9]+\\s+\\K\\w+' || true) # Filter out slaves from SCSI device list for dev in \$scsi_devs; do if ! echo \"\$mpath_slaves\" | grep -qw \"\$dev\"; then echo \"/dev/\$dev\" fi done | sort else # No multipath, output all SCSI devices for dev in \$scsi_devs; do echo \"/dev/\$dev\" done | sort fi fi # Discover NVMe devices if [ $num_nvme -gt 0 ]; then nvme list 2>/dev/null | grep -oP '/dev/nvme[0-9]+n[0-9]+' | sort fi # Discover multipath devices if [ $num_multipath -gt 0 ]; then multipath -ll 2>/dev/null | grep -oP '^[a-z0-9]+(?= \()' | sort | while read mpath; do echo \"/dev/mapper/\$mpath\" done fi # Source the importer library if available (includes rescan - slower but thorough) elif [ -f /root/cluster-scripts/cluster-test-lib.sh ]; then source /root/cluster-scripts/cluster-test-lib.sh source /root/cluster-scripts/cluster-storage-importer.sh # Use the built-in discovery function # Filter output to only device paths (in case of stdout pollution from rescan) cluster_storage_discover_devices $num_scsi $num_nvme $num_multipath 2>/dev/null | grep '^/dev/' || exit 1 else # Fallback: manual discovery # Discover devices in order: SCSI, NVMe, then multipath # Discover iSCSI SCSI devices (excluding multipath backing devices) if [ $num_scsi -gt 0 ]; then # Get all LIO-ORG SCSI devices scsi_devs=\$(lsblk -d -n -o NAME,VENDOR 2>/dev/null | grep -i 'LIO-ORG' | awk '{print \$1}') # If multipath is enabled, filter out devices that are multipath slaves if [ $num_multipath -gt 0 ] && [ -n \"\$scsi_devs\" ]; then # Get list of multipath slave devices mpath_slaves=\$(multipath -ll 2>/dev/null | grep -oP '[0-9]+:[0-9]+:[0-9]+:[0-9]+\\s+\\K\\w+' || true) # Filter out slaves from SCSI device list for dev in \$scsi_devs; do if ! echo \"\$mpath_slaves\" | grep -qw \"\$dev\"; then echo \"/dev/\$dev\" fi done | sort else # No multipath, output all SCSI devices for dev in \$scsi_devs; do echo \"/dev/\$dev\" done | sort fi fi # Discover NVMe devices if [ $num_nvme -gt 0 ]; then nvme list 2>/dev/null | grep -oP '/dev/nvme[0-9]+n[0-9]+' | sort fi # Discover multipath devices if [ $num_multipath -gt 0 ]; then multipath -ll 2>/dev/null | grep -oP '^[a-z0-9]+(?= \()' | sort | while read mpath; do echo \"/dev/mapper/\$mpath\" done fi fi " cluster_node_exec "$node_num" "$discovery_script" return $? } # # cluster_setup_device_vars - Set up device variable system # # Discovers imported storage devices on all test nodes and creates # device variables: $dev1, $dev2, ..., $devN # # These variables point to the same physical devices across all nodes, # allowing tests to reference devices consistently: # $node1 vgcreate vg $dev1 $dev2 # $node2 vgchange --lock-start vg # # Also exports DEVICES array with all device paths. # # Arguments: # None (uses cluster configuration from environment) # # Returns: # 0 on success, 1 on failure # cluster_setup_device_vars() { if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi cluster_log "Discovering storage devices on test nodes" # Discover devices on node 1 (use as reference) # Use skip_rescan=1 because devices were already imported during cluster setup local devices=() while IFS= read -r dev; do if [ -n "$dev" ]; then devices+=("$dev") fi done < <(cluster_discover_devices_on_node 1 1) if [ ${#devices[@]} -eq 0 ]; then cluster_error "No storage devices found on node 1" local _diag_vm=$(cluster_vm_get_name "$CLUSTER_ID" 1) cluster_error "VM state: $(cluster_virsh domstate "$_diag_vm" 2>&1)" cluster_error "VM IPs: $(cluster_virsh domifaddr "$_diag_vm" 2>&1)" return 1 fi cluster_log "Found ${#devices[@]} device(s) on node 1" # Get WWNs for devices on node 1 (for verification) # Batch all WWN lookups in one SSH call for speed cluster_log "Verifying device WWNs across all nodes" local node1_wwns=() # Build script to get all WWNs/WWIDs at once # lsblk provides WWN for SCSI/NVMe; multipath -ll provides WWID for dm-multipath # (lsblk cannot report WWN for device-mapper devices) local wwn_script="{ lsblk -d -n -o NAME,WWN 2>/dev/null; multipath -ll 2>/dev/null | sed -n 's/^\([a-z][a-z0-9]*\) (\([^)]*\)).*/\1 \2/p'; } | grep -E '" for i in "${!devices[@]}"; do local dev_base=$(basename "${devices[$i]}") if [ $i -gt 0 ]; then wwn_script+="|" fi wwn_script+="^${dev_base} " done wwn_script+="' || true" # Get all WWNs in one SSH call local wwn_output=$(cluster_node_exec 1 "$wwn_script") # Parse WWNs in order for dev in "${devices[@]}"; do local dev_base=$(basename "$dev") local wwn=$(echo "$wwn_output" | grep "^${dev_base} " | awk '{print $2}' || echo "NO_WWN") wwn=$(echo "$wwn" | tr -d '[:space:]') # Remove whitespace [ -z "$wwn" ] && wwn="NO_WWN" node1_wwns+=("$wwn") cluster_debug " Node 1: $dev → WWN: $wwn" done # Build WWN-to-device mapping for all nodes # Instead of assuming devices match by position, we map by WWN declare -A node_device_maps # Key: "nodeN_devI" → Value: device_path # Store node 1 mapping for i in "${!devices[@]}"; do node_device_maps["1_${i}"]="${devices[$i]}" done # Map devices on other nodes by WWN local num_nodes="${CLUSTER_NUM_NODES}" for node_num in $(seq 2 "$num_nodes"); do local node_devices=() while IFS= read -r dev; do if [ -n "$dev" ]; then node_devices+=("$dev") fi done < <(cluster_discover_devices_on_node "$node_num" 1) # Use fast discovery (skip rescan) if [ ${#node_devices[@]} -ne ${#devices[@]} ]; then cluster_warn "Node $node_num has ${#node_devices[@]} devices, node 1 has ${#devices[@]}" cluster_warn "This may cause issues with device variable consistency" continue fi # Get WWNs for all devices on this node - batch all lookups in one SSH call cluster_debug "Mapping devices on node $node_num by WWN" # Build script to get all WWNs/WWIDs at once local node_wwn_script="{ lsblk -d -n -o NAME,WWN 2>/dev/null; multipath -ll 2>/dev/null | sed -n 's/^\([a-z][a-z0-9]*\) (\([^)]*\)).*/\1 \2/p'; } | grep -E '" for i in "${!node_devices[@]}"; do local dev_base=$(basename "${node_devices[$i]}") if [ $i -gt 0 ]; then node_wwn_script+="|" fi node_wwn_script+="^${dev_base} " done node_wwn_script+="' || true" # Get all WWNs in one SSH call local node_wwn_output=$(cluster_node_exec "$node_num" "$node_wwn_script") # Parse WWNs for this node local node_wwns=() for dev in "${node_devices[@]}"; do local dev_base=$(basename "$dev") local wwn=$(echo "$node_wwn_output" | grep "^${dev_base} " | awk '{print $2}' || echo "NO_WWN") wwn=$(echo "$wwn" | tr -d '[:space:]') # Remove whitespace [ -z "$wwn" ] && wwn="NO_WWN" node_wwns+=("$wwn") done # Map by WWN: for each node1 WWN, find matching device on current node for i in "${!node1_wwns[@]}"; do local target_wwn="${node1_wwns[$i]}" local found=0 for j in "${!node_wwns[@]}"; do if [ "${node_wwns[$j]}" = "$target_wwn" ]; then node_device_maps["${node_num}_${i}"]="${node_devices[$j]}" cluster_debug " dev$((i+1)) (WWN: $target_wwn) → Node $node_num: ${node_devices[$j]}" found=1 break fi done if [ $found -eq 0 ]; then cluster_error "WWN $target_wwn (dev$((i+1)) on node 1: ${devices[$i]}) not found on node $node_num" cluster_error "This means node $node_num is missing a physical device that exists on node 1" return 1 fi done done cluster_log "✓ Device WWN mapping completed - all nodes see the same physical devices" # Log device mapping for debugging if [ "${CLUSTER_DEBUG:-0}" -eq 1 ]; then cluster_debug "Device mapping by WWN:" for i in "${!node1_wwns[@]}"; do cluster_debug " dev$((i+1)) (WWN: ${node1_wwns[$i]}):" for node_num in $(seq 1 "$num_nodes"); do cluster_debug " Node $node_num: ${node_device_maps["${node_num}_${i}"]}" done done fi # Deploy per-node device mapping files cluster_log "Deploying device mappings to all nodes" for node_num in $(seq 1 "$num_nodes"); do # Build device variable script for this node local dev_map_script="#!/bin/bash # Auto-generated device mapping for node $node_num # Maps device variables to correct physical devices by WWN " # Counters for type-specific device aliases local scsi_count=0 local nvme_count=0 local mpath_count=0 # First pass: Create $dev1, $dev2, etc. for i in "${!devices[@]}"; do local dev_num=$((i + 1)) local dev_path="${node_device_maps["${node_num}_${i}"]}" dev_map_script+="export dev${dev_num}='${dev_path}' " done # Second pass: Create type-specific aliases ($scsi1, $nvme1, $mpath1, etc.) for i in "${!devices[@]}"; do local dev_path="${node_device_maps["${node_num}_${i}"]}" # Detect device type and create alias if [[ "$dev_path" =~ ^/dev/sd[a-z]+$ ]]; then # SCSI device scsi_count=$((scsi_count + 1)) dev_map_script+="export scsi${scsi_count}='${dev_path}' " elif [[ "$dev_path" =~ ^/dev/nvme[0-9]+n[0-9]+$ ]]; then # NVMe device nvme_count=$((nvme_count + 1)) dev_map_script+="export nvme${nvme_count}='${dev_path}' " elif [[ "$dev_path" =~ ^/dev/mapper/mpath ]]; then # Multipath device mpath_count=$((mpath_count + 1)) dev_map_script+="export mpath${mpath_count}='${dev_path}' " fi done # Add DEVICES array dev_map_script+="export DEVICES=(" for i in "${!devices[@]}"; do dev_map_script+="'${node_device_maps["${node_num}_${i}"]}' " done dev_map_script+=") " # Deploy to node if ! echo "$dev_map_script" | cluster_node_exec "$node_num" "cat > /root/cluster-dev-map.sh && chmod +x /root/cluster-dev-map.sh" > /dev/null; then cluster_error "Failed to reach node $node_num during setup (device map deployment)" return 1 fi cluster_debug "Deployed device map to node $node_num (scsi:$scsi_count nvme:$nvme_count mpath:$mpath_count)" done # Persist device mapping as exported variables for command translation. # Uses flat CLUSTER_DEV_MAP_<node>_<idx> variables instead of an associative # array because bash cannot export associative arrays to child processes. export CLUSTER_DEV_COUNT=${#devices[@]} for node_num in $(seq 1 "$num_nodes"); do for i in "${!devices[@]}"; do export "CLUSTER_DEV_MAP_${node_num}_${i}=${node_device_maps["${node_num}_${i}"]}" done done # Export per-device WWNs so refresh_devices can re-map by identity for i in "${!node1_wwns[@]}"; do export "CLUSTER_DEV_WWN_${i}=${node1_wwns[$i]}" done # Also export device variables in current shell (for node 1 paths, used in non-remote contexts). # First unset any stale variables from a previous test group that had more devices. if [ -n "${CLUSTER_PREV_DEV_COUNT:-}" ]; then for i in $(seq 1 "$CLUSTER_PREV_DEV_COUNT"); do unset "dev${i}" "scsi${i}" "nvme${i}" "mpath${i}" 2>/dev/null || true done fi export CLUSTER_PREV_DEV_COUNT=${#devices[@]} local scsi_count=0 local nvme_count=0 local mpath_count=0 for i in "${!devices[@]}"; do local dev_num=$((i + 1)) local dev_path="${devices[$i]}" export "dev${dev_num}=${dev_path}" # Export type-specific aliases if [[ "$dev_path" =~ ^/dev/sd[a-z]+$ ]]; then scsi_count=$((scsi_count + 1)) export "scsi${scsi_count}=${dev_path}" elif [[ "$dev_path" =~ ^/dev/nvme[0-9]+n[0-9]+$ ]]; then nvme_count=$((nvme_count + 1)) export "nvme${nvme_count}=${dev_path}" elif [[ "$dev_path" =~ ^/dev/mapper/mpath ]]; then mpath_count=$((mpath_count + 1)) export "mpath${mpath_count}=${dev_path}" fi done export DEVICES=("${devices[@]}") cluster_log "Device variables set up successfully" cluster_log " Devices: ${devices[*]}" [ $scsi_count -gt 0 ] && cluster_log " SCSI devices: $scsi_count" [ $nvme_count -gt 0 ] && cluster_log " NVMe devices: $nvme_count" [ $mpath_count -gt 0 ] && cluster_log " Multipath devices: $mpath_count" # Verify NVMe hostnqn uniqueness across nodes (duplicate hostnqn/hostid # breaks persistent reservations because nvmet identifies registrants # by hostid). if [ $nvme_count -gt 0 ]; then cluster_log "Verifying NVMe host identity uniqueness across nodes" local -a hostnqns=() for node_num in $(seq 1 "$num_nodes"); do local nqn nqn=$(cluster_node_exec "$node_num" "cat /etc/nvme/hostnqn 2>/dev/null" 2>/dev/null | tr -d '[:space:]') hostnqns+=("$nqn") cluster_debug " Node $node_num hostnqn: $nqn" done local dup_found=0 for ((i=0; i<${#hostnqns[@]}; i++)); do for ((j=i+1; j<${#hostnqns[@]}; j++)); do if [ "${hostnqns[$i]}" = "${hostnqns[$j]}" ]; then cluster_error "Nodes $((i+1)) and $((j+1)) share NVMe hostnqn: ${hostnqns[$i]}" dup_found=1 fi done done if [ $dup_found -eq 1 ]; then cluster_error "Duplicate NVMe hostnqn detected — PR operations will fail" cluster_error "Fix: remove /etc/nvme/hostnqn and /etc/nvme/hostid from the golden image" return 1 fi cluster_log " All $num_nodes nodes have unique NVMe hostnqn" fi # Set up debug log directory on each node cluster_log "Setting up debug log directory on nodes" for node_num in $(seq 1 "$num_nodes"); do if ! cluster_node_exec "$node_num" "mkdir -p /var/log/lvm-test" > /dev/null 2>&1; then cluster_error "Failed to reach node $node_num during setup (debug log directory)" return 1 fi cluster_debug "Created debug log directory on node $node_num: /var/log/lvm-test" done # Configure LVM debug logging in /etc/lvm/lvm.conf on each node cluster_log "Configuring LVM debug logging on nodes" # Get test name from environment (set by cluster_run_test) local test_name="${CLUSTER_TEST_NAME:-unknown}" for node_num in $(seq 1 "$num_nodes"); do # Backup existing lvm.conf if not already backed up if ! cluster_node_exec "$node_num" \ "[ -f /etc/lvm/lvm.conf.cluster-test-backup ] || cp /etc/lvm/lvm.conf /etc/lvm/lvm.conf.cluster-test-backup"; then cluster_error "Failed to reach node $node_num during setup (lvm.conf backup)" return 1 fi # Update lvm.conf with test-specific debug log file # Use lvmconfig to merge settings (safest approach) if ! cluster_node_exec "$node_num" \ "lvmconfig --mergedconfig --config 'log { file=\"/var/log/lvm-test/${test_name}-node${node_num}.log\" level=9 indent=1 activation=1 overwrite=0 }' > /etc/lvm/lvm.conf.tmp && mv /etc/lvm/lvm.conf.tmp /etc/lvm/lvm.conf"; then cluster_error "Failed to reach node $node_num during setup (lvm.conf logging)" return 1 fi cluster_debug "Configured LVM debug logging on node $node_num: ${test_name}-node${node_num}.log" done # Deploy test helper functions to all nodes cluster_log "Deploying test helper functions to all nodes" local helper_functions='#!/bin/bash # Auto-generated test helper functions for cluster tests # Node number (injected during deployment) export NODE_NUM=${NODE_NUM:-unknown} # # LVM Debug Logging Configuration # # Note: LVM debug logging is configured via /etc/lvm/lvm.conf # We do NOT set LVM_LOG_FILE_EPOCH to avoid file name suffixes # and to ensure all commands append to the same log file. # Set expected exit status for LVM commands export LVM_EXPECTED_EXIT_STATUS=1 # # Test Helper Functions # # not - Execute command expecting failure # Usage: not <command> [args...] # Returns 0 if command fails, 1 if command succeeds not() { # Set expected exit status for LVM commands export LVM_EXPECTED_EXIT_STATUS=">1" # Execute the command "$@" local exit_code=$? # Restore expected exit status export LVM_EXPECTED_EXIT_STATUS=1 # Invert exit code: success if command failed if [ $exit_code -eq 0 ]; then echo "ERROR: Command succeeded but was expected to fail: $*" >&2 return 1 else return 0 fi } # Export functions so they'\''re available in subshells export -f not ' # Deploy to all nodes for node_num in $(seq 1 "$num_nodes"); do # Inject NODE_NUM into helper script for this specific node local node_helper_script="${helper_functions//\$\{NODE_NUM:-unknown\}/$node_num}" if ! echo "$node_helper_script" | cluster_node_exec "$node_num" "cat > /root/cluster-node-helpers.sh && chmod +x /root/cluster-node-helpers.sh" > /dev/null; then cluster_error "Failed to reach node $node_num during setup (helper deployment)" return 1 fi cluster_debug "Deployed helper functions with debug logging to node $node_num" done # Deploy cluster test helpers if they exist local script_dir="$(dirname "${BASH_SOURCE[0]}")" local test_helpers="${script_dir}/shell/lib/cluster-test-helpers.sh" if [ -f "$test_helpers" ]; then cluster_log "Deploying cluster test helpers to all nodes" for node_num in $(seq 1 "$num_nodes"); do if ! cat "$test_helpers" | cluster_node_exec "$node_num" \ "cat > /root/cluster-test-helpers.sh && chmod +x /root/cluster-test-helpers.sh" > /dev/null; then cluster_error "Failed to reach node $node_num during setup (test helper deployment)" return 1 fi cluster_debug "Deployed cluster test helpers to node $node_num" done fi return 0 } # # cluster_deploy_test_files - Deploy test files to test nodes # # Copies test script and dependencies to all test nodes for execution. # # Arguments: # $1 - test_script (path to test script to deploy) # # Returns: # 0 on success, 1 on failure # cluster_deploy_test_files() { local test_script="$1" if [ -z "$test_script" ]; then cluster_error "cluster_deploy_test_files: test_script is required" return 1 fi if [ ! -f "$test_script" ]; then cluster_error "Test script not found: $test_script" return 1 fi if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi cluster_log "Deploying test files to cluster nodes" local num_nodes="${CLUSTER_NUM_NODES}" local test_dir="/root/cluster-tests" local script_name=$(basename "$test_script") # Deploy to each test node for node_num in $(seq 1 "$num_nodes"); do cluster_debug "Deploying test files to node $node_num" # Get node IP local vm_name=$(cluster_vm_get_name "$CLUSTER_ID" "$node_num") local node_ip if ! node_ip=$(cluster_vm_get_ip "$vm_name" 2>/dev/null); then cluster_error "Failed to get IP for node $node_num" return 1 fi # Create test directory on node cluster_vm_ssh "$node_ip" "mkdir -p $test_dir" || { cluster_error "Failed to create test directory on node $node_num" return 1 } # Copy test script cluster_vm_scp "$test_script" "${CLUSTER_SSH_USER}@${node_ip}:${test_dir}/${script_name}" || { cluster_error "Failed to copy test script to node $node_num" return 1 } # Make script executable cluster_vm_ssh "$node_ip" "chmod +x ${test_dir}/${script_name}" || { cluster_warn "Failed to make test script executable on node $node_num" } # Copy test/lib directory if it exists (test dependencies) local lib_dir="test/lib" if [ -d "$lib_dir" ]; then cluster_debug "Copying test/lib directory to node $node_num" # Use rsync via SCP for directory copy rsync -az -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i ${CLUSTER_SSH_KEY_DIR}/cluster_test_rsa" \ "$lib_dir/" "${CLUSTER_SSH_USER}@${node_ip}:${test_dir}/lib/" 2>/dev/null || { cluster_warn "Failed to copy test/lib to node $node_num (may not be needed)" } fi done cluster_log "Test files deployed successfully" return 0 } # # cluster_collect_debug_logs - Collect debug logs from all test nodes # # Copies LVM debug logs from all test nodes to local results directory. # Called automatically on test failure. # # Arguments: # $1 - test_name (name of the test, without .sh extension) # # Returns: # 0 on success, 1 on failure # cluster_restore_lvm_config() { if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi cluster_log "Restoring original LVM configuration on nodes" for node_num in $(seq 1 "$CLUSTER_NUM_NODES"); do cluster_node_exec "$node_num" \ "[ -f /etc/lvm/lvm.conf.cluster-test-backup ] && cp /etc/lvm/lvm.conf.cluster-test-backup /etc/lvm/lvm.conf || true" > /dev/null 2>&1 cluster_debug "Restored LVM configuration on node $node_num" done } # # cluster_stress_run - Run registered stress loops on all nodes concurrently # # For each node, generates a self-contained script that runs each registered # loop body inside a timed while-loop as a background subshell. All loops run # concurrently on all nodes for the specified duration, then terminate. # # Uses STRESS_LOOP_NAMES, STRESS_LOOP_BODIES, STRESS_LOOP_NODES arrays # populated by stress_loop/stress_loop_on. # # Arguments: # $1 - duration in minutes # # Returns: # 0 if all nodes completed normally, 1 if any node failed # cluster_stress_run() { local duration_secs="$1" if [ -z "$duration_secs" ]; then cluster_error "cluster_stress_run: duration in seconds is required" return 1 fi local num_nodes="${CLUSTER_NUM_NODES}" local num_loops=${#STRESS_LOOP_NAMES[@]} cluster_log "==========================================" cluster_log "Stress test: ${num_loops} loops on ${num_nodes} nodes for ${duration_secs} seconds" cluster_log "==========================================" for i in $(seq 0 $((num_loops - 1))); do cluster_log " loop: ${STRESS_LOOP_NAMES[$i]} (nodes: ${STRESS_LOOP_NODES[$i]})" done # Pre-resolve all node IPs local node_ips=() for node_num in $(seq 1 "$num_nodes"); do local vm_name=$(cluster_vm_get_name "$CLUSTER_ID" "$node_num") local node_ip if ! node_ip=$(cluster_vm_get_ip "$vm_name" 2>/dev/null); then cluster_error "Failed to get IP for node $node_num ($vm_name)" return 1 fi node_ips[$node_num]="$node_ip" done local ssh_key="${CLUSTER_SSH_KEY_DIR}/cluster_test_rsa" if [ ! -f "$ssh_key" ]; then cluster_error "SSH key not found: $ssh_key" return 1 fi local tmpdir=$(mktemp -d) trap "rm -rf '$tmpdir'" RETURN # Generate and deploy the stress script for each node for node_num in $(seq 1 "$num_nodes"); do local script_file="$tmpdir/stress_node${node_num}.sh" cat > "$script_file" <<'STRESS_HEADER' #!/bin/bash source /root/cluster-dev-map.sh 2>/dev/null STRESS_HEADER cat >> "$script_file" <<STRESS_VARS DURATION_SECS=$duration_secs END_TIME=\$(( \$(date +%s) + DURATION_SECS )) NODE_NUM=$node_num LOGDIR="/tmp/cluster-stress-logs" STRESS_VARS cat >> "$script_file" <<'STRESS_INIT' mkdir -p "$LOGDIR" rm -f "$LOGDIR"/*.log "$LOGDIR"/*.iter PIDS=() LOOP_NAMES=() STRESS_INIT # Add each loop that applies to this node for i in $(seq 0 $((num_loops - 1))); do local loop_name="${STRESS_LOOP_NAMES[$i]}" local loop_body="${STRESS_LOOP_BODIES[$i]}" local loop_nodes="${STRESS_LOOP_NODES[$i]}" # Check if this loop runs on this node if [ "$loop_nodes" != "all" ]; then local runs_here=0 for n in $loop_nodes; do if [ "$n" = "$node_num" ]; then runs_here=1 break fi done if [ "$runs_here" -eq 0 ]; then continue fi fi cat >> "$script_file" <<STRESS_LOOP # Loop: $loop_name ( set +e iter=0 while [ \$(date +%s) -lt \$END_TIME ]; do iter=\$((iter + 1)) echo \$iter > "\$LOGDIR/${loop_name}.iter" $loop_body done echo "loop_iterations=\$iter" ) > "\$LOGDIR/${loop_name}.log" 2>&1 & PIDS+=(\$!) LOOP_NAMES+=("$loop_name") STRESS_LOOP done cat >> "$script_file" <<'STRESS_FOOTER' if [ ${#PIDS[@]} -eq 0 ]; then echo "STRESS_COMPLETE loops=0" exit 0 fi # Wait for loops to finish naturally via END_TIME, plus buffer for # the final iteration to complete and write its iteration count. # Kill only as a backstop for stuck loops. sleep $((DURATION_SECS + 30)) for pid in "${PIDS[@]}"; do kill $pid 2>/dev/null done wait 2>/dev/null # Report results failed=0 for idx in "${!LOOP_NAMES[@]}"; do name="${LOOP_NAMES[$idx]}" logfile="$LOGDIR/${name}.log" iters=$(grep -o 'loop_iterations=[0-9]*' "$logfile" 2>/dev/null | cut -d= -f2) if [ -z "$iters" ]; then iters=$(cat "$LOGDIR/${name}.iter" 2>/dev/null) fi if [ -z "$iters" ]; then echo " ${name}: CRASHED" failed=1 else echo " ${name}: completed (${iters} iterations)" fi done echo "STRESS_COMPLETE loops=${#LOOP_NAMES[@]}" exit $failed STRESS_FOOTER # Deploy the script to the node local node_ip="${node_ips[$node_num]}" scp -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -i "$ssh_key" \ "$script_file" \ "${CLUSTER_SSH_USER}@${node_ip}:/tmp/cluster-stress.sh" > /dev/null 2>&1 if [ $? -ne 0 ]; then cluster_error "Failed to deploy stress script to node $node_num" return 1 fi cluster_debug "Deployed stress script to node $node_num" done # Launch the stress script on all nodes in parallel local pids=() for node_num in $(seq 1 "$num_nodes"); do local output_file="$tmpdir/node${node_num}.out" local status_file="$tmpdir/node${node_num}.status" local node_ip="${node_ips[$node_num]}" bash -c " ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -o ServerAliveInterval=30 \ -o ServerAliveCountMax=10 \ -i '$ssh_key' \ '$CLUSTER_SSH_USER@$node_ip' \ 'bash /tmp/cluster-stress.sh' >'$output_file' 2>&1 echo \$? > '$status_file' " & pids+=($!) cluster_debug "Launched stress on node $node_num (PID: ${pids[-1]})" done cluster_log "Stress running on all nodes for ${duration_secs} seconds..." # Wait for all nodes for pid in "${pids[@]}"; do wait "$pid" 2>/dev/null || true done sleep 0.1 # Collect and display results local failed=0 for node_num in $(seq 1 "$num_nodes"); do local output_file="$tmpdir/node${node_num}.out" local status_file="$tmpdir/node${node_num}.status" if [ ! -f "$status_file" ]; then echo " node${node_num}: ERROR (no status)" >&2 failed=1 continue fi local status=$(cat "$status_file" 2>/dev/null || echo "-1") if [ "$status" -eq 0 ]; then echo " node${node_num}: success" else echo " node${node_num}: failed" failed=1 fi if [ -s "$output_file" ]; then cat "$output_file" fi done # Collect stress logs from all nodes cluster_stress_collect_logs if [ $failed -eq 0 ]; then cluster_log "Stress test completed successfully" # Remove stress logs on success - only failures need debugging local results_dir="${CLUSTER_RESULTS_DIR:-$(pwd)/results}" local test_name="${CLUSTER_TEST_NAME:-stress}" local timestamp="${CLUSTER_TEST_TIMESTAMP:-$(date +%m%d%H%M%S)}" local stress_dir="$results_dir/${test_name}_${timestamp}_stress" rm -rf "$stress_dir" else cluster_log "Stress test FAILED on one or more nodes" fi return $failed } # # cluster_stress_collect_logs - Collect stress loop logs from all nodes # # Copies per-loop log files from /tmp/cluster-stress-logs/ on each node # to the local results directory. # cluster_stress_collect_logs() { local results_dir="${CLUSTER_RESULTS_DIR:-$(pwd)/results}" local test_name="${CLUSTER_TEST_NAME:-stress}" local timestamp="${CLUSTER_TEST_TIMESTAMP:-$(date +%m%d%H%M%S)}" local stress_dir="$results_dir/${test_name}_${timestamp}_stress" mkdir -p "$stress_dir" 2>/dev/null || true for node_num in $(seq 1 "$CLUSTER_NUM_NODES"); do local vm_name=$(cluster_vm_get_name "$CLUSTER_ID" "$node_num") local node_ip node_ip=$(cluster_vm_get_ip "$vm_name" 2>/dev/null) || continue local node_dir="$stress_dir/node${node_num}" mkdir -p "$node_dir" 2>/dev/null || true local ssh_key="${CLUSTER_SSH_KEY_DIR}/cluster_test_rsa" # List log files on the node and copy each one local log_files log_files=$(ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -i "$ssh_key" \ "${CLUSTER_SSH_USER}@${node_ip}" \ "ls /tmp/cluster-stress-logs/*.log 2>/dev/null" 2>/dev/null) || continue for remote_log in $log_files; do local log_name=$(basename "$remote_log") scp -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -i "$ssh_key" \ "${CLUSTER_SSH_USER}@${node_ip}:${remote_log}" \ "${node_dir}/${log_name}" > /dev/null 2>&1 || true done cluster_debug "Collected stress logs from node $node_num" done chmod -R a+rw "$stress_dir" 2>/dev/null || true cluster_log "Stress logs collected to: $stress_dir" } # # cluster_collect_debug_logs - Collect debug logs from all test nodes # # Copies LVM debug logs from all test nodes to local results directory. # Called automatically on test failure. # # Arguments: # $1 - test_name (name of the test, without .sh extension) # # Returns: # 0 on success, 1 on failure # cluster_collect_debug_logs() { local test_name="$1" local prefix="${2:-}" local timestamp="${CLUSTER_TEST_TIMESTAMP:-unknown}" if [ -z "$test_name" ]; then cluster_error "cluster_collect_debug_logs: test_name is required" return 1 fi if [ -z "${CLUSTER_NUM_NODES:-}" ]; then cluster_error "CLUSTER_NUM_NODES not set" return 1 fi # Determine script directory for results path with timestamp local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local failure_logs_dir="$script_dir/results/${prefix}${test_name}_${timestamp}_debug" cluster_log "Collecting debug logs from all nodes" # Create failure logs directory mkdir -p "$failure_logs_dir" || { cluster_error "Failed to create failure logs directory: $failure_logs_dir" return 1 } chmod 777 "$failure_logs_dir" 2>/dev/null || true # Copy config file to debug directory if [ -n "${CLUSTER_CONFIG_FILE:-}" ] && [ -f "$CLUSTER_CONFIG_FILE" ]; then cp "$CLUSTER_CONFIG_FILE" "$failure_logs_dir/$(basename "$CLUSTER_CONFIG_FILE")" 2>/dev/null || true cluster_debug "Copied config file to debug directory" fi # Copy group file to debug directory (if running from group) if [ -n "${CLUSTER_GROUP_FILE:-}" ] && [ -f "$CLUSTER_GROUP_FILE" ]; then cp "$CLUSTER_GROUP_FILE" "$failure_logs_dir/$(basename "$CLUSTER_GROUP_FILE")" 2>/dev/null || true cluster_debug "Copied group file to debug directory" fi local num_nodes="${CLUSTER_NUM_NODES}" local collected=0 for node_num in $(seq 1 "$num_nodes"); do cluster_debug "Collecting debug logs from node $node_num" # Get node IP local vm_name=$(cluster_vm_get_name "$CLUSTER_ID" "$node_num") local node_ip if ! node_ip=$(cluster_vm_get_ip "$vm_name" 2>/dev/null); then cluster_warn "Failed to get IP for node $node_num, skipping log collection" continue fi # Check if debug log exists for this test on node # LVM may append suffix like _DEBUG_PID_number, so use pattern match local log_pattern="${test_name}-node${node_num}.log*" local log_files if ! log_files=$(cluster_node_exec "$node_num" "ls /var/log/lvm-test/${log_pattern} 2>/dev/null" 2>/dev/null); then cluster_debug "No debug log found on node $node_num: /var/log/lvm-test/${log_pattern}" continue fi # Copy all matching log files to failure logs directory # If LVM created multiple files (with suffixes), copy them all while IFS= read -r remote_log; do if [ -z "$remote_log" ]; then continue fi local base_name=$(basename "$remote_log") cluster_vm_scp "${CLUSTER_SSH_USER}@${node_ip}:${remote_log}" \ "$failure_logs_dir/${base_name}" 2>/dev/null || { cluster_warn "Failed to copy debug log from node $node_num: $remote_log" continue } chmod 666 "$failure_logs_dir/${base_name}" 2>/dev/null || true cluster_debug "Copied debug log: $base_name" done <<< "$log_files" # Collect dmesg output from node local dmesg_file="${test_name}-node${node_num}-dmesg.log" if cluster_node_exec "$node_num" "dmesg > /tmp/${dmesg_file} 2>&1" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node_ip}:/tmp/${dmesg_file}" \ "$failure_logs_dir/${dmesg_file}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${dmesg_file}" 2>/dev/null || true cluster_debug "Collected dmesg from node $node_num" } || { cluster_debug "Failed to copy dmesg from node $node_num" } # Clean up remote dmesg file cluster_node_exec "$node_num" "rm -f /tmp/${dmesg_file}" > /dev/null 2>&1 || true # Check for iSCSI connection failures in dmesg if [ -f "$failure_logs_dir/${dmesg_file}" ]; then if grep -q "ISCSI_ERR_NOP_TIMEDOUT\|detected conn error\|session recovery timed out" \ "$failure_logs_dir/${dmesg_file}" 2>/dev/null; then cluster_warn "Node $node_num: iSCSI connection failure detected." cluster_warn " NOP timeout often means the target stopped responding while a SCSI" cluster_warn " command (e.g. sg_persist preempt-abort) was in flight, not initiator" cluster_warn " CPU scheduling. Check node0 (LIO target) logs in the debug directory." fi fi else cluster_debug "Failed to collect dmesg from node $node_num" fi # Collect journalctl output from node since test start local journal_file="${test_name}-node${node_num}-journal.log" if cluster_node_exec "$node_num" "[ -f /tmp/cluster_test_start_time ]" > /dev/null 2>&1; then # Get the saved start time local start_time if start_time=$(cluster_node_exec "$node_num" "cat /tmp/cluster_test_start_time" 2>/dev/null); then # Collect journalctl since that time if cluster_node_exec "$node_num" "journalctl --since '${start_time}' > /tmp/${journal_file} 2>&1" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node_ip}:/tmp/${journal_file}" \ "$failure_logs_dir/${journal_file}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${journal_file}" 2>/dev/null || true cluster_debug "Collected journalctl from node $node_num" } || { cluster_debug "Failed to copy journalctl from node $node_num" } # Clean up remote journal file cluster_node_exec "$node_num" "rm -f /tmp/${journal_file}" > /dev/null 2>&1 || true else cluster_debug "Failed to collect journalctl from node $node_num" fi else cluster_debug "Failed to read start time for journalctl on node $node_num" fi else cluster_debug "No start time saved for journalctl on node $node_num" fi # Collect sanlock log_dump output from node (if sanlock is running) local sanlock_file="${test_name}-node${node_num}-sanlock.log" if cluster_node_exec "$node_num" "command -v sanlock >/dev/null 2>&1" > /dev/null 2>&1; then # Note: sanlock log_dump doesn't work with > redirect, use tee instead if cluster_node_exec "$node_num" "sanlock log_dump 2>&1 | tee /tmp/${sanlock_file} >/dev/null" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node_ip}:/tmp/${sanlock_file}" \ "$failure_logs_dir/${sanlock_file}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${sanlock_file}" 2>/dev/null || true cluster_debug "Collected sanlock log_dump from node $node_num" } || { cluster_debug "Failed to copy sanlock log_dump from node $node_num" } # Clean up remote sanlock file cluster_node_exec "$node_num" "rm -f /tmp/${sanlock_file}" > /dev/null 2>&1 || true else cluster_debug "Failed to collect sanlock log_dump from node $node_num" fi else cluster_debug "sanlock not available on node $node_num" fi # Collect lvmlockctl output from node (if lvmlockd is running) local lvmlockd_file="${test_name}-node${node_num}-lvmlockd.log" if cluster_node_exec "$node_num" "command -v lvmlockctl >/dev/null 2>&1" > /dev/null 2>&1; then if cluster_node_exec "$node_num" "lvmlockctl -d > /tmp/${lvmlockd_file} 2>&1" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node_ip}:/tmp/${lvmlockd_file}" \ "$failure_logs_dir/${lvmlockd_file}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${lvmlockd_file}" 2>/dev/null || true cluster_debug "Collected lvmlockctl -d from node $node_num" } || { cluster_debug "Failed to copy lvmlockctl output from node $node_num" } # Clean up remote lvmlockd file cluster_node_exec "$node_num" "rm -f /tmp/${lvmlockd_file}" > /dev/null 2>&1 || true else cluster_debug "Failed to collect lvmlockctl -d from node $node_num" fi else cluster_debug "lvmlockctl not available on node $node_num" fi collected=$((collected + 1)) cluster_debug "Collected debug logs from node $node_num" done # Collect debug logs from node0 (storage exporter / iSCSI target) local vm_name_node0=$(cluster_vm_get_name "$CLUSTER_ID" 0) local node0_ip if node0_ip=$(cluster_vm_get_ip "$vm_name_node0" 2>/dev/null); then cluster_debug "Collecting debug logs from node 0 (storage exporter)" local dmesg_file_node0="${test_name}-node0-dmesg.log" if cluster_node_exec 0 "dmesg > /tmp/${dmesg_file_node0} 2>&1" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node0_ip}:/tmp/${dmesg_file_node0}" \ "$failure_logs_dir/${dmesg_file_node0}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${dmesg_file_node0}" 2>/dev/null || true cluster_debug "Collected dmesg from node 0" } cluster_node_exec 0 "rm -f /tmp/${dmesg_file_node0}" > /dev/null 2>&1 || true fi local journal_file_node0="${test_name}-node0-journal.log" if cluster_node_exec 0 "[ -f /tmp/cluster_test_start_time ]" > /dev/null 2>&1; then local start_time_node0 if start_time_node0=$(cluster_node_exec 0 "cat /tmp/cluster_test_start_time" 2>/dev/null); then if cluster_node_exec 0 "journalctl --since '${start_time_node0}' > /tmp/${journal_file_node0} 2>&1" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node0_ip}:/tmp/${journal_file_node0}" \ "$failure_logs_dir/${journal_file_node0}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${journal_file_node0}" 2>/dev/null || true cluster_debug "Collected journalctl from node 0" } cluster_node_exec 0 "rm -f /tmp/${journal_file_node0}" > /dev/null 2>&1 || true fi fi else if cluster_node_exec 0 "journalctl -b > /tmp/${journal_file_node0} 2>&1" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node0_ip}:/tmp/${journal_file_node0}" \ "$failure_logs_dir/${journal_file_node0}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${journal_file_node0}" 2>/dev/null || true cluster_debug "Collected journalctl (full boot) from node 0" } cluster_node_exec 0 "rm -f /tmp/${journal_file_node0}" > /dev/null 2>&1 || true fi fi local lio_file_node0="${test_name}-node0-targetcli.log" if cluster_node_exec 0 "command -v targetcli >/dev/null 2>&1" > /dev/null 2>&1; then if cluster_node_exec 0 "targetcli ls > /tmp/${lio_file_node0} 2>&1" > /dev/null 2>&1; then cluster_vm_scp "${CLUSTER_SSH_USER}@${node0_ip}:/tmp/${lio_file_node0}" \ "$failure_logs_dir/${lio_file_node0}" 2>/dev/null && { chmod 666 "$failure_logs_dir/${lio_file_node0}" 2>/dev/null || true cluster_debug "Collected targetcli status from node 0" } cluster_node_exec 0 "rm -f /tmp/${lio_file_node0}" > /dev/null 2>&1 || true fi fi else cluster_debug "Failed to get IP for node 0, skipping log collection" fi # Report VM steal time delta for all nodes (detects hypervisor scheduling pressure) local steal_file="${test_name}-steal-time.log" local steal_path="$failure_logs_dir/${steal_file}" local steal_warned=0 for node_num in $(seq 0 "$CLUSTER_NUM_NODES"); do local steal_start steal_end steal_delta steal_secs steal_start=$(cluster_node_exec "$node_num" "cat /tmp/cluster_steal_start 2>/dev/null" 2>/dev/null) || continue steal_end=$(cluster_node_exec "$node_num" "awk '/^cpu / {print \$9}' /proc/stat 2>/dev/null" 2>/dev/null) || continue if [ -n "$steal_start" ] && [ -n "$steal_end" ]; then steal_delta=$((steal_end - steal_start)) steal_secs=$((steal_delta / 100)) echo "node $node_num: steal_start=$steal_start steal_end=$steal_end delta=${steal_delta} (${steal_secs}s)" >> "$steal_path" if [ "$steal_secs" -ge 10 ]; then cluster_warn "Node $node_num: ${steal_secs}s of VM steal time during test — hypervisor scheduling pressure likely." steal_warned=1 fi fi done if [ -f "$steal_path" ]; then chmod 666 "$steal_path" 2>/dev/null || true cluster_debug "Collected steal time report" if [ "$steal_warned" -eq 0 ]; then cluster_debug "No significant VM steal time detected" fi fi if [ $collected -gt 0 ]; then cluster_log "Debug logs collected from $collected node(s) to: $failure_logs_dir" return 0 else cluster_warn "No debug logs were collected from any nodes" return 1 fi } # # cluster_run_test - Run a test script on the cluster # # This is the main entry point for test execution. It: # 1. Sets up test variables ($node1, $nodes, etc.) # 2. Sets up device variables ($dev1, $dev2, etc.) # 3. Deploys test files to nodes # 4. Executes the test script # 5. Collects and reports results # # Arguments: # $1 - test_script (path to test script) # # Returns: # Exit code from test script # cluster_run_test() { local test_script="$1" if [ -z "$test_script" ]; then cluster_error "cluster_run_test: test_script is required" return 1 fi if [ ! -f "$test_script" ]; then cluster_error "Test script not found: $test_script" return 1 fi # Determine log file path with timestamp local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local results_dir="$script_dir/results" local test_name="$(basename "$test_script" .sh)" local timestamp="$(date +%m%d%H%M%S)" local log_file="$results_dir/log_${test_name}_${timestamp}.txt" # Export timestamp, test name, and results dir for use in child process export CLUSTER_TEST_TIMESTAMP="$timestamp" export CLUSTER_TEST_NAME="$test_name" export CLUSTER_RESULTS_DIR="$results_dir" # Create results directory if [ ! -d "$results_dir" ]; then mkdir -p "$results_dir" || { cluster_warn "Failed to create results directory: $results_dir" cluster_warn "Test output will not be logged to file" log_file="" } chmod 777 "$results_dir" 2>/dev/null || true fi cluster_log "==========================================" cluster_log "Running cluster test: $(basename "$test_script")" cluster_log "==========================================" cluster_log "Cluster ID: ${CLUSTER_ID}" cluster_log "Test nodes: ${CLUSTER_NUM_NODES}" cluster_log "==========================================" # Initialize log file if [ -n "$log_file" ]; then { echo "==========================================" echo "Cluster Test Log" echo "==========================================" echo "Test: $(basename "$test_script")" echo "Cluster ID: ${CLUSTER_ID}" echo "Test nodes: ${CLUSTER_NUM_NODES}" if [ -n "${CLUSTER_CONFIG_FILE:-}" ]; then echo "Config: $(basename "$CLUSTER_CONFIG_FILE")" fi if [ -n "${CLUSTER_GROUP_FILE:-}" ]; then echo "Group: $(basename "$CLUSTER_GROUP_FILE")" fi echo "Date: $(date)" echo "==========================================" echo "" } > "$log_file" chmod 666 "$log_file" 2>/dev/null || true if [ -n "${CLUSTER_REVERT_LOG:-}" ] && [ -f "$CLUSTER_REVERT_LOG" ] && [ -s "$CLUSTER_REVERT_LOG" ]; then echo "--- Cluster revert (before this test) ---" >> "$log_file" cat "$CLUSTER_REVERT_LOG" >> "$log_file" echo "--- End revert log ---" >> "$log_file" echo "" >> "$log_file" rm -f "$CLUSTER_REVERT_LOG" unset CLUSTER_REVERT_LOG fi cluster_log "Test output will be logged to: $log_file" fi # Start logging all output (setup + execution + results) to the log file. # Use a file descriptor with tee so that setup failures are also captured. local _log_tee_pid="" if [ -n "$log_file" ]; then exec 3>&1 4>&2 exec > >(tee -a "$log_file") 2>&1 _log_tee_pid=$! fi # Step 1: Set up test variables cluster_log "Setting up test variables" if ! cluster_setup_test_vars; then cluster_error "Failed to set up test variables" if [ -n "$log_file" ]; then exec 1>&3 2>&4 3>&- 4>&- wait "$_log_tee_pid" 2>/dev/null || true fi return 1 fi # Step 2: Set up device variables cluster_log "Setting up device variables" if ! cluster_setup_device_vars; then cluster_error "Failed to set up device variables" if [ -n "$log_file" ]; then exec 1>&3 2>&4 3>&- 4>&- wait "$_log_tee_pid" 2>/dev/null || true fi return 1 fi # Step 3: Clean up all old debug logs from previous tests cluster_log "Cleaning up old debug logs on all nodes" for node_num in $(seq 1 "$CLUSTER_NUM_NODES"); do # Remove all old logs from /var/log/lvm-test/ cluster_node_exec "$node_num" "rm -rf /var/log/lvm-test/* 2>/dev/null || true" > /dev/null cluster_debug "Cleaned all debug logs on node $node_num" done # Clear kernel message buffer and save journal start time on all nodes cluster_log "Clearing kernel message buffer on all nodes" for node_num in $(seq 1 "$CLUSTER_NUM_NODES"); do # Clear dmesg cluster_node_exec "$node_num" "dmesg -C 2>/dev/null || true" > /dev/null cluster_debug "Cleared dmesg on node $node_num" # Save current timestamp for journalctl collection cluster_node_exec "$node_num" "date '+%Y-%m-%d %H:%M:%S' > /tmp/cluster_test_start_time" > /dev/null cluster_debug "Saved journal start time on node $node_num" done # Clear dmesg and save journal start time on node0 (storage exporter) cluster_node_exec 0 "dmesg -C 2>/dev/null || true" > /dev/null 2>&1 cluster_node_exec 0 "date '+%Y-%m-%d %H:%M:%S' > /tmp/cluster_test_start_time" > /dev/null 2>&1 cluster_debug "Cleared dmesg and saved journal start time on node 0" # Snapshot VM steal time on all nodes for detecting hypervisor scheduling pressure for node_num in $(seq 0 "$CLUSTER_NUM_NODES"); do cluster_node_exec "$node_num" "awk '/^cpu / {print \$9}' /proc/stat > /tmp/cluster_steal_start" > /dev/null 2>&1 done cluster_debug "Saved steal time baseline on all nodes" # Step 4: Deploy test files (optional - only if not executing locally) # For now, we'll execute the test script locally with SSH commands # A future enhancement could deploy and execute remotely # Step 5: Execute test script cluster_log "==========================================" cluster_log "Executing test script: $(basename "$test_script")" cluster_log "==========================================" # Create a wrapper to track failures with line numbers local fail_info_file="/tmp/cluster_test_fail_$$" local wrapper_script="/tmp/cluster_test_wrapper_$$" cat > "$wrapper_script" <<WRAPPER_EOF #!/bin/bash set -eE # Save test file path for trap TEST_SCRIPT="\$1" # ERR trap that captures the line number from the test script, not from helper functions trap 'failure_handler' ERR failure_handler() { local line_num="" local cmd="" # Walk the call stack backwards to find the frame from the test script # BASH_LINENO[i] contains the line number where frame i+1 was called # BASH_SOURCE[i+1] contains the file where frame i+1 was called from local i for (( i=0; i < \${#BASH_LINENO[@]}; i++ )); do # Get the absolute path of the source file at this frame local source_file="\${BASH_SOURCE[i+1]}" # Check if this frame is from the test script if [[ "\$source_file" == "\$TEST_SCRIPT" ]] || [[ "\$(realpath "\$source_file" 2>/dev/null)" == "\$(realpath "\$TEST_SCRIPT" 2>/dev/null)" ]]; then line_num=\${BASH_LINENO[i]} break fi done # If we didn't find the test script in the call stack, use the deepest frame if [[ -z "\$line_num" ]] || [[ "\$line_num" == "0" ]]; then # Try to find the last non-zero line number for (( i=\${#BASH_LINENO[@]}-1; i >= 0; i-- )); do if [[ \${BASH_LINENO[i]} -gt 0 ]]; then line_num=\${BASH_LINENO[i]} break fi done fi # Fallback to immediate caller if still not found if [[ -z "\$line_num" ]] || [[ "\$line_num" == "0" ]]; then line_num=\${BASH_LINENO[0]} fi # Extract the actual command from the test script if [[ -n "\$line_num" ]] && [[ -f "\$TEST_SCRIPT" ]]; then # Read the specific line from the test script and trim whitespace cmd=\$(sed -n "\${line_num}p" "\$TEST_SCRIPT" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') fi # Fallback to BASH_COMMAND if we couldn't read from file if [[ -z "\$cmd" ]]; then cmd="\${BASH_COMMAND}" fi echo "FAILED_LINE:\$line_num:\$cmd" > $fail_info_file exit 1 } # Source cluster test helpers if they exist (for test scripts to use) if [ -f "$script_dir/shell/lib/cluster-test-helpers.sh" ]; then source "$script_dir/shell/lib/cluster-test-helpers.sh" fi source "\$TEST_SCRIPT" WRAPPER_EOF chmod +x "$wrapper_script" # Execute test script bash "$wrapper_script" "$test_script" 2>&1 local exit_code=$? # Capture failure information local failed_line="" local failed_command="" if [ $exit_code -ne 0 ] && [ -f "$fail_info_file" ]; then local fail_info=$(cat "$fail_info_file") failed_line=$(echo "$fail_info" | cut -d: -f2) failed_command=$(echo "$fail_info" | cut -d: -f3-) fi # Cleanup wrapper files rm -f "$wrapper_script" "$fail_info_file" # Step 6: Report results local fail_prefix="FAILED_" if [ $exit_code -ne 0 ] && cluster_is_known_failure "$test_name"; then fail_prefix="KNOWN_" fi cluster_log "==========================================" if [ $exit_code -eq 0 ]; then cluster_log "Test PASSED: $(basename "$test_script")" else if [ -n "$failed_line" ] && [ -n "$failed_command" ]; then cluster_error "Test ${fail_prefix%_}: $(basename "$test_script") line $failed_line $failed_command" else cluster_error "Test ${fail_prefix%_}: $(basename "$test_script") (exit code: $exit_code)" fi # Collect debug logs on failure cluster_log "Collecting debug logs from ${fail_prefix%_} test" cluster_collect_debug_logs "$test_name" "$fail_prefix" || { cluster_warn "Some debug logs could not be collected" } fi cluster_log "==========================================" # Step 7: Restore original LVM configuration cluster_restore_lvm_config # Stop logging to file and restore original stdout/stderr if [ -n "$log_file" ]; then { echo "" echo "==========================================" if [ $exit_code -eq 0 ]; then echo "Test PASSED: $(basename "$test_script")" else if [ -n "$failed_line" ] && [ -n "$failed_command" ]; then echo "Test ${fail_prefix%_}: $(basename "$test_script") line $failed_line $failed_command" else echo "Test ${fail_prefix%_}: $(basename "$test_script") (exit code: $exit_code)" fi fi echo "Date: $(date)" echo "==========================================" } >> "$log_file" exec 1>&3 2>&4 3>&- 4>&- wait "$_log_tee_pid" 2>/dev/null || true # Rename log file based on test result local final_log_file="" if [ $exit_code -eq 0 ]; then # Test passed - move to passed subdirectory mkdir -p "$results_dir/passed" 2>/dev/null || true chmod 777 "$results_dir/passed" 2>/dev/null || true final_log_file="$results_dir/passed/log_${test_name}_${timestamp}.txt" mv "$log_file" "$final_log_file" 2>/dev/null || final_log_file="$log_file" else final_log_file="$results_dir/${fail_prefix}${test_name}_${timestamp}.txt" mv "$log_file" "$final_log_file" 2>/dev/null || final_log_file="$log_file" chmod 666 "$final_log_file" 2>/dev/null || true fi cluster_log "Test log saved to: $final_log_file" fi return $exit_code } # Export functions for use in other scripts export -f cluster_translate_devs export -f cluster_node_exec export -f cluster_all_exec export -f cluster_nodes_exec export -f cluster_setup_test_vars export -f cluster_setup_device_vars export -f cluster_discover_devices_on_node export -f cluster_deploy_test_files export -f cluster_restore_lvm_config export -f cluster_stress_run export -f cluster_stress_collect_logs export -f cluster_collect_debug_logs export -f cluster_run_test