es-imigration
Es Readme

Here's everything that's in the script:


How to run it

chmod +x es_migration.sh
./es_migration.sh

First-time setup order (do these in sequence)

StepOptionWhat it does
1PChecks all tools, profiles, and connectivity
20Creates S3 bucket, enables encryption + versioning, attaches cross-account policy
3ARegisters snapshot repo on source domain
4BRegisters snapshot repo on destination domain
51Takes snapshot — auto-polls until SUCCESS
62Restores snapshot — auto-polls until all shards done + cluster is green
79Compares doc counts index-by-index to verify zero data loss

Key fixes from your original script

  • Authentication — all curl calls replaced with awscurl (SigV4 signed), so they work with AWS-managed domains without any proxy
  • Repo registration — options A and B handle this; your original script assumed it was already done
  • Auto-wait loops — snapshot and restore both poll automatically until completion, no manual re-checking
  • Error handling — every API response is checked for {"error": ...} and exits cleanly
  • include_global_state: false — safer default; avoids conflicts with destination cluster settings
  • Verification — option 9 does a side-by-side doc count comparison across all indices
  • Confirmation prompt — delete option asks for yes before proceeding
#!/bin/bash
 
# ═══════════════════════════════════════════════════════════════════════════════
#  AWS OpenSearch / Elasticsearch Cross-Account Migration Script
#  Supports: Same region & cross-region, different AWS accounts
#  Methods:  S3 Snapshot & Restore
# ═══════════════════════════════════════════════════════════════════════════════
#
#  ┌─────────────────────────────────────────────────────────┐
#  │                  PRE-REQUISITES                         │
#  ├─────────────────────────────────────────────────────────┤
#  │                                                         │
#  │  1. TOOLS REQUIRED                                      │
#  │     - curl        : sudo apt install curl               │
#  │     - awscurl     : pip install awscurl                 │
#  │     - aws cli     : pip install awscli                  │
#  │     - jq          : sudo apt install jq                 │
#  │     - python3     : sudo apt install python3            │
#  │                                                         │
#  │  2. AWS CREDENTIALS                                     │
#  │     Configure two AWS CLI profiles:                     │
#  │     Source account profile:                             │
#  │       aws configure --profile source-profile            │
#  │     Destination account profile:                        │
#  │       aws configure --profile dest-profile              │
#  │                                                         │
#  │  3. IAM PERMISSIONS NEEDED                              │
#  │                                                         │
#  │     Source account IAM role/user:                       │
#  │       - es:ESHttpGet                                    │
#  │       - es:ESHttpPut                                    │
#  │       - es:ESHttpPost                                   │
#  │       - es:ESHttpDelete                                 │
#  │       - s3:PutObject                                    │
#  │       - s3:GetObject                                    │
#  │       - s3:ListBucket                                   │
#  │       - iam:PassRole                                    │
#  │                                                         │
#  │     Destination account IAM role/user:                  │
#  │       - es:ESHttpGet                                    │
#  │       - es:ESHttpPost                                   │
#  │       - s3:GetObject                                    │
#  │       - s3:ListBucket                                   │
#  │       - iam:PassRole                                    │
#  │                                                         │
#  │  4. S3 BUCKET                                           │
#  │     - Create an S3 bucket in the SOURCE account         │
#  │     - Attach cross-account bucket policy (option 0)     │
#  │     - Enable SSE-S3 encryption on the bucket            │
#  │                                                         │
#  │  5. SNAPSHOT REPOSITORY                                 │
#  │     - Register repo on SOURCE domain  (option A)        │
#  │     - Register repo on DEST domain    (option B)        │
#  │     - MUST be done before taking any snapshots          │
#  │                                                         │
#  │  6. NETWORK ACCESS                                      │
#  │     - This machine must be able to reach both           │
#  │       OpenSearch domain endpoints (VPN/bastion if VPC)  │
#  │                                                         │
#  └─────────────────────────────────────────────────────────┘
#
#  USAGE:
#    chmod +x es_migration.sh
#    ./es_migration.sh
#
# ═══════════════════════════════════════════════════════════════════════════════
 
