#!/usr/bin/env bash # 2026-03-30 — vm_stress_test.sh # Автономный stress/chaos тест для examples/VM. # # ЦЕЛЬ: прогнать полную матрицу сценариев lifecycle VM + sless_job без # вмешательства человека. Скрипт самовосстанавливается при ошибках # и продолжает следующую фазу. # # ФАЗЫ: # 1 BASELINE — apply с полным набором (packages + nginx + docker) # 2 IDEMPOTENT — повторный apply → "No changes" # 3 PARTIAL_DISABLE — выключить nginx + docker → apply (2 job уничтожаются) # 4 PARTIAL_ENABLE — включить обратно → apply (2 job создаются) # 5 REORDER_PACKAGES — изменить порядок и состав base_packages # 6 MANUAL_PURGE — удалить все пакеты с VM по SSH → re-run jobs # 7 DESTROY — terraform destroy → VM уходит в suspend # 8 RESURRECT — apply после destroy → VM просыпается, всё ставится # 9 STRESS_CYCLES — N подряд destroy/apply циклов # 10 FINAL_SANITY — проверить доступность VM и установленные пакеты # # ЗАПУСК: # bash ~/terra/sless/examples/VM/vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log # # ПАРАМЕТРЫ (env): # STRESS_CYCLES=2 — количество destroy/apply циклов в фазе 9 (default: 2) # SKIP_DESTROY=1 — пропустить фазы 7-9 (для быстрого прогона) # # ТРЕБОВАНИЯ: terraform, ssh, python3 — всё доступно на VM naeel@5.172.178.213 # # ДЛЯ БЕСПЛАТНЫХ AI-АГЕНТОВ: # 1. Подключиться по SSH к VM # 2. Запустить: bash ~/terra/sless/examples/VM/vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log # 3. Дождаться завершения (exit code 0 = все тесты прошли) # 4. Проанализировать вывод: grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log # 5. Итоговая сводка печатается в конце лога set -uo pipefail # ── CONFIG ──────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" VM_KEY="$SCRIPT_DIR/vm_key" TFVARS="$SCRIPT_DIR/terraform.tfvars" TFVARS_BACKUP="$SCRIPT_DIR/.terraform.tfvars.test-backup" STRESS_CYCLES="${STRESS_CYCLES:-2}" SKIP_DESTROY="${SKIP_DESTROY:-0}" # ── СТАТИСТИКА ──────────────────────────────────────────────────────────────── PASS=0; FAIL=0; SKIP=0 PHASE_RESULTS=() # "PHASE_NAME:PASS|FAIL|SKIP" START_TIME=$SECONDS # ── ЦВЕТА ───────────────────────────────────────────────────────────────────── GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' # ── HELPERS ─────────────────────────────────────────────────────────────────── pass() { echo -e " ${GREEN}[PASS]${RESET} $1"; ((PASS++)); } fail() { echo -e " ${RED}[FAIL]${RESET} $1"; ((FAIL++)); } skip() { echo -e " ${YELLOW}[SKIP]${RESET} $1"; ((SKIP++)); } info() { echo -e " ${CYAN}[INFO]${RESET} $1"; } warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; } phase_header() { local num="$1" name="$2" echo "" echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo -e "${BOLD}${CYAN} ФАЗА $num: $name${RESET}" echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo -e " ${CYAN}[TIME]${RESET} $(date '+%H:%M:%S')" } phase_result() { local name="$1" result="$2" PHASE_RESULTS+=("$name:$result") echo -e " ${CYAN}[PHASE]${RESET} $name → ${result}" } # ── TERRAFORM HELPERS ───────────────────────────────────────────────────────── # tf_apply: terraform apply с retry при сетевых ошибках. # Возвращает 0 при успехе, 1 при ошибке. tf_apply() { local attempt=1 max=3 while [[ $attempt -le $max ]]; do info "terraform apply (попытка $attempt/$max)..." # Явное указание var-file и input=false чтобы избежать запросов в терминале if terraform apply -var-file="terraform.tfvars" -auto-approve -input=false -no-color 2>&1 | tee /tmp/vm_tf_apply.log; then return 0 fi if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_apply.log && [[ $attempt -lt $max ]]; then warn "сетевой сбой, retry через $((attempt * 5))s..." sleep $((attempt * 5)) ((attempt++)) continue fi return 1 done return 1 } # tf_destroy: terraform destroy с retry. tf_destroy() { local attempt=1 max=3 while [[ $attempt -le $max ]]; do info "terraform destroy (попытка $attempt/$max)..." # Добавляем -var-file и -input=false сюда тоже if terraform destroy -var-file="terraform.tfvars" -auto-approve -input=false -no-color 2>&1 | tee /tmp/vm_tf_destroy.log; then return 0 fi if grep -Eiq 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline|Client\.Timeout' /tmp/vm_tf_destroy.log && [[ $attempt -lt $max ]]; then warn "сетевой сбой, retry через $((attempt * 5))s..." sleep $((attempt * 5)) ((attempt++)) continue fi return 1 done return 1 } # tf_plan_no_changes: проверить что plan не содержит изменений. # Возвращает 0 если "No changes", 1 если есть изменения. tf_plan_no_changes() { # Добавляем -var-file и -input=false чтобы не было запросов в терминал terraform plan -var-file="terraform.tfvars" -input=false -no-color 2>&1 | tee /tmp/vm_tf_plan.log if grep -q 'No changes' /tmp/vm_tf_plan.log; then return 0 fi return 1 } # tf_state_count: количество ресурсов в state. tf_state_count() { terraform state list 2>/dev/null | wc -l } # ── TFVARS HELPERS ──────────────────────────────────────────────────────────── # Сохранять/восстанавливать tfvars для самовосстановления. backup_tfvars() { if [[ ! -f "$TFVARS_BACKUP" ]]; then cp "$TFVARS" "$TFVARS_BACKUP" info "Создан бэкап tfvars: $TFVARS_BACKUP" fi } restore_tfvars() { if [[ -f "$TFVARS_BACKUP" ]]; then # Читаем токен и ключ из бэкапа перед перезаписью local token key token=$(grep 'api_token' "$TFVARS_BACKUP" | cut -d'"' -f2) key=$(grep 'vm_public_key' "$TFVARS_BACKUP" | cut -d'"' -f2) # Если в бэкапе пусто (такое бывает при сбоях сохранения), не портим оригинал if [[ -n "$token" && -n "$key" ]]; then cp "$TFVARS_BACKUP" "$TFVARS" info "tfvars восстановлен из бэкапа" else warn "Бэкап tfvars поврежден или пуст, восстановление пропущено" fi fi } # write_tfvars: перезаписать tfvars программно. # Аргументы: install_packages install_nginx install_docker run_id base_packages_json write_tfvars() { local pkgs="$1" nginx="$2" docker="$3" run_id="$4" base_pkgs="$5" # Читаем токен напрямую из файла secrets если в бэкапе пусто local token key token=$(grep 'api_token' "$TFVARS_BACKUP" 2>/dev/null | cut -d'=' -f2- | cut -d'#' -f1 | xargs | sed 's/^"//;s/"$//') if [[ -z "$token" ]]; then info "Токен не найден в бэкапе, берем из ~/terra/sless/secrets/tazetnarodtest.token" token=$(ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=5 "ubuntu@185.247.187.154" "cat ~/terra/sless/secrets/tazetnarodtest.token" 2>/dev/null || cat "$SCRIPT_DIR/../../secrets/tazetnarodtest.token" 2>/dev/null) fi key=$(grep 'vm_public_key' "$TFVARS_BACKUP" 2>/dev/null | cut -d'=' -f2- | cut -d'#' -f1 | xargs | sed 's/^"//;s/"$//') if [[ -z "$key" ]]; then key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQB+Kyevn0H8QSdfm6ZpCU9jtskBxUS9BV0+i1/M04A sless-demo-vm" fi cat > "$TFVARS" </dev/null \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('externalConnect',''))" 2>/dev/null } # vm_ssh: выполнить команду на VM. Возвращает exit code команды. vm_ssh() { local ip="$1"; shift ssh -i "$VM_KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=15 \ -o ServerAliveInterval=5 -o ServerAliveCountMax=3 \ "ubuntu@$ip" "$@" } # vm_alive: проверить доступность VM по SSH. vm_alive() { local ip="$1" vm_ssh "$ip" 'echo alive' &>/dev/null } # vm_wait_alive: ждать пока VM ответит по SSH (до timeout_sec). vm_wait_alive() { local ip="$1" timeout_sec="${2:-120}" local deadline=$((SECONDS + timeout_sec)) while [[ $SECONDS -lt $deadline ]]; do if vm_alive "$ip"; then return 0; fi sleep 10 done return 1 } # vm_check_package: проверить что пакет установлен. vm_check_package() { local ip="$1" pkg="$2" vm_ssh "$ip" "dpkg -l $pkg 2>/dev/null | grep -q '^ii'" 2>/dev/null } # vm_check_binary: проверить что бинарник доступен. vm_check_binary() { local ip="$1" bin="$2" vm_ssh "$ip" "command -v $bin" &>/dev/null } # vm_purge_all: удалить все установленные пакеты с VM. vm_purge_all() { local ip="$1" info "удаляю пакеты с VM $ip..." vm_ssh "$ip" 'sudo systemctl stop nginx docker 2>/dev/null; sudo apt-get purge -y jq python3-pip htop unzip nginx docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null; sudo apt-get autoremove -y 2>/dev/null; sudo rm -rf /var/lib/docker /var/lib/containerd; echo purge_done' 2>/dev/null } # ── SELF-RECOVERY ───────────────────────────────────────────────────────────── # ensure_baseline: гарантировать что VM и все 3 job в state. # Используется для восстановления после сбоя фазы. ensure_baseline() { info "восстанавливаю baseline state..." restore_tfvars # Перезаписать tfvars на полный набор с текущим run_id local run_id run_id=$(grep 'install_run_id' "$TFVARS_BACKUP" | grep -oP '\d+' | head -1) write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' tf_apply || warn "baseline apply не удался — продолжаю" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 1: BASELINE — apply с полным набором # ══════════════════════════════════════════════════════════════════════════════ phase_1_baseline() { phase_header 1 "BASELINE — apply с полным набором" # Сохранить оригинальный tfvars backup_tfvars # Читаем текущий run_id и инкрементируем local run_id run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) run_id=$((run_id + 1)) write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' if tf_apply; then pass "1.1 terraform apply завершился успешно" else fail "1.1 terraform apply упал" phase_result "BASELINE" "FAIL" return 1 fi # Проверить количество ресурсов local count count=$(tf_state_count) if [[ $count -ge 5 ]]; then pass "1.2 state содержит $count ресурсов (ожидали ≥5)" else fail "1.2 state содержит $count ресурсов (ожидали ≥5)" fi # Проверить outputs local out out=$(terraform output -json 2>/dev/null) if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'ok' in d['install_packages_result']['value'] or 'already' in d['install_packages_result']['value']" 2>/dev/null; then pass "1.3 install_packages_result содержит ok/already_installed" else fail "1.3 install_packages_result не содержит ok" fi if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_nginx_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then pass "1.4 install_nginx_result содержит ok/already" else fail "1.4 install_nginx_result не содержит ok" fi if echo "$out" | python3 -c "import sys,json; d=json.load(sys.stdin); v=d['install_docker_result']['value']; assert 'ok' in v or 'already' in v" 2>/dev/null; then pass "1.5 install_docker_result содержит ok/already" else fail "1.5 install_docker_result не содержит ok" fi # Проверить VM по SSH local ip ip=$(get_vm_ip) if [[ -n "$ip" ]] && vm_alive "$ip"; then pass "1.6 VM $ip доступна по SSH" else fail "1.6 VM не доступна по SSH (ip=$ip)" fi # Обновить бэкап с новым run_id backup_tfvars phase_result "BASELINE" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 2: IDEMPOTENT — повторный apply без изменений # ══════════════════════════════════════════════════════════════════════════════ phase_2_idempotent() { phase_header 2 "IDEMPOTENT — повторный apply без изменений" if tf_plan_no_changes; then pass "2.1 terraform plan → No changes" else fail "2.1 terraform plan показывает изменения (ожидали No changes)" # Показать что именно grep -E 'will be|must be' /tmp/vm_tf_plan.log 2>/dev/null | head -5 | while read -r line; do info " $line" done fi phase_result "IDEMPOTENT" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 3: PARTIAL_DISABLE — выключить nginx + docker # ══════════════════════════════════════════════════════════════════════════════ phase_3_partial_disable() { phase_header 3 "PARTIAL_DISABLE — выключить nginx + docker" local run_id run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) write_tfvars "true" "false" "false" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' if tf_apply; then pass "3.1 apply с partial disable завершился" else fail "3.1 apply с partial disable упал" restore_tfvars phase_result "PARTIAL_DISABLE" "FAIL" return 1 fi # Проверить что nginx и docker job пропали из state local state_list state_list=$(terraform state list 2>/dev/null) if echo "$state_list" | grep -q 'install_nginx'; then fail "3.2 install_nginx всё ещё в state" else pass "3.2 install_nginx убран из state" fi if echo "$state_list" | grep -q 'install_docker'; then fail "3.3 install_docker всё ещё в state" else pass "3.3 install_docker убран из state" fi if echo "$state_list" | grep -q 'install_packages'; then pass "3.4 install_packages остался в state" else fail "3.4 install_packages пропал из state" fi # Проверить outputs local nginx_out docker_out nginx_out=$(terraform output -raw install_nginx_result 2>/dev/null) docker_out=$(terraform output -raw install_docker_result 2>/dev/null) if [[ "$nginx_out" == "skipped" ]]; then pass "3.5 install_nginx_result = skipped" else fail "3.5 install_nginx_result = '$nginx_out' (ожидали skipped)" fi if [[ "$docker_out" == "skipped" ]]; then pass "3.6 install_docker_result = skipped" else fail "3.6 install_docker_result = '$docker_out' (ожидали skipped)" fi phase_result "PARTIAL_DISABLE" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 4: PARTIAL_ENABLE — включить обратно всё # ══════════════════════════════════════════════════════════════════════════════ phase_4_partial_enable() { phase_header 4 "PARTIAL_ENABLE — включить обратно всё" local run_id run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) run_id=$((run_id + 1)) write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' if tf_apply; then pass "4.1 apply с полным набором завершился" else fail "4.1 apply с полным набором упал" phase_result "PARTIAL_ENABLE" "FAIL" return 1 fi local count count=$(tf_state_count) if [[ $count -ge 5 ]]; then pass "4.2 state содержит $count ресурсов (ожидали ≥5)" else fail "4.2 state содержит $count ресурсов (ожидали ≥5)" fi # Обновить бэкап backup_tfvars phase_result "PARTIAL_ENABLE" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 5: REORDER_PACKAGES — изменить порядок и состав base_packages # ══════════════════════════════════════════════════════════════════════════════ phase_5_reorder() { phase_header 5 "REORDER_PACKAGES — изменить порядок и состав пакетов" local run_id run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) run_id=$((run_id + 1)) # Другой порядок и сокращённый набор write_tfvars "true" "true" "true" "$run_id" '["htop", "jq"]' if tf_apply; then pass "5.1 apply с изменённым набором пакетов завершился" else fail "5.1 apply с изменённым набором пакетов упал" restore_tfvars phase_result "REORDER_PACKAGES" "FAIL" return 1 fi # Проверить output local pkg_out pkg_out=$(terraform output -raw install_packages_result 2>/dev/null) if echo "$pkg_out" | grep -q '"status"'; then pass "5.2 install_packages вернул JSON с status" else fail "5.2 install_packages output неожиданный: $pkg_out" fi # Вернуть полный набор run_id=$((run_id + 1)) write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' tf_apply || warn "5.3 восстановление полного набора не удалось" backup_tfvars phase_result "REORDER_PACKAGES" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 6: MANUAL_PURGE — удалить пакеты с VM вручную, заново установить # ══════════════════════════════════════════════════════════════════════════════ phase_6_manual_purge() { phase_header 6 "MANUAL_PURGE — удалить пакеты с VM, заново установить" local ip ip=$(get_vm_ip) if [[ -z "$ip" ]]; then fail "6.0 не удалось получить IP VM" phase_result "MANUAL_PURGE" "FAIL" return 1 fi # Удалить всё с VM vm_purge_all "$ip" # Проверить что пакеты действительно удалены if vm_check_binary "$ip" "docker"; then fail "6.1 docker всё ещё на VM после purge" else pass "6.1 docker удалён с VM" fi if vm_check_binary "$ip" "nginx"; then fail "6.2 nginx всё ещё на VM после purge" else pass "6.2 nginx удалён с VM" fi if vm_check_binary "$ip" "jq"; then fail "6.3 jq всё ещё на VM после purge" else pass "6.3 jq удалён с VM" fi # Пересоздать jobs (bump run_id) local run_id run_id=$(grep 'install_run_id' "$TFVARS" | grep -oP '\d+' | head -1) run_id=$((run_id + 1)) write_tfvars "true" "true" "true" "$run_id" '["jq", "python3-pip", "htop", "unzip"]' info "запускаю переустановку через sless_job..." if tf_apply; then pass "6.4 apply после purge завершился" else fail "6.4 apply после purge упал" phase_result "MANUAL_PURGE" "FAIL" return 1 fi # Подождать чуть-чуть и проверить что пакеты вернулись sleep 5 if vm_check_binary "$ip" "docker"; then pass "6.5 docker установлен заново" else fail "6.5 docker НЕ установлен после re-apply" fi if vm_check_binary "$ip" "nginx"; then pass "6.6 nginx установлен заново" else fail "6.6 nginx НЕ установлен после re-apply" fi if vm_check_binary "$ip" "jq"; then pass "6.7 jq установлен заново" else fail "6.7 jq НЕ установлен после re-apply" fi backup_tfvars phase_result "MANUAL_PURGE" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 7: DESTROY — terraform destroy, VM уходит в suspend # ══════════════════════════════════════════════════════════════════════════════ phase_7_destroy() { phase_header 7 "DESTROY — terraform destroy → VM в suspend" local ip ip=$(get_vm_ip) if tf_destroy; then pass "7.1 terraform destroy завершился" else fail "7.1 terraform destroy упал" phase_result "DESTROY" "FAIL" return 1 fi # State должен быть пуст local count count=$(tf_state_count) if [[ $count -eq 0 ]]; then pass "7.2 state пуст ($count ресурсов)" else fail "7.2 state не пуст ($count ресурсов)" fi # VM не должна отвечать по SSH if [[ -n "$ip" ]]; then info "проверяю что VM $ip недоступна..." if vm_alive "$ip"; then fail "7.3 VM $ip всё ещё отвечает по SSH после destroy" else pass "7.3 VM $ip не отвечает по SSH (suspend подтверждён)" fi else skip "7.3 IP VM неизвестен, пропускаю SSH-проверку" fi phase_result "DESTROY" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 8: RESURRECT — apply после destroy, VM просыпается # ══════════════════════════════════════════════════════════════════════════════ phase_8_resurrect() { phase_header 8 "RESURRECT — apply после destroy" if tf_apply; then pass "8.1 apply после destroy завершился" else fail "8.1 apply после destroy упал" phase_result "RESURRECT" "FAIL" return 1 fi local count count=$(tf_state_count) if [[ $count -ge 5 ]]; then pass "8.2 state содержит $count ресурсов" else fail "8.2 state содержит $count ресурсов (ожидали ≥5)" fi # Проверить VM local ip ip=$(get_vm_ip) if [[ -n "$ip" ]] && vm_alive "$ip"; then pass "8.3 VM $ip доступна по SSH после resurrect" else # VM может ещё просыпаться — ждём info "VM не отвечает, жду до 120s..." if vm_wait_alive "$ip" 120; then pass "8.3 VM $ip доступна по SSH после ожидания" else fail "8.3 VM $ip не доступна по SSH через 120s" fi fi # Проверить пакеты if [[ -n "$ip" ]] && vm_alive "$ip"; then if vm_check_binary "$ip" "jq"; then pass "8.4 jq установлен после resurrect" else fail "8.4 jq НЕ установлен после resurrect" fi if vm_check_binary "$ip" "nginx"; then pass "8.5 nginx установлен после resurrect" else fail "8.5 nginx НЕ установлен после resurrect" fi if vm_check_binary "$ip" "docker"; then pass "8.6 docker установлен после resurrect" else fail "8.6 docker НЕ установлен после resurrect" fi fi backup_tfvars phase_result "RESURRECT" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 9: STRESS_CYCLES — N destroy/apply циклов подряд # ══════════════════════════════════════════════════════════════════════════════ phase_9_stress() { phase_header 9 "STRESS_CYCLES — $STRESS_CYCLES циклов destroy/apply" local i for i in $(seq 1 "$STRESS_CYCLES"); do info "── цикл $i/$STRESS_CYCLES ──" info "[$i] destroy..." if tf_destroy; then pass "9.${i}a destroy цикл $i" else fail "9.${i}a destroy цикл $i упал" # Попробовать восстановиться tf_apply || true continue fi info "[$i] apply..." if tf_apply; then pass "9.${i}b apply цикл $i" else fail "9.${i}b apply цикл $i упал" continue fi done phase_result "STRESS_CYCLES" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # ФАЗА 10: FINAL_SANITY — финальная проверка состояния # ══════════════════════════════════════════════════════════════════════════════ phase_10_final() { phase_header 10 "FINAL_SANITY — финальная проверка" # Убедиться что state на месте local count count=$(tf_state_count) if [[ $count -ge 5 ]]; then pass "10.1 state содержит $count ресурсов" else fail "10.1 state содержит $count ресурсов (ожидали ≥5)" # Попробовать восстановить ensure_baseline fi # Plan = no changes if tf_plan_no_changes; then pass "10.2 terraform plan → No changes" else fail "10.2 terraform plan показывает изменения" fi # VM доступна local ip ip=$(get_vm_ip) if [[ -n "$ip" ]] && vm_alive "$ip"; then pass "10.3 VM $ip доступна по SSH" # Полная проверка пакетов local all_ok=true for pkg in jq htop unzip; do if vm_check_package "$ip" "$pkg"; then pass "10.4 пакет $pkg установлен" else fail "10.4 пакет $pkg НЕ установлен" all_ok=false fi done if vm_check_binary "$ip" "nginx"; then pass "10.5 nginx работает" else fail "10.5 nginx НЕ найден" fi if vm_check_binary "$ip" "docker"; then pass "10.6 docker работает" else fail "10.6 docker НЕ найден" fi else fail "10.3 VM не доступна по SSH" fi phase_result "FINAL_SANITY" "PASS" } # ══════════════════════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════════════════════ echo -e "${BOLD}${CYAN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ VM STRESS TEST — examples/VM ║" echo "║ $(date '+%Y-%m-%d %H:%M:%S') ║" echo "║ Циклов stress: $STRESS_CYCLES ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${RESET}" # Проверить что мы в правильной директории if [[ ! -f "$TFVARS" ]]; then echo -e "${RED}ОШИБКА: $TFVARS не найден. Запускайте из examples/VM/${RESET}" exit 1 fi if [[ ! -f "$VM_KEY" ]]; then echo -e "${RED}ОШИБКА: $VM_KEY не найден${RESET}" exit 1 fi # Запуск фаз phase_1_baseline phase_2_idempotent phase_3_partial_disable phase_4_partial_enable phase_5_reorder phase_6_manual_purge if [[ "$SKIP_DESTROY" == "1" ]]; then skip "фазы 7-9 пропущены (SKIP_DESTROY=1)" PHASE_RESULTS+=("DESTROY:SKIP" "RESURRECT:SKIP" "STRESS_CYCLES:SKIP") else phase_7_destroy phase_8_resurrect phase_9_stress fi phase_10_final # ── ИТОГОВАЯ СВОДКА ────────────────────────────────────────────────────────── ELAPSED=$((SECONDS - START_TIME)) ELAPSED_MIN=$((ELAPSED / 60)) ELAPSED_SEC=$((ELAPSED % 60)) echo "" echo -e "${BOLD}${CYAN}╔═══════════════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}${CYAN}║ ИТОГОВАЯ СВОДКА ║${RESET}" echo -e "${BOLD}${CYAN}╠═══════════════════════════════════════════════════════════════╣${RESET}" echo -e "${BOLD} Время: ${ELAPSED_MIN}m ${ELAPSED_SEC}s${RESET}" echo -e "${BOLD} ${GREEN}PASS: $PASS${RESET} ${RED}FAIL: $FAIL${RESET} ${YELLOW}SKIP: $SKIP${RESET}" echo "" echo -e "${BOLD} Фазы:${RESET}" for pr in "${PHASE_RESULTS[@]}"; do name="${pr%%:*}" result="${pr##*:}" case "$result" in PASS) echo -e " ${GREEN}✓${RESET} $name" ;; FAIL) echo -e " ${RED}✗${RESET} $name" ;; SKIP) echo -e " ${YELLOW}○${RESET} $name" ;; esac done echo -e "${BOLD}${CYAN}╚═══════════════════════════════════════════════════════════════╝${RESET}" # Восстановить оригинальный tfvars restore_tfvars # Exit code: 0 если все PASS, 1 если есть FAIL if [[ $FAIL -gt 0 ]]; then echo -e "\n${RED}РЕЗУЛЬТАТ: FAIL ($FAIL ошибок)${RESET}" exit 1 else echo -e "\n${GREEN}РЕЗУЛЬТАТ: ALL PASS${RESET}" exit 0 fi