sless-primer/VM/.vm_stress_test.sh.OLD

853 lines
36 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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" <<EOF
# AUTO-GENERATED by vm_stress_test.sh — $(date '+%Y-%m-%d %H:%M:%S')
vm_public_key = "$key"
api_token = "$token"
# ---- Флаги установки --------------------------------------------------------
install_packages = $pkgs
install_nginx = $nginx
install_docker = $docker
install_run_id = $run_id
base_packages = $base_pkgs
EOF
}
# ── VM SSH HELPERS ────────────────────────────────────────────────────────────
# get_vm_ip: получить внешний IP VM из terraform output.
get_vm_ip() {
terraform output -json vm_state 2>/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