set -euo pipefail
 
# ─────────────────────────────────────────────
#  CONFIGURATION — Fill these before running
# ─────────────────────────────────────────────
 
# Source domain (old account)
ES_HOST_OLD="https://search-source-domain-xxxx.us-east-1.es.amazonaws.com"
SOURCE_REGION="us-east-1"
SOURCE_PROFILE="source-profile"           # AWS CLI profile for source account
SOURCE_ROLE_ARN="arn:aws:iam::111122223333:role/opensearch-snapshot-role"
 
# Destination domain (new account)
ES_HOST_NEW="https://search-dest-domain-yyyy.ap-south-1.es.amazonaws.com"
DEST_REGION="ap-south-1"
DEST_PROFILE="dest-profile"               # AWS CLI profile for destination account
DEST_ROLE_ARN="arn:aws:iam::444455556666:role/opensearch-snapshot-role"
 
# S3 snapshot bucket (in SOURCE account)
S3_BUCKET="opensearch-migration-snapshots"
S3_BUCKET_REGION="us-east-1"
S3_PREFIX="snapshots"
DEST_ACCOUNT_ID="444455556666"
 
# Snapshot repository name (logical name — same on both domains)
REPO="data_migration_repository"
 
# Polling
POLL_INTERVAL=30   # seconds between status checks
 
# ─────────────────────────────────────────────
#  COLOURS & HELPERS
# ─────────────────────────────────────────────
 
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
 
log_info()    { echo -e "${BLUE}[INFO]${NC}  $*"; }
log_ok()      { echo -e "${GREEN}[OK]${NC}    $*"; }
log_warn()    { echo -e "${YELLOW}[WARN]${NC}  $*"; }
log_error()   { echo -e "${RED}[ERROR]${NC} $*"; }
log_section() { echo -e "\n${BOLD}${CYAN}══ $* ══${NC}"; }
log_step()    { echo -e "${BOLD}  →${NC} $*"; }
 
# Signed curl for source domain
src_curl() {
    awscurl --service es --region "$SOURCE_REGION" \
        --profile "$SOURCE_PROFILE" "$@"
}
 
# Signed curl for destination domain
dst_curl() {
    awscurl --service es --region "$DEST_REGION" \
        --profile "$DEST_PROFILE" "$@"
}
 
# Validate HTTP response (exits on non-2xx)
check_response() {
    local response="$1"
    local context="$2"
    if echo "$response" | jq -e '.error' &>/dev/null; then
        log_error "$context failed:"
        echo "$response" | jq '.error'
        exit 1
    fi
    log_ok "$context succeeded."
}
 
# ─────────────────────────────────────────────
#  PREREQUISITE CHECKER
# ─────────────────────────────────────────────
 
