#!/bin/bash set -e # Multi-node E2E test runner # This script sets up a 3-node Hive cluster using simulated IPs on the loopback interface # and runs distributed instance tests. # Ensure we are in the project root cd "$(dirname "$0")/../.." # Source helper functions source ./tests/e2e/lib/multinode-helpers.sh # Cleanup function - ensure resources are cleaned up on exit cleanup() { local exit_code=$? echo "" echo "Cleanup triggered (exit code: $exit_code)..." if [ $exit_code -ne 0 ]; then dump_all_node_logs fi # Kill any lingering formation background processes [ -n "$LEADER_INIT_PID" ] && kill "$LEADER_INIT_PID" 2>/dev/null || true [ -n "$JOIN2_PID" ] && kill "$JOIN2_PID" 2>/dev/null || true [ -n "$JOIN3_PID" ] && kill "$JOIN3_PID" 2>/dev/null || true # Stop all node services stop_all_nodes || true # Remove simulated IPs remove_simulated_ips || true echo "Cleanup complete" } trap cleanup EXIT # PIDs for background formation processes (used in cleanup) JOIN2_PID="" JOIN3_PID="" # Use Hive profile for AWS CLI export AWS_PROFILE=hive echo "========================================" echo "Multi-Node E2E Test Suite" echo "========================================" echo "" # Phase 1: Environment Setup echo "Phase 1: Environment Setup" echo "========================================" # Check for KVM support echo "Checking for KVM support..." if [ -e /dev/kvm ]; then echo " /dev/kvm exists" if [ -w /dev/kvm ]; then echo " /dev/kvm is writable" else echo " ERROR: /dev/kvm is NOT writable" exit 1 fi else echo " ERROR: /dev/kvm does NOT exist" exit 1 fi # Check for ip command (iproute2) if ! command -v ip &> /dev/null; then echo " ERROR: 'ip' command not found. Install iproute2." exit 1 fi # Setup simulated network echo "" echo "Setting up simulated network..." add_simulated_ips # Create ramdisk mount point mkdir -p /mnt/ramdisk echo "" # Phase 2: Cluster Initialization echo "Phase 2: Cluster Initialization" echo "========================================" # Background init — starts formation server, generates certs first echo "" init_leader_node # Trust CA cert (exists before formation completes — cert generation is the first step) echo "" echo "Adding Hive CA certificate to system trust store..." sudo cp ~/node1/config/ca.pem /usr/local/share/ca-certificates/hive-ca.crt sudo update-ca-certificates # Background BOTH joins — they poll /formation/status until all 3 nodes have joined. # Must be concurrent: each join blocks until formation is complete. echo "" echo "Joining follower nodes concurrently..." join_follower_node 2 & JOIN2_PID=$! join_follower_node 3 & JOIN3_PID=$! # Wait for formation to complete (all processes generate their configs) echo "Waiting for cluster formation to complete..." wait $JOIN2_PID || { echo "ERROR: Node 2 join failed"; exit 1; } wait $JOIN3_PID || { echo "ERROR: Node 3 join failed"; exit 1; } wait $LEADER_INIT_PID || { echo "ERROR: Leader init failed"; exit 1; } echo "Cluster formation complete — all configs generated" # Now start services (configs exist for all nodes) echo "" echo "Starting node services..." start_node_services 1 "$HOME/node1" start_node_services 2 "$HOME/node2" start_node_services 3 "$HOME/node3" # Wait for all services to stabilize echo "" echo "Waiting for cluster to stabilize..." sleep 10 # Phase 3: Cluster Health Verification echo "" echo "Phase 3: Cluster Health Verification" echo "========================================" # Verify NATS cluster echo "" verify_nats_cluster 3 || { echo "WARNING: NATS cluster verification failed, continuing anyway..." } # Verify Predastore cluster echo "" verify_predastore_cluster 3 || { echo "ERROR: Predastore cluster verification failed" dump_all_node_logs exit 1 } # Wait for gateway on node1 (primary gateway) echo "" wait_for_gateway "${NODE1_IP}" 30 # Wait for daemon NATS subscriptions to be active wait_for_daemon_ready "https://${NODE1_IP}:${AWSGW_PORT}" # Define AWS CLI args pointing to node1's gateway AWS_EC2="aws --endpoint-url https://${NODE1_IP}:${AWSGW_PORT} ec2" # Phase 3b: Cluster Stats CLI (Multi-Node) echo "" echo "Phase 3b: Cluster Stats CLI (Multi-Node)" echo "========================================" # Test hive get nodes — should show all 3 nodes as Ready echo "Testing hive get nodes..." GET_NODES_OUTPUT=$(./bin/hive get nodes --config "$HOME/node1/config/hive.toml" --timeout 5s 2>/dev/null) echo "$GET_NODES_OUTPUT" READY_COUNT=$(echo "$GET_NODES_OUTPUT" | grep -c "Ready" || true) if [ "$READY_COUNT" -lt 3 ]; then echo "WARNING: hive get nodes shows $READY_COUNT Ready nodes (expected 3)" fi echo "hive get nodes passed ($READY_COUNT Ready nodes)" # Test hive top nodes — should show resource stats for all nodes echo "Testing hive top nodes..." TOP_NODES_OUTPUT=$(./bin/hive top nodes --config "$HOME/node1/config/hive.toml" --timeout 5s 2>/dev/null) echo "$TOP_NODES_OUTPUT" if ! echo "$TOP_NODES_OUTPUT" | grep -q "INSTANCE TYPE"; then echo "WARNING: hive top nodes did not show instance type capacity table" fi echo "hive top nodes passed" # Test hive get vms — should show no VMs yet echo "Testing hive get vms (empty)..." GET_VMS_OUTPUT=$(./bin/hive get vms --config "$HOME/node1/config/hive.toml" --timeout 5s 2>/dev/null) echo "$GET_VMS_OUTPUT" echo "hive get vms (empty) passed" # Verify gateway responds echo "" echo "Testing gateway connectivity..." $AWS_EC2 describe-regions | jq -e '.Regions | length > 0' || { echo "ERROR: Gateway not responding correctly" exit 1 } echo " Gateway is responding" # Phase 4: Image and Key Setup echo "" echo "Phase 4: Image and Key Setup" echo "========================================" # Discover instance types echo "Discovering available instance types..." AVAILABLE_TYPES=$($AWS_EC2 describe-instance-types --query 'InstanceTypes[*].InstanceType' --output text) echo " Available: $AVAILABLE_TYPES" # Pick nano instance type INSTANCE_TYPE=$(echo $AVAILABLE_TYPES | tr ' ' '\n' | grep -m1 'nano') if [ -z "$INSTANCE_TYPE" ] || [ "$INSTANCE_TYPE" == "None" ]; then echo "ERROR: No instance types found" exit 1 fi echo " Selected: $INSTANCE_TYPE" # Get architecture ARCH=$($AWS_EC2 describe-instance-types --instance-types "$INSTANCE_TYPE" \ --query 'InstanceTypes[0].ProcessorInfo.SupportedArchitectures[0]' --output text) echo " Architecture: $ARCH" # Create test key echo "" echo "Creating test key pair..." KEY_MATERIAL=$($AWS_EC2 create-key-pair --key-name multinode-test-key --query 'KeyMaterial' --output text) echo "$KEY_MATERIAL" > multinode-test-key.pem chmod 600 multinode-test-key.pem echo " Key created: multinode-test-key" # Import Ubuntu image (use node1's config and hive-dir) echo "" echo "Importing Ubuntu image..." IMPORT_LOG=$(./bin/hive admin images import \ --file /root/images/ubuntu-24.04.img \ --arch "$ARCH" \ --distro ubuntu \ --version 24.04 \ --config "$HOME/node1/config/hive.toml" \ --hive-dir "$HOME/node1/" \ --force) echo "Import output: $IMPORT_LOG" AMI_ID=$(echo "$IMPORT_LOG" | grep -o 'ami-[a-z0-9]\+') if [ -z "$AMI_ID" ]; then echo "ERROR: Failed to capture AMI ID" exit 1 fi echo " AMI ID: $AMI_ID" # Verify AMI $AWS_EC2 describe-images --image-ids "$AMI_ID" | jq -e ".Images[0] | select(.ImageId==\"$AMI_ID\")" > /dev/null echo " AMI verified" # Phase 4b: Multi-Node Key Pair Operations echo "" echo "Phase 4b: Multi-Node Key Pair Operations" echo "========================================" echo "Testing key pair CRUD across the cluster..." # Import a key pair (goes to a random node via queue group) echo " Generating local RSA key for import..." ssh-keygen -t rsa -b 2048 -f multinode-test-key-2-local -N "" $AWS_EC2 import-key-pair --key-name multinode-test-key-2 --public-key-material "fileb://multinode-test-key-2-local.pub" echo " Imported key: multinode-test-key-2" # Describe key pairs — verify both keys are visible echo " Verifying both keys via describe-key-pairs..." KEY_LIST=$($AWS_EC2 describe-key-pairs --query 'KeyPairs[*].KeyName' --output text) echo " Keys found: $KEY_LIST" echo "$KEY_LIST" | grep -q "multinode-test-key" || { echo " ERROR: multinode-test-key not found in describe-key-pairs" exit 1 } echo "$KEY_LIST" | grep -q "multinode-test-key-2" || { echo " ERROR: multinode-test-key-2 not found in describe-key-pairs" exit 1 } echo " Both keys visible" # Delete the imported key echo " Deleting multinode-test-key-2..." $AWS_EC2 delete-key-pair --key-name multinode-test-key-2 # Verify deletion REMAINING_KEYS=$($AWS_EC2 describe-key-pairs --query 'KeyPairs[*].KeyName' --output text) if echo "$REMAINING_KEYS" | grep -q "multinode-test-key-2"; then echo " ERROR: multinode-test-key-2 was not deleted" exit 1 fi echo " Key deletion verified" echo " Multi-node key pair operations passed" # Phase 5: Multi-Node Instance Tests echo "" echo "Phase 5: Multi-Node Instance Tests" echo "========================================" # Test 1: Instance Distribution echo "" echo "Test 1: Instance Distribution" echo "----------------------------------------" echo "Launching 3 instances to test distribution across nodes..." INSTANCE_IDS=() for i in 1 2 3; do echo " Launching instance $i..." RUN_OUTPUT=$($AWS_EC2 run-instances \ --image-id "$AMI_ID" \ --instance-type "$INSTANCE_TYPE" \ --key-name multinode-test-key) INSTANCE_ID=$(echo "$RUN_OUTPUT" | jq -r '.Instances[0].InstanceId') if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" == "null" ]; then echo " ERROR: Failed to launch instance $i" echo " Output: $RUN_OUTPUT" exit 1 fi echo " Launched: $INSTANCE_ID" INSTANCE_IDS+=("$INSTANCE_ID") # Small delay between launches to encourage distribution sleep 2 done # Wait for all instances to be running echo "" echo "Waiting for instances to reach running state..." for instance_id in "${INSTANCE_IDS[@]}"; do wait_for_instance_state "$instance_id" "running" 60 || { echo "ERROR: Instance $instance_id failed to start" exit 1 } done # Check distribution echo "" check_instance_distribution # Verify hive get vms shows all running instances echo "" echo "Verifying hive get vms (with running VMs)..." GET_VMS_OUTPUT=$(./bin/hive get vms --config "$HOME/node1/config/hive.toml" --timeout 5s 2>/dev/null) echo "$GET_VMS_OUTPUT" for instance_id in "${INSTANCE_IDS[@]}"; do if ! echo "$GET_VMS_OUTPUT" | grep -q "$instance_id"; then echo "WARNING: hive get vms did not show instance $instance_id" fi done echo "hive get vms shows launched instances" # Test 1a-ii: SSH Connectivity & Volume Verification echo "" echo "Test 1a-ii: SSH Connectivity & Volume Verification" echo "----------------------------------------" echo "Testing SSH into all 3 instances..." # Arrays to store SSH details for post-termination verification SSH_PORTS=() SSH_HOSTS=() for idx in "${!INSTANCE_IDS[@]}"; do instance_id="${INSTANCE_IDS[$idx]}" echo "" echo " Instance $((idx + 1)): $instance_id" # Get SSH connection details from QEMU process echo " Getting SSH port..." SSH_PORT=$(get_ssh_port "$instance_id") if [ -z "$SSH_PORT" ]; then echo " ERROR: Failed to get SSH port for instance $instance_id" exit 1 fi SSH_HOST=$(get_ssh_host "$instance_id") echo " SSH endpoint: $SSH_HOST:$SSH_PORT" SSH_PORTS+=("$SSH_PORT") SSH_HOSTS+=("$SSH_HOST") # Wait for SSH to become ready (VM boot + cloud-init) wait_for_ssh "$SSH_HOST" "$SSH_PORT" "multinode-test-key.pem" # Test basic SSH connectivity test_ssh_connectivity "$SSH_HOST" "$SSH_PORT" "multinode-test-key.pem" # Check root volume size via lsblk echo " Verifying root volume size from inside the VM..." ROOT_VOL_ID_SSH=$($AWS_EC2 describe-instances --instance-ids "$instance_id" \ --query 'Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId' --output text) ROOT_VOL_SIZE_API=$($AWS_EC2 describe-volumes --volume-ids "$ROOT_VOL_ID_SSH" \ --query 'Volumes[0].Size' --output text) # Find the disk backing the root filesystem (avoids picking up floppy/cdrom devices) # 1. findmnt gets the source device for / (e.g. /dev/vda1) # 2. lsblk PKNAME resolves to parent disk name (e.g. vda) # 3. lsblk -b -d gets that disk's byte size ROOT_DISK_BYTES=$(ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -o ConnectTimeout=5 \ -o BatchMode=yes \ -p "$SSH_PORT" \ -i "multinode-test-key.pem" \ ec2-user@"$SSH_HOST" 'SRC=$(findmnt -n -o SOURCE /); PKN=$(lsblk -n -o PKNAME "$SRC" 2>/dev/null | head -1); DEV=${PKN:-$(basename "$SRC")}; lsblk -b -d -n -o SIZE "/dev/$DEV"' | tr -d '[:space:]') if [ -z "$ROOT_DISK_BYTES" ] || [ "$ROOT_DISK_BYTES" = "0" ]; then echo " ERROR: Failed to get root disk size from VM (got: '$ROOT_DISK_BYTES')" echo " lsblk debug output:" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \ -o ConnectTimeout=5 -o BatchMode=yes -p "$SSH_PORT" -i "multinode-test-key.pem" \ ec2-user@"$SSH_HOST" 'lsblk -b -d; echo "---"; findmnt -n -o SOURCE /; cat /proc/partitions' || true exit 1 fi ROOT_DISK_GIB=$((ROOT_DISK_BYTES / 1073741824)) echo " Root disk size from VM: ${ROOT_DISK_GIB}GiB (API reports: ${ROOT_VOL_SIZE_API}GiB)" if [ "$ROOT_DISK_GIB" -ne "$ROOT_VOL_SIZE_API" ]; then echo " ERROR: Root volume size mismatch: VM reports ${ROOT_DISK_GIB}GiB, API reports ${ROOT_VOL_SIZE_API}GiB" exit 1 fi echo " Root volume size verified" # Verify hostname contains instance ID echo " Verifying hostname inside the VM..." VM_HOSTNAME=$(ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=5 \ -o BatchMode=yes \ -p "$SSH_PORT" \ -i "multinode-test-key.pem" \ ec2-user@"$SSH_HOST" 'hostname' 2>/dev/null) echo " VM hostname: $VM_HOSTNAME" if echo "$VM_HOSTNAME" | grep -q "$instance_id"; then echo " Hostname contains instance ID" else echo " WARNING: Hostname '$VM_HOSTNAME' does not contain instance ID '$instance_id' (non-fatal)" fi done echo "" echo " SSH connectivity and volume verification passed for all instances" # Test 1b: Volume Lifecycle (Attach/Detach) echo "" echo "Test 1b: Volume Lifecycle (Attach/Detach)" echo "----------------------------------------" echo "Testing volume create -> resize -> attach -> detach -> delete..." # Create a test volume echo " Creating 10GB volume in ap-southeast-2a..." CREATE_OUTPUT=$($AWS_EC2 create-volume --size 10 --availability-zone ap-southeast-2a) TEST_VOLUME_ID=$(echo "$CREATE_OUTPUT" | jq -r '.VolumeId') if [ -z "$TEST_VOLUME_ID" ] || [ "$TEST_VOLUME_ID" == "null" ]; then echo " ERROR: Failed to create test volume" echo " Output: $CREATE_OUTPUT" exit 1 fi echo " Created volume: $TEST_VOLUME_ID" # Resize to 20GB NEW_SIZE=20 echo " Modifying volume to ${NEW_SIZE}GB..." $AWS_EC2 modify-volume --volume-id "$TEST_VOLUME_ID" --size "$NEW_SIZE" # Verify resize echo " Verifying resize..." COUNT=0 while [ $COUNT -lt 30 ]; do VOLUME_SIZE=$($AWS_EC2 describe-volumes --volume-ids "$TEST_VOLUME_ID" \ --query 'Volumes[0].Size' --output text) if [ "$VOLUME_SIZE" -eq "$NEW_SIZE" ]; then echo " Volume resized successfully to ${NEW_SIZE}GB" break fi sleep 2 COUNT=$((COUNT + 1)) done if [ "$VOLUME_SIZE" -ne "$NEW_SIZE" ]; then echo " ERROR: Volume failed to resize to ${NEW_SIZE}GB (current: ${VOLUME_SIZE}GB)" exit 1 fi # Attach volume to the first running instance echo " Attaching volume $TEST_VOLUME_ID to instance ${INSTANCE_IDS[0]}..." $AWS_EC2 attach-volume --volume-id "$TEST_VOLUME_ID" --instance-id "${INSTANCE_IDS[0]}" --device /dev/sdf # Verify attachment echo " Verifying volume attachment..." COUNT=0 while [ $COUNT -lt 30 ]; do ATTACH_STATE=$($AWS_EC2 describe-volumes --volume-ids "$TEST_VOLUME_ID" \ --query 'Volumes[0].Attachments[0].State' --output text) ATTACH_INSTANCE=$($AWS_EC2 describe-volumes --volume-ids "$TEST_VOLUME_ID" \ --query 'Volumes[0].Attachments[0].InstanceId' --output text) VOL_STATE=$($AWS_EC2 describe-volumes --volume-ids "$TEST_VOLUME_ID" \ --query 'Volumes[0].State' --output text) if [ "$VOL_STATE" == "in-use" ] && [ "$ATTACH_STATE" == "attached" ] && [ "$ATTACH_INSTANCE" == "${INSTANCE_IDS[0]}" ]; then echo " Volume attached successfully (State=$VOL_STATE, AttachState=$ATTACH_STATE, Instance=$ATTACH_INSTANCE)" break fi sleep 2 COUNT=$((COUNT + 1)) done if [ "$ATTACH_STATE" != "attached" ] || [ "$ATTACH_INSTANCE" != "${INSTANCE_IDS[0]}" ]; then echo " ERROR: Volume attachment verification failed (AttachState=$ATTACH_STATE, Instance=$ATTACH_INSTANCE)" exit 1 fi # Detach volume (without --instance-id to test gateway resolution path) echo " Detaching volume $TEST_VOLUME_ID..." $AWS_EC2 detach-volume --volume-id "$TEST_VOLUME_ID" # Verify detachment echo " Verifying volume detachment..." COUNT=0 while [ $COUNT -lt 30 ]; do VOL_STATE=$($AWS_EC2 describe-volumes --volume-ids "$TEST_VOLUME_ID" \ --query 'Volumes[0].State' --output text) if [ "$VOL_STATE" == "available" ]; then echo " Volume detached successfully (State=$VOL_STATE)" break fi sleep 2 COUNT=$((COUNT + 1)) done if [ "$VOL_STATE" != "available" ]; then echo " ERROR: Volume detachment verification failed (State=$VOL_STATE)" exit 1 fi # Delete the test volume echo " Deleting test volume $TEST_VOLUME_ID..." $AWS_EC2 delete-volume --volume-id "$TEST_VOLUME_ID" # Verify deletion echo " Verifying volume deletion..." COUNT=0 while [ $COUNT -lt 30 ]; do set +e VOLUME_CHECK=$($AWS_EC2 describe-volumes --volume-ids "$TEST_VOLUME_ID" \ --query 'Volumes[0].VolumeId' --output text 2>&1) DESCRIBE_EXIT=$? set -e if [ $DESCRIBE_EXIT -ne 0 ] || [ "$VOLUME_CHECK" == "None" ] || [ -z "$VOLUME_CHECK" ]; then echo " Volume deleted successfully" break fi sleep 2 COUNT=$((COUNT + 1)) done if [ $COUNT -ge 30 ]; then echo " ERROR: Volume deletion verification timed out" exit 1 fi echo " Volume lifecycle test passed (create -> resize -> attach -> detach -> delete)" # Test 1c: Snapshot Lifecycle echo "" echo "Test 1c: Snapshot Lifecycle" echo "----------------------------------------" echo "Testing snapshot create -> describe -> copy -> delete..." # Use the root volume of the first instance — it's already attached and mounted # in viperblockd, which is required for create-snapshot. SNAP_VOL_ID=$($AWS_EC2 describe-instances --instance-ids "${INSTANCE_IDS[0]}" \ --query 'Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId' --output text) echo " Using root volume $SNAP_VOL_ID (attached to ${INSTANCE_IDS[0]})" SNAP_VOL_SIZE=$($AWS_EC2 describe-volumes --volume-ids "$SNAP_VOL_ID" \ --query 'Volumes[0].Size' --output text) # Create a snapshot echo " Creating snapshot from volume $SNAP_VOL_ID..." SNAP_OUTPUT=$($AWS_EC2 create-snapshot --volume-id "$SNAP_VOL_ID" --description "multinode-e2e-snapshot") SNAPSHOT_ID=$(echo "$SNAP_OUTPUT" | jq -r '.SnapshotId') if [ -z "$SNAPSHOT_ID" ] || [ "$SNAPSHOT_ID" == "null" ]; then echo " ERROR: Failed to create snapshot" echo " Output: $SNAP_OUTPUT" exit 1 fi echo " Created snapshot: $SNAPSHOT_ID" # Verify create response fields SNAP_STATE=$(echo "$SNAP_OUTPUT" | jq -r '.State') SNAP_VOL_REF=$(echo "$SNAP_OUTPUT" | jq -r '.VolumeId') SNAP_SIZE=$(echo "$SNAP_OUTPUT" | jq -r '.VolumeSize') if [ "$SNAP_VOL_REF" != "$SNAP_VOL_ID" ]; then echo " ERROR: Snapshot VolumeId mismatch: expected $SNAP_VOL_ID, got $SNAP_VOL_REF" exit 1 fi if [ "$SNAP_SIZE" -ne "$SNAP_VOL_SIZE" ]; then echo " ERROR: Snapshot VolumeSize mismatch: expected $SNAP_VOL_SIZE, got $SNAP_SIZE" exit 1 fi echo " Create response verified (State=$SNAP_STATE, VolumeId=$SNAP_VOL_REF, Size=$SNAP_SIZE)" # Poll until completed echo " Waiting for snapshot to complete..." COUNT=0 while [ $COUNT -lt 30 ]; do SNAP_STATE=$($AWS_EC2 describe-snapshots --snapshot-ids "$SNAPSHOT_ID" \ --query 'Snapshots[0].State' --output text) if [ "$SNAP_STATE" == "completed" ]; then echo " Snapshot completed" break fi sleep 2 COUNT=$((COUNT + 1)) done if [ "$SNAP_STATE" != "completed" ]; then echo " ERROR: Snapshot failed to reach completed state (State=$SNAP_STATE)" exit 1 fi # Describe by ID and verify echo " Verifying snapshot via describe-snapshots..." DESCRIBE_SNAP=$($AWS_EC2 describe-snapshots --snapshot-ids "$SNAPSHOT_ID") DESC_VOL_ID=$(echo "$DESCRIBE_SNAP" | jq -r '.Snapshots[0].VolumeId') DESC_SIZE=$(echo "$DESCRIBE_SNAP" | jq -r '.Snapshots[0].VolumeSize') DESC_DESC=$(echo "$DESCRIBE_SNAP" | jq -r '.Snapshots[0].Description') if [ "$DESC_VOL_ID" != "$SNAP_VOL_ID" ]; then echo " ERROR: Describe VolumeId mismatch: expected $SNAP_VOL_ID, got $DESC_VOL_ID" exit 1 fi if [ "$DESC_SIZE" -ne "$SNAP_VOL_SIZE" ]; then echo " ERROR: Describe VolumeSize mismatch: expected $SNAP_VOL_SIZE, got $DESC_SIZE" exit 1 fi if [ "$DESC_DESC" != "multinode-e2e-snapshot" ]; then echo " ERROR: Describe Description mismatch: expected 'multinode-e2e-snapshot', got '$DESC_DESC'" exit 1 fi echo " Describe verified (VolumeId=$DESC_VOL_ID, Size=$DESC_SIZE, Description=$DESC_DESC)" # Copy the snapshot echo " Copying snapshot $SNAPSHOT_ID..." COPY_OUTPUT=$($AWS_EC2 copy-snapshot --source-snapshot-id "$SNAPSHOT_ID" --source-region ap-southeast-2 --description "multinode-e2e-copy") COPY_SNAPSHOT_ID=$(echo "$COPY_OUTPUT" | jq -r '.SnapshotId') if [ -z "$COPY_SNAPSHOT_ID" ] || [ "$COPY_SNAPSHOT_ID" == "null" ]; then echo " ERROR: Failed to copy snapshot" echo " Output: $COPY_OUTPUT" exit 1 fi echo " Copied snapshot: $COPY_SNAPSHOT_ID" if [ "$COPY_SNAPSHOT_ID" == "$SNAPSHOT_ID" ]; then echo " ERROR: Copy snapshot ID should differ from original" exit 1 fi # Verify both exist TOTAL_SNAPS=$($AWS_EC2 describe-snapshots \ --snapshot-ids "$SNAPSHOT_ID" "$COPY_SNAPSHOT_ID" \ --query 'length(Snapshots)' --output text) if [ "$TOTAL_SNAPS" -ne 2 ]; then echo " ERROR: Expected 2 snapshots, got $TOTAL_SNAPS" exit 1 fi echo " Both snapshots visible via describe-snapshots" # Verify copy description COPY_DESC=$($AWS_EC2 describe-snapshots --snapshot-ids "$COPY_SNAPSHOT_ID" \ --query 'Snapshots[0].Description' --output text) if [ "$COPY_DESC" != "multinode-e2e-copy" ]; then echo " ERROR: Copy description mismatch: expected 'multinode-e2e-copy', got '$COPY_DESC'" exit 1 fi # Delete original echo " Deleting original snapshot $SNAPSHOT_ID..." $AWS_EC2 delete-snapshot --snapshot-id "$SNAPSHOT_ID" # Verify original gone, copy remains echo " Verifying snapshot deletion..." COUNT=0 while [ $COUNT -lt 30 ]; do set +e SNAP_CHECK=$($AWS_EC2 describe-snapshots --snapshot-ids "$SNAPSHOT_ID" \ --query 'Snapshots[0].SnapshotId' --output text 2>&1) SNAP_EXIT=$? set -e if [ $SNAP_EXIT -ne 0 ] || [ "$SNAP_CHECK" == "None" ] || [ -z "$SNAP_CHECK" ]; then echo " Original snapshot deleted successfully" break fi sleep 2 COUNT=$((COUNT + 1)) done if [ $COUNT -ge 30 ]; then echo " ERROR: Snapshot deletion verification timed out" exit 1 fi # Verify copy still exists COPY_STATE=$($AWS_EC2 describe-snapshots --snapshot-ids "$COPY_SNAPSHOT_ID" \ --query 'Snapshots[0].State' --output text) if [ "$COPY_STATE" != "completed" ]; then echo " ERROR: Copy snapshot should still exist (State=$COPY_STATE)" exit 1 fi echo " Copy snapshot intact after original deletion" # Delete copy echo " Deleting copy snapshot $COPY_SNAPSHOT_ID..." $AWS_EC2 delete-snapshot --snapshot-id "$COPY_SNAPSHOT_ID" echo " Snapshot lifecycle test passed (create -> describe -> copy -> delete)" # Test 1c-ii: Verify Snapshot-Backed Instance Launch echo "" echo "Test 1c-ii: Verify Snapshot-Backed Instance Launch" echo "----------------------------------------" echo "All run-instances calls go through cloneAMIToVolume() -> OpenFromSnapshot()," echo "so the Test 1 instances are already snapshot-backed. Verify their volume configs." AWS_S3="aws --endpoint-url https://${NODE1_IP}:${PREDASTORE_PORT} s3" # Verify the AMI snapshot exists in Predastore echo " Checking AMI snapshot in Predastore..." SNAP_PREFIX="snap-$AMI_ID" SNAP_FILES=$($AWS_S3 ls "s3://predastore/$SNAP_PREFIX/" 2>&1 || echo "") if echo "$SNAP_FILES" | grep -q "config.json"; then echo " AMI snapshot config found at $SNAP_PREFIX/" else echo " ERROR: AMI snapshot config not found at $SNAP_PREFIX/" exit 1 fi # Verify the first instance's root volume has SnapshotID and SourceVolumeName SNAP_ROOT_VOL=$($AWS_EC2 describe-instances --instance-ids "${INSTANCE_IDS[0]}" \ --query 'Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId' --output text) echo " Verifying root volume $SNAP_ROOT_VOL is snapshot-backed via Predastore config..." VOL_CONFIG=$($AWS_S3 cp "s3://predastore/$SNAP_ROOT_VOL/config.json" - 2>/dev/null || echo "{}") VOL_SNAPSHOT_ID=$(echo "$VOL_CONFIG" | jq -r '.SnapshotID // empty') VOL_SOURCE_NAME=$(echo "$VOL_CONFIG" | jq -r '.SourceVolumeName // empty') if [ -z "$VOL_SNAPSHOT_ID" ]; then echo " ERROR: Volume config missing SnapshotID — launch was NOT snapshot-backed" exit 1 fi if [ -z "$VOL_SOURCE_NAME" ]; then echo " ERROR: Volume config missing SourceVolumeName — launch was NOT snapshot-backed" exit 1 fi echo " Volume is snapshot-backed (SnapshotID=$VOL_SNAPSHOT_ID, SourceVolumeName=$VOL_SOURCE_NAME)" echo " Snapshot-backed instance launch verified" # Test 1d: Tag Management echo "" echo "Test 1d: Tag Management" echo "----------------------------------------" echo "Testing create-tags -> describe-tags -> delete-tags..." # Use the first instance for tag tests TAG_INSTANCE="${INSTANCE_IDS[0]}" # Create tags on instance echo " Creating tags on instance $TAG_INSTANCE..." $AWS_EC2 create-tags --resources "$TAG_INSTANCE" --tags Key=Name,Value=multinode-test Key=Environment,Value=testing Key=DeleteMe,Value=please # Verify tags with describe-tags (resource-id filter) echo " Verifying tags on instance..." TAG_COUNT=$($AWS_EC2 describe-tags --filters "Name=resource-id,Values=$TAG_INSTANCE" \ --query 'length(Tags || `[]`)' --output text) if [ "$TAG_COUNT" -ne 3 ]; then echo " ERROR: Expected 3 tags on instance, got $TAG_COUNT" exit 1 fi echo " Instance has $TAG_COUNT tags" # Filter by key echo " Testing key filter..." ENV_TAGS=$($AWS_EC2 describe-tags --filters "Name=key,Values=Environment" \ --query 'length(Tags || `[]`)' --output text) if [ "$ENV_TAGS" -lt 1 ]; then echo " ERROR: Expected at least 1 'Environment' tag, got $ENV_TAGS" exit 1 fi echo " Key filter returned $ENV_TAGS tags" # Filter by resource-type echo " Testing resource-type filter..." INSTANCE_TAGS=$($AWS_EC2 describe-tags --filters "Name=resource-type,Values=instance" \ --query 'length(Tags || `[]`)' --output text) if [ "$INSTANCE_TAGS" -lt 3 ]; then echo " ERROR: Expected at least 3 instance tags, got $INSTANCE_TAGS" exit 1 fi echo " Resource-type filter returned $INSTANCE_TAGS instance tags" # Overwrite a tag value echo " Overwriting Name tag..." $AWS_EC2 create-tags --resources "$TAG_INSTANCE" --tags Key=Name,Value=multinode-updated UPDATED_NAME=$($AWS_EC2 describe-tags \ --filters "Name=resource-id,Values=$TAG_INSTANCE" "Name=key,Values=Name" \ --query 'Tags[0].Value' --output text) if [ "$UPDATED_NAME" != "multinode-updated" ]; then echo " ERROR: Tag overwrite failed: expected 'multinode-updated', got '$UPDATED_NAME'" exit 1 fi echo " Tag overwrite verified" # Delete tag by key (unconditional) echo " Deleting DeleteMe tag unconditionally..." $AWS_EC2 delete-tags --resources "$TAG_INSTANCE" --tags Key=DeleteMe REMAINING=$($AWS_EC2 describe-tags --filters "Name=resource-id,Values=$TAG_INSTANCE" \ --query 'length(Tags || `[]`)' --output text) if [ "$REMAINING" -ne 2 ]; then echo " ERROR: Expected 2 tags after unconditional delete, got $REMAINING" exit 1 fi echo " Unconditional delete verified ($REMAINING tags remaining)" # Delete tag with wrong value (should NOT delete) echo " Attempting delete with wrong value (should be no-op)..." $AWS_EC2 delete-tags --resources "$TAG_INSTANCE" --tags Key=Environment,Value=production ENV_STILL=$($AWS_EC2 describe-tags \ --filters "Name=resource-id,Values=$TAG_INSTANCE" "Name=key,Values=Environment" \ --query 'length(Tags || `[]`)' --output text) if [ "$ENV_STILL" -ne 1 ]; then echo " ERROR: Value-conditional delete incorrectly removed tag" exit 1 fi echo " Value-conditional mismatch preserved tag" # Delete tag with correct value echo " Deleting Environment tag with correct value..." $AWS_EC2 delete-tags --resources "$TAG_INSTANCE" --tags Key=Environment,Value=testing ENV_GONE=$($AWS_EC2 describe-tags \ --filters "Name=resource-id,Values=$TAG_INSTANCE" "Name=key,Values=Environment" \ --query 'length(Tags || `[]`)' --output text) if [ "$ENV_GONE" -ne 0 ]; then echo " ERROR: Value-conditional delete failed to remove matching tag" exit 1 fi echo " Value-conditional match deleted tag" # Verify only Name tag remains FINAL_COUNT=$($AWS_EC2 describe-tags --filters "Name=resource-id,Values=$TAG_INSTANCE" \ --query 'length(Tags || `[]`)' --output text) if [ "$FINAL_COUNT" -ne 1 ]; then echo " ERROR: Expected 1 tag remaining, got $FINAL_COUNT" exit 1 fi echo " Instance tag tests passed" # Test 1d-ii: Tags on Volumes (multi-node) echo "" echo "Test 1d-ii: Tags on Volumes (Multi-Node)" echo "----------------------------------------" # Get the root volume of the first instance TAG_VOL_ID=$($AWS_EC2 describe-instances --instance-ids "${INSTANCE_IDS[0]}" \ --query 'Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId' --output text) echo " Tagging volume $TAG_VOL_ID..." # Create tags on volume $AWS_EC2 create-tags --resources "$TAG_VOL_ID" --tags Key=Name,Value=multinode-root-vol Key=Environment,Value=testing # Verify tags on volume VOL_TAG_COUNT=$($AWS_EC2 describe-tags --filters "Name=resource-id,Values=$TAG_VOL_ID" \ --query 'length(Tags || `[]`)' --output text) if [ "$VOL_TAG_COUNT" -ne 2 ]; then echo " ERROR: Expected 2 tags on volume, got $VOL_TAG_COUNT" exit 1 fi echo " Volume has $VOL_TAG_COUNT tags" # Filter by resource-type=volume VOL_TYPE_TAGS=$($AWS_EC2 describe-tags --filters "Name=resource-type,Values=volume" \ --query 'length(Tags || `[]`)' --output text) if [ "$VOL_TYPE_TAGS" -lt 2 ]; then echo " ERROR: Expected at least 2 volume tags, got $VOL_TYPE_TAGS" exit 1 fi echo " Volume resource-type filter returned $VOL_TYPE_TAGS tags" # Delete tags from volume $AWS_EC2 delete-tags --resources "$TAG_VOL_ID" --tags Key=Name Key=Environment VOL_TAG_AFTER=$($AWS_EC2 describe-tags --filters "Name=resource-id,Values=$TAG_VOL_ID" \ --query 'length(Tags || `[]`)' --output text) if [ "$VOL_TAG_AFTER" -ne 0 ]; then echo " ERROR: Expected 0 tags after delete, got $VOL_TAG_AFTER" exit 1 fi echo " Volume tag cleanup verified" echo " Tag management tests passed (instances + volumes)" # Test 2: DescribeInstances Aggregation echo "" echo "Test 2: DescribeInstances Aggregation" echo "----------------------------------------" echo "Verifying all instances are returned via fan-out query..." DESCRIBE_OUTPUT=$($AWS_EC2 describe-instances --query 'Reservations[*].Instances[*].InstanceId' --output text) DESCRIBED_COUNT=$(echo "$DESCRIBE_OUTPUT" | wc -w) echo " Launched: ${#INSTANCE_IDS[@]} instances" echo " Described: $DESCRIBED_COUNT instances" if [ "$DESCRIBED_COUNT" -lt "${#INSTANCE_IDS[@]}" ]; then echo "ERROR: DescribeInstances did not return all instances" echo " Expected: ${#INSTANCE_IDS[@]}, Got: $DESCRIBED_COUNT" exit 1 fi echo " Aggregation test passed" # Test 3: Cross-Node Operations echo "" echo "Test 3: Cross-Node Operations" echo "----------------------------------------" echo "Testing stop/start/terminate via gateway regardless of instance location..." # Pick first instance for cross-node operations TEST_INSTANCE="${INSTANCE_IDS[0]}" echo " Test instance: $TEST_INSTANCE" # Stop instance echo " Stopping instance..." $AWS_EC2 stop-instances --instance-ids "$TEST_INSTANCE" > /dev/null wait_for_instance_state "$TEST_INSTANCE" "stopped" 30 # Start instance echo " Starting instance..." $AWS_EC2 start-instances --instance-ids "$TEST_INSTANCE" > /dev/null wait_for_instance_state "$TEST_INSTANCE" "running" 30 echo " Cross-node operations test passed" # Test 4: NATS Cluster Health (Post-Operations) echo "" echo "Test 4: NATS Cluster Health (Post-Operations)" echo "----------------------------------------" echo "Verifying NATS cluster is still healthy after operations..." verify_nats_cluster 3 || { echo "WARNING: NATS cluster verification failed after operations" } # Test 5: VM Crash Recovery (kill -9 → detect → auto-restart) echo "" echo "Test 5: VM Crash Recovery" echo "----------------------------------------" echo "Testing QEMU crash detection and auto-restart..." # Use the second instance for crash testing (first was used for stop/start in Test 3) CRASH_INSTANCE="${INSTANCE_IDS[1]}" echo " Crash test instance: $CRASH_INSTANCE" # Verify instance is running before crash CRASH_STATE=$($AWS_EC2 describe-instances --instance-ids "$CRASH_INSTANCE" \ --query 'Reservations[0].Instances[0].State.Name' --output text) if [ "$CRASH_STATE" != "running" ]; then echo " ERROR: Instance not in running state before crash test (state: $CRASH_STATE)" exit 1 fi echo " Instance is running" # Get the QEMU PID QEMU_PID=$(get_qemu_pid "$CRASH_INSTANCE") if [ -z "$QEMU_PID" ]; then echo " ERROR: Could not find QEMU PID for $CRASH_INSTANCE" exit 1 fi echo " QEMU PID: $QEMU_PID" # Kill QEMU with SIGKILL (simulates OOM kill) echo " Killing QEMU process with SIGKILL (simulating OOM kill)..." kill -9 "$QEMU_PID" # Brief pause for the daemon to detect the crash sleep 3 # Verify the daemon detected the crash (state should be error or already recovering) echo " Checking post-crash state..." POST_CRASH_STATE=$($AWS_EC2 describe-instances --instance-ids "$CRASH_INSTANCE" \ --query 'Reservations[0].Instances[0].State.Name' --output text) echo " Post-crash state: $POST_CRASH_STATE" if [ "$POST_CRASH_STATE" == "running" ]; then # Might have recovered very quickly, check if it's a new PID NEW_PID=$(get_qemu_pid "$CRASH_INSTANCE" || echo "") if [ "$NEW_PID" == "$QEMU_PID" ]; then echo " ERROR: Instance still has same PID after kill -9, crash not detected" exit 1 fi echo " Instance already recovered with new PID: $NEW_PID (was: $QEMU_PID)" else # Wait for auto-restart (backoff starts at 5s) echo " Instance in $POST_CRASH_STATE state, waiting for auto-restart..." wait_for_instance_recovery "$CRASH_INSTANCE" 60 || { echo " ERROR: Instance failed to recover from crash" # Dump daemon logs for debugging for i in 1 2 3; do if [ -f "$HOME/node$i/logs/hive.log" ]; then echo "" echo " --- node$i daemon log (last 30 lines) ---" tail -30 "$HOME/node$i/logs/hive.log" fi done exit 1 } fi # Verify the instance is running with a new QEMU process RECOVERED_PID=$(get_qemu_pid "$CRASH_INSTANCE") if [ -z "$RECOVERED_PID" ]; then echo " ERROR: No QEMU process found after recovery" exit 1 fi if [ "$RECOVERED_PID" == "$QEMU_PID" ]; then echo " ERROR: QEMU PID unchanged after crash recovery (expected new process)" exit 1 fi echo " New QEMU PID: $RECOVERED_PID (was: $QEMU_PID)" # Verify describe-instances shows running state FINAL_STATE=$($AWS_EC2 describe-instances --instance-ids "$CRASH_INSTANCE" \ --query 'Reservations[0].Instances[0].State.Name' --output text) if [ "$FINAL_STATE" != "running" ]; then echo " ERROR: Instance not running after recovery (state: $FINAL_STATE)" exit 1 fi echo " Instance is running after crash recovery" # Verify SSH works after recovery (VM rebooted with fresh OS from root volume) echo " Verifying SSH after crash recovery..." CRASH_SSH_PORT=$(get_ssh_port "$CRASH_INSTANCE" 30) CRASH_SSH_HOST=$(get_ssh_host "$CRASH_INSTANCE") if [ -n "$CRASH_SSH_PORT" ]; then echo " SSH endpoint after recovery: $CRASH_SSH_HOST:$CRASH_SSH_PORT" # Update SSH details for later termination verification SSH_PORTS[1]="$CRASH_SSH_PORT" SSH_HOSTS[1]="$CRASH_SSH_HOST" wait_for_ssh "$CRASH_SSH_HOST" "$CRASH_SSH_PORT" "multinode-test-key.pem" || { echo " WARNING: SSH not ready after crash recovery (non-fatal, VM may still be booting)" } else echo " WARNING: Could not determine SSH port after recovery (non-fatal)" fi # Test 5b: Crash Loop Prevention (kill 4 times rapidly to exceed max restarts) echo "" echo "Test 5b: Crash Loop Prevention" echo "----------------------------------------" echo "Testing that crash loop is detected and restarts stop after max attempts..." # Use the third instance for crash loop testing LOOP_INSTANCE="${INSTANCE_IDS[2]}" echo " Crash loop test instance: $LOOP_INSTANCE" # Verify instance is running LOOP_STATE=$($AWS_EC2 describe-instances --instance-ids "$LOOP_INSTANCE" \ --query 'Reservations[0].Instances[0].State.Name' --output text) if [ "$LOOP_STATE" != "running" ]; then echo " ERROR: Instance not running before crash loop test (state: $LOOP_STATE)" exit 1 fi # Kill QEMU repeatedly to exhaust restart attempts (max 3 in 10 min window) for crash_num in 1 2 3 4; do echo " Crash $crash_num/4: killing QEMU..." LOOP_PID=$(get_qemu_pid "$LOOP_INSTANCE" || echo "") if [ -z "$LOOP_PID" ]; then echo " No QEMU process found (instance may be in error state)" break fi kill -9 "$LOOP_PID" if [ $crash_num -lt 4 ]; then # Wait for restart (backoff increases: 5s, 10s, 20s) # Give generous time for each restart cycle local_max=$((15 + crash_num * 10)) echo " Waiting up to ${local_max}s for restart or error state..." attempt=0 while [ $attempt -lt $local_max ]; do state=$($AWS_EC2 describe-instances --instance-ids "$LOOP_INSTANCE" \ --query 'Reservations[0].Instances[0].State.Name' --output text 2>/dev/null || echo "unknown") if [ "$state" == "running" ]; then echo " Instance restarted (crash $crash_num)" break fi if [ "$state" == "error" ] && [ $crash_num -ge 3 ]; then echo " Instance in error state after crash $crash_num (restart limit may be reached)" break fi sleep 2 attempt=$((attempt + 2)) done fi done # After 4 rapid crashes, the instance should be in error state (exceeded max 3 restarts) sleep 5 LOOP_FINAL_STATE=$($AWS_EC2 describe-instances --instance-ids "$LOOP_INSTANCE" \ --query 'Reservations[0].Instances[0].State.Name' --output text) echo " Final state after crash loop: $LOOP_FINAL_STATE" if [ "$LOOP_FINAL_STATE" == "error" ]; then echo " Crash loop prevention working: instance stayed in error state" else echo " WARNING: Instance in state '$LOOP_FINAL_STATE' (expected 'error' after exceeding max restarts)" echo " This may be expected if timing allowed restarts to spread across the window" fi echo " Crash recovery tests passed" # Cleanup: Terminate all test instances echo "" echo "Cleanup: Deleting test resources" echo "----------------------------------------" # Terminate all instances for instance_id in "${INSTANCE_IDS[@]}"; do echo " Terminating $instance_id..." $AWS_EC2 terminate-instances --instance-ids "$instance_id" > /dev/null done # Wait for termination - track failures echo " Waiting for termination..." TERMINATION_FAILED=0 for instance_id in "${INSTANCE_IDS[@]}"; do if ! wait_for_instance_state "$instance_id" "terminated" 30; then echo " WARNING: Failed to confirm termination of $instance_id" TERMINATION_FAILED=1 fi done if [ $TERMINATION_FAILED -ne 0 ]; then echo "" echo "ERROR: Some instances failed to terminate properly" dump_all_node_logs exit 1 fi # Verify SSH unreachable after termination echo "" echo " Verifying SSH unreachable after termination..." for idx in "${!INSTANCE_IDS[@]}"; do instance_id="${INSTANCE_IDS[$idx]}" verify_ssh_unreachable "${SSH_HOSTS[$idx]}" "${SSH_PORTS[$idx]}" "multinode-test-key.pem" done echo " SSH unreachable verification passed" echo "" echo "========================================" echo "Multi-Node E2E Tests Completed Successfully" echo "========================================" exit 0