Here's everything that's in the script:
How to run it
chmod +x es_migration.sh
./es_migration.shFirst-time setup order (do these in sequence)
| Step | Option | What it does |
|---|---|---|
| 1 | P | Checks all tools, profiles, and connectivity |
| 2 | 0 | Creates S3 bucket, enables encryption + versioning, attaches cross-account policy |
| 3 | A | Registers snapshot repo on source domain |
| 4 | B | Registers snapshot repo on destination domain |
| 5 | 1 | Takes snapshot — auto-polls until SUCCESS |
| 6 | 2 | Restores snapshot — auto-polls until all shards done + cluster is green |
| 7 | 9 | Compares doc counts index-by-index to verify zero data loss |
Key fixes from your original script
- Authentication — all
curlcalls replaced withawscurl(SigV4 signed), so they work with AWS-managed domains without any proxy - Repo registration — options
AandBhandle 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
9does a side-by-side doc count comparison across all indices - Confirmation prompt — delete option asks for
yesbefore 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