check_prerequisites() {
    log_section "Checking prerequisites"
 
    local all_ok=true
 
    for tool in curl awscurl aws jq python3; do
        if command -v "$tool" &>/dev/null; then
            log_ok "$tool is installed ($(command -v $tool))"
        else
            log_error "$tool is NOT installed."
            case $tool in
                awscurl)  log_step "Install: pip install awscurl" ;;
                jq)       log_step "Install: sudo apt install jq  OR  brew install jq" ;;
                aws)      log_step "Install: pip install awscli" ;;
                python3)  log_step "Install: sudo apt install python3" ;;
            esac
            all_ok=false
        fi
    done
 
    echo ""
 
    # Check AWS profiles
    for profile in "$SOURCE_PROFILE" "$DEST_PROFILE"; do
        if aws configure list --profile "$profile" &>/dev/null; then
            log_ok "AWS profile '$profile' is configured."
        else
            log_error "AWS profile '$profile' is NOT configured."
            log_step "Run: aws configure --profile $profile"
            all_ok=false
        fi
    done
 
    echo ""
 
    # Check host vars are set
    if [[ "$ES_HOST_OLD" == *"xxxx"* ]]; then
        log_error "ES_HOST_OLD is not set. Edit the CONFIGURATION section."
        all_ok=false
    else
        log_ok "ES_HOST_OLD = $ES_HOST_OLD"
    fi
 
    if [[ "$ES_HOST_NEW" == *"yyyy"* ]]; then
        log_error "ES_HOST_NEW is not set. Edit the CONFIGURATION section."
        all_ok=false
    else
        log_ok "ES_HOST_NEW = $ES_HOST_NEW"
    fi
 
    echo ""
 
    # Check domain reachability
    log_step "Testing source domain connectivity..."
    if src_curl -s -o /dev/null -w "%{http_code}" \
        "$ES_HOST_OLD/_cluster/health" | grep -q "^2"; then
        log_ok "Source domain is reachable."
    else
        log_warn "Source domain may not be reachable. Check VPN/network access."
    fi
 
    log_step "Testing destination domain connectivity..."
    if dst_curl -s -o /dev/null -w "%{http_code}" \
        "$ES_HOST_NEW/_cluster/health" | grep -q "^2"; then
        log_ok "Destination domain is reachable."
    else
        log_warn "Destination domain may not be reachable. Check VPN/network access."
    fi
 
    if [ "$all_ok" = false ]; then
        log_error "Fix the above issues before continuing."
        exit 1
    fi
 
    log_ok "All prerequisites passed!"
}
 
# ─────────────────────────────────────────────
#  OPTION 0: S3 CROSS-ACCOUNT BUCKET POLICY
# ─────────────────────────────────────────────
 
setup_s3_bucket_policy() {
    log_section "Setting up S3 cross-account bucket policy"
 
    log_step "Creating S3 bucket if it doesn't exist..."
    aws s3api create-bucket \
        --bucket "$S3_BUCKET" \
        --region "$S3_BUCKET_REGION" \
        --profile "$SOURCE_PROFILE" \
        $([ "$S3_BUCKET_REGION" != "us-east-1" ] \
            && echo "--create-bucket-configuration LocationConstraint=$S3_BUCKET_REGION" \
            || echo "") 2>/dev/null || true
 
    log_step "Enabling bucket versioning..."
    aws s3api put-bucket-versioning \
        --bucket "$S3_BUCKET" \
        --versioning-configuration Status=Enabled \
        --profile "$SOURCE_PROFILE"
 
    log_step "Enabling SSE-S3 encryption..."
    aws s3api put-bucket-encryption \
        --bucket "$S3_BUCKET" \
        --server-side-encryption-configuration '{
          "Rules": [{
            "ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}
          }]
        }' \
        --profile "$SOURCE_PROFILE"
 
    log_step "Attaching cross-account bucket policy..."
    POLICY=$(cat <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SourceAccountSnapshotAccess",
      "Effect": "Allow",
      "Principal": { "AWS": "$SOURCE_ROLE_ARN" },
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket",
        "s3:DeleteObject",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::$S3_BUCKET",
        "arn:aws:s3:::$S3_BUCKET/*"
      ]
    },
    {
      "Sid": "DestinationAccountRestoreAccess",
      "Effect": "Allow",
      "Principal": { "AWS": "$DEST_ROLE_ARN" },
      "Action": [
        "s3:GetObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::$S3_BUCKET",
        "arn:aws:s3:::$S3_BUCKET/*"
      ]
    }
  ]
}
EOF
)
 
    aws s3api put-bucket-policy \
        --bucket "$S3_BUCKET" \
        --policy "$POLICY" \
        --profile "$SOURCE_PROFILE"
 
    log_ok "S3 bucket '$S3_BUCKET' is ready with cross-account policy."
    log_ok "Source account can write. Destination account ($DEST_ACCOUNT_ID) can read."
}
 
# ─────────────────────────────────────────────
#  OPTION A: REGISTER REPO ON SOURCE
# ─────────────────────────────────────────────
 
register_repo_source() {
    log_section "Registering snapshot repository on SOURCE domain"
    log_step "Repo name : $REPO"
    log_step "S3 bucket : $S3_BUCKET ($S3_BUCKET_REGION)"
    log_step "IAM role  : $SOURCE_ROLE_ARN"
 
    RESPONSE=$(src_curl -s -X PUT \
        "$ES_HOST_OLD/_snapshot/$REPO" \
        -H 'Content-Type: application/json' \
        -d "{
          \"type\": \"s3\",
          \"settings\": {
            \"bucket\":   \"$S3_BUCKET\",
            \"region\":   \"$S3_BUCKET_REGION\",
            \"base_path\": \"$S3_PREFIX\",
            \"role_arn\": \"$SOURCE_ROLE_ARN\"
          }
        }")
 
    check_response "$RESPONSE" "Repository registration on source"
    echo "$RESPONSE" | jq .
}
 
# ─────────────────────────────────────────────
#  OPTION B: REGISTER REPO ON DESTINATION
# ─────────────────────────────────────────────
 
register_repo_dest() {
    log_section "Registering snapshot repository on DESTINATION domain"
    log_step "Repo name : $REPO"
    log_step "S3 bucket : $S3_BUCKET ($S3_BUCKET_REGION)"
    log_step "IAM role  : $DEST_ROLE_ARN"
 
    RESPONSE=$(dst_curl -s -X PUT \
        "$ES_HOST_NEW/_snapshot/$REPO" \
        -H 'Content-Type: application/json' \
        -d "{
          \"type\": \"s3\",
          \"settings\": {
            \"bucket\":   \"$S3_BUCKET\",
            \"region\":   \"$S3_BUCKET_REGION\",
            \"base_path\": \"$S3_PREFIX\",
            \"role_arn\": \"$DEST_ROLE_ARN\"
          }
        }")
 
    check_response "$RESPONSE" "Repository registration on destination"
    echo "$RESPONSE" | jq .
}
 
# ─────────────────────────────────────────────
#  OPTION 1: CREATE SNAPSHOT + WAIT
# ─────────────────────────────────────────────
 
create_snapshot() {
    read -rp "  Enter snapshot name: " snapshot_name
    read -rp "  Enter comma-separated indices (leave blank for ALL): " indices_input
 
    local indices_body
    if [[ -z "$indices_input" ]]; then
        indices_body='"*"'
        log_step "Snapshotting ALL indices."
    else
        # Convert "idx1,idx2" → ["idx1","idx2"]
        indices_body=$(echo "$indices_input" | \
            python3 -c "import sys; idxs=sys.stdin.read().strip().split(','); \
            print('[' + ','.join('\"'+i.strip()+'\"' for i in idxs) + ']')")
        log_step "Snapshotting indices: $indices_body"
    fi
 
    log_section "Creating snapshot '$snapshot_name'"
 
    RESPONSE=$(src_curl -s -X PUT \
        "$ES_HOST_OLD/_snapshot/$REPO/$snapshot_name?wait_for_completion=false" \
        -H 'Content-Type: application/json' \
        -d "{
          \"indices\": $indices_body,
          \"ignore_unavailable\": true,
          \"include_global_state\": false
        }")
 
    check_response "$RESPONSE" "Snapshot initiation"
    echo "$RESPONSE" | jq .
 
    # ── Poll until complete ──────────────────────
    log_section "Waiting for snapshot to complete..."
    while true; do
        sleep "$POLL_INTERVAL"
 
        STATUS_JSON=$(src_curl -s \
            "$ES_HOST_OLD/_snapshot/$REPO/$snapshot_name")
 
        STATE=$(echo "$STATUS_JSON" | \
            jq -r '.snapshots[0].state // "UNKNOWN"')
 
        SHARDS_TOTAL=$(echo "$STATUS_JSON" | \
            jq -r '.snapshots[0].shards.total // 0')
        SHARDS_DONE=$(echo "$STATUS_JSON" | \
            jq -r '.snapshots[0].shards.successful // 0')
        SHARDS_FAIL=$(echo "$STATUS_JSON" | \
            jq -r '.snapshots[0].shards.failed // 0')
 
        log_step "State: ${BOLD}$STATE${NC}  |  Shards: $SHARDS_DONE/$SHARDS_TOTAL done, $SHARDS_FAIL failed"
 
        case $STATE in
            SUCCESS)
                log_ok "Snapshot '$snapshot_name' completed successfully!"
                break ;;
            FAILED)
                log_error "Snapshot FAILED. Check OpenSearch logs."
                echo "$STATUS_JSON" | jq '.snapshots[0].failures'
                exit 1 ;;
            PARTIAL)
                log_warn "Snapshot PARTIAL — some shards failed. Proceeding but verify data."
                break ;;
        esac
    done
}
 
# ─────────────────────────────────────────────
#  OPTION 2: RESTORE SNAPSHOT + WAIT
# ─────────────────────────────────────────────
 
restore_snapshot() {
    read -rp "  Enter snapshot name to restore: " snapshot_name
 
    log_section "Restoring snapshot '$snapshot_name' on destination"
 
    # Verify snapshot exists on source before restoring
    log_step "Verifying snapshot exists..."
    SNAP_CHECK=$(src_curl -s \
        "$ES_HOST_OLD/_snapshot/$REPO/$snapshot_name")
 
    STATE=$(echo "$SNAP_CHECK" | jq -r '.snapshots[0].state // "NOT_FOUND"')
    if [[ "$STATE" != "SUCCESS" && "$STATE" != "PARTIAL" ]]; then
        log_error "Snapshot '$snapshot_name' state is '$STATE'. Cannot restore."
        exit 1
    fi
    log_ok "Snapshot found. State: $STATE"
 
    RESPONSE=$(dst_curl -s -X POST \
        "$ES_HOST_NEW/_snapshot/$REPO/$snapshot_name/_restore?pretty" \
        -H 'Content-Type: application/json' \
        -d '{
          "include_global_state": false,
          "include_aliases": true,
          "ignore_unavailable": true
        }')
 
    check_response "$RESPONSE" "Restore initiation"
    echo "$RESPONSE" | jq .
 
    # ── Poll shard recovery ──────────────────────
    log_section "Monitoring restore progress..."
    while true; do
        sleep "$POLL_INTERVAL"
 
        RECOVERY=$(dst_curl -s "$ES_HOST_NEW/_cat/recovery?format=json")
 
        TOTAL=$(echo "$RECOVERY"   | jq '[.[] | select(.type=="snapshot")] | length')
        DONE=$(echo "$RECOVERY"    | jq '[.[] | select(.type=="snapshot" and .stage=="done")] | length')
        ONGOING=$(echo "$RECOVERY" | jq '[.[] | select(.type=="snapshot" and .stage!="done")] | length')
 
        log_step "Shards restored: ${DONE}/${TOTAL}  |  In progress: $ONGOING"
 
        if [[ "$ONGOING" -eq 0 && "$TOTAL" -gt 0 ]]; then
            log_ok "All shards restored!"
            break
        fi
 
        if [[ "$TOTAL" -eq 0 ]]; then
            log_warn "No recovery activity detected yet. Waiting..."
        fi
    done
 
    # ── Wait for cluster green ───────────────────
    log_step "Waiting for cluster health to turn GREEN..."
    while true; do
        sleep 15
        HEALTH=$(dst_curl -s "$ES_HOST_NEW/_cluster/health" | \
            jq -r '.status // "unknown"')
        log_step "Cluster health: ${BOLD}$HEALTH${NC}"
        [[ "$HEALTH" == "green" ]] && break
        [[ "$HEALTH" == "red"   ]] && log_warn "Cluster is RED — check unassigned shards."
    done
    log_ok "Cluster is GREEN. Restore complete!"
}
 
# ─────────────────────────────────────────────
#  OPTION 3: DELETE SNAPSHOT
# ─────────────────────────────────────────────
 
delete_snapshot() {
    read -rp "  Enter snapshot name to delete: " snapshot_name
    read -rp "  Are you sure you want to delete '$snapshot_name'? (yes/no): " confirm
    [[ "$confirm" != "yes" ]] && { log_warn "Aborted."; return; }
 
    log_section "Deleting snapshot '$snapshot_name'"
    RESPONSE=$(src_curl -s -X DELETE \
        "$ES_HOST_OLD/_snapshot/$REPO/$snapshot_name?pretty")
    check_response "$RESPONSE" "Snapshot deletion"
    echo "$RESPONSE" | jq .
}
 
# ─────────────────────────────────────────────
#  OPTION 4: MONITOR RUNNING SNAPSHOT
# ─────────────────────────────────────────────
 
monitor_running_snapshot() {
    log_section "Currently running snapshot on source"
    RESPONSE=$(src_curl -s "$ES_HOST_OLD/_snapshot/$REPO/_current?pretty")
    if echo "$RESPONSE" | jq -e '.snapshots | length == 0' &>/dev/null; then
        log_info "No snapshot is currently running."
    else
        echo "$RESPONSE" | jq '{
          name:  .snapshots[0].snapshot,
          state: .snapshots[0].state,
          shards: .snapshots[0].shards,
          start_time: .snapshots[0].start_time
        }'
    fi
}
 
# ─────────────────────────────────────────────
#  OPTION 5: MONITOR RESTORE (recovery)
# ─────────────────────────────────────────────
 
monitor_restore() {
    log_section "Monitoring snapshot restore on destination"
    log_step "Showing non-completed shards only:"
    echo ""
    dst_curl -s "$ES_HOST_NEW/_cat/recovery?v&h=index,shard,stage,files_percent,bytes_percent,time,source_host" | \
        grep -v "^done" || log_info "All shards are done!"
}
 
# ─────────────────────────────────────────────
#  OPTION 6: GET SNAPSHOT DETAILS
# ─────────────────────────────────────────────
 
get_snapshot_details() {
    read -rp "  Enter snapshot name: " snapshot_name
    log_section "Snapshot details: $snapshot_name"
    src_curl -s "$ES_HOST_OLD/_snapshot/$REPO/$snapshot_name?pretty" | jq '{
      name:       .snapshots[0].snapshot,
      state:      .snapshots[0].state,
      start_time: .snapshots[0].start_time,
      end_time:   .snapshots[0].end_time,
      indices:    .snapshots[0].indices,
      shards:     .snapshots[0].shards,
      failures:   .snapshots[0].failures
    }'
}
 
# ─────────────────────────────────────────────
#  OPTION 7: LIST ALL SNAPSHOTS
# ─────────────────────────────────────────────
 
list_all_snapshots() {
    log_section "All snapshots in repo '$REPO'"
    src_curl -s "$ES_HOST_OLD/_snapshot/$REPO/_all?pretty" | \
        jq '.snapshots[] | {name: .snapshot, state: .state, indices: (.indices | length), start_time: .start_time}'
}
 
# ─────────────────────────────────────────────
#  OPTION 8: SHARD STATUS (non-STARTED)
# ─────────────────────────────────────────────
 
check_shards() {
    log_section "Shard status on destination (non-STARTED shards)"
    RESULT=$(dst_curl -s \
        "$ES_HOST_NEW/_cat/shards?v=true&h=index,shard,prirep,state,node,unassigned.reason&s=state" | \
        grep -v "^STARTED" || true)
 
    if [[ -z "$RESULT" ]]; then
        log_ok "All shards are STARTED — no issues detected!"
    else
        log_warn "Non-STARTED shards found:"
        echo "$RESULT"
    fi
}
 
# ─────────────────────────────────────────────
#  OPTION 9: VERIFY DATA (doc count comparison)
# ─────────────────────────────────────────────
 
verify_migration() {
    log_section "Verifying migration — comparing document counts"
 
    SRC_INDICES=$(src_curl -s "$ES_HOST_OLD/_cat/indices?format=json&h=index,docs.count" | \
        jq -r '.[] | select(.index | startswith(".") | not) | "\(.index) \(."docs.count")"')
 
    DST_INDICES=$(dst_curl -s "$ES_HOST_NEW/_cat/indices?format=json&h=index,docs.count" | \
        jq -r '.[] | select(.index | startswith(".") | not) | "\(.index) \(."docs.count")"')
 
    printf "\n  %-45s %12s %12s %8s\n" "INDEX" "SOURCE" "DESTINATION" "STATUS"
    printf "  %-45s %12s %12s %8s\n" \
        "─────────────────────────────────────────────" \
        "────────────" "────────────" "────────"
 
    ALL_OK=true
    while IFS= read -r line; do
        INDEX=$(echo "$line" | awk '{print $1}')
        SRC_COUNT=$(echo "$line" | awk '{print $2}')
        DST_COUNT=$(echo "$DST_INDICES" | grep "^$INDEX " | awk '{print $2}')
        DST_COUNT="${DST_COUNT:-0}"
 
        if [[ "$SRC_COUNT" -eq "$DST_COUNT" ]]; then
            STATUS="${GREEN}✓${NC}"
        else
            STATUS="${RED}✗${NC}"
            ALL_OK=false
        fi
 
        printf "  %-45s %12s %12s   " "$INDEX" "$SRC_COUNT" "$DST_COUNT"
        echo -e "$STATUS"
    done <<< "$SRC_INDICES"
 
    echo ""
    if $ALL_OK; then
        log_ok "All document counts match — migration verified!"
    else
        log_warn "Some indices have mismatched counts. Consider re-running restore."
    fi
}
 
# ─────────────────────────────────────────────
#  MENU
# ─────────────────────────────────────────────
 
show_menu() {
    echo ""
    echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  AWS OpenSearch Cross-Account Migration Tool${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
    echo ""
    echo -e "${BOLD}  ── Setup (run in order first time) ──${NC}"
    echo "  P  Check prerequisites"
    echo "  0  Setup S3 bucket + cross-account policy"
    echo "  A  Register snapshot repo on SOURCE domain"
    echo "  B  Register snapshot repo on DESTINATION domain"
    echo ""
    echo -e "${BOLD}  ── Migration ──${NC}"
    echo "  1  Create snapshot (with auto-wait)"
    echo "  2  Restore snapshot (with auto-wait)"
    echo ""
    echo -e "${BOLD}  ── Management ──${NC}"
    echo "  3  Delete snapshot"
    echo "  6  Get snapshot details"
    echo "  7  List all snapshots"
    echo ""
    echo -e "${BOLD}  ── Monitoring ──${NC}"
    echo "  4  Monitor currently running snapshot"
    echo "  5  Monitor restore (shard recovery)"
    echo "  8  Shard health check (non-STARTED shards)"
    echo "  9  Verify migration (compare doc counts)"
    echo ""
    echo "  Q  Quit"
    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
}
 
# ─────────────────────────────────────────────
#  MAIN LOOP
# ─────────────────────────────────────────────
 
main() {
    while true; do
        show_menu
        read -rp "  Enter your choice: " choice
 
        case "${choice^^}" in
            P)  check_prerequisites ;;
            0)  setup_s3_bucket_policy ;;
            A)  register_repo_source ;;
            B)  register_repo_dest ;;
            1)  create_snapshot ;;
            2)  restore_snapshot ;;
            3)  delete_snapshot ;;
            4)  monitor_running_snapshot ;;
            5)  monitor_restore ;;
            6)  get_snapshot_details ;;
            7)  list_all_snapshots ;;
            8)  check_shards ;;
            9)  verify_migration ;;
            Q)  log_info "Goodbye!"; exit 0 ;;
            *)  log_warn "Invalid choice '$choice'. Try again." ;;
        esac
 
        echo ""
        read -rp "  Press Enter to return to menu..." _
    done
}
 
main