sless-primer/VM/vm_stress_test.sh

921 lines
41 KiB
Bash
Executable File
Raw 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 (v2 — READ-ONLY)
# Автономный READ-ONLY stress/chaos тест для examples/VM.
#
# ⛔⛔⛔ КРИТИЧЕСКОЕ ПРАВИЛО — ДЛЯ AI-АГЕНТОВ И ЛЮДЕЙ ⛔⛔⛔
#
# Этот скрипт НИКОГДА НЕ МОДИФИЦИРУЕТ terraform.tfvars и НИКАКИЕ ДРУГИЕ ФАЙЛЫ.
# Все переопределения переменных — ТОЛЬКО через terraform CLI опцию -var.
# Файл terraform.tfvars ЧИТАЕТСЯ, но НИКОГДА НЕ ПЕРЕЗАПИСЫВАЕТСЯ.
#
# ЗАПРЕЩЕНО:
# - Редактировать этот скрипт
# - Редактировать terraform.tfvars
# - Редактировать любые .tf файлы
# - Запускать terraform напрямую — только через этот скрипт
#
# ПРИЧИНА: предыдущая версия скрипта уничтожила terraform.tfvars
# через write_tfvars() — потерян JWT-токен api_token. Это НЕДОПУСТИМО.
#
# ФАЗЫ:
# 1 BASELINE — apply с текущим tfvars (packages + nginx + docker)
# 2 IDEMPOTENT — повторный plan → "No changes"
# 3 PARTIAL_DISABLE — apply с -var install_nginx=false -var install_docker=false
# 4 PARTIAL_ENABLE — apply с -var install_nginx=true -var install_docker=true (run_id+1)
# 5 REORDER_PACKAGES — apply с -var 'base_packages=["htop","jq"]' (run_id+1)
# 6 MANUAL_PURGE — удалить пакеты с VM по SSH → apply (run_id+1)
# 7 DESTROY — terraform destroy → VM уходит в suspend
# 8 RESURRECT — apply после destroy → VM просыпается
# 9 STRESS_CYCLES — N подряд destroy/apply циклов
# 10 FINAL_SANITY — проверить доступность VM и пакеты
#
# ЗАПУСК:
# cd ~/terra/sless/examples/VM
# bash 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 (для быстрого прогона)
#
# АНАЛИЗ РЕЗУЛЬТАТОВ:
# grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log
# Итоговая сводка печатается в конце лога.
#
# ТРЕБОВАНИЯ: terraform, ssh, python3 — всё на VM naeel@5.172.178.213
set -uo pipefail
# ── CONFIG ────────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
VM_KEY="$SCRIPT_DIR/vm_key"
STRESS_CYCLES="${STRESS_CYCLES:-2}"
SKIP_DESTROY="${SKIP_DESTROY:-0}"
# Читаем текущий run_id из terraform.tfvars (ТОЛЬКО ЧТЕНИЕ, не запись)
RUN_ID=$(grep 'install_run_id' terraform.tfvars | grep -oP '\d+' | head -1)
# ── СТАТИСТИКА ────────────────────────────────────────────────────────────────
PASS=0; FAIL=0; SKIP=0
PHASE_RESULTS=()
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 ─────────────────────────────────────────────────────────
# ⛔ НЕ ТРОГАЕМ terraform.tfvars. Переопределения — ТОЛЬКО через -var.
# terraform.tfvars подхватывается автоматически (лежит в рабочей директории).
# Доп. -var аргументы передаются во все tf_* функции и ПЕРЕОПРЕДЕЛЯЮТ tfvars.
# tf_apply: apply с retry при сетевых ошибках.
# Аргументы: любые доп. -var (опционально).
# Пример: tf_apply -var install_nginx=false -var install_docker=false
tf_apply() {
local attempt=1 max=3
while [[ $attempt -le $max ]]; do
info "terraform apply (попытка $attempt/$max)..."
if terraform apply -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: destroy с retry.
# Аргументы: любые доп. -var (опционально).
tf_destroy() {
local attempt=1 max=3
while [[ $attempt -le $max ]]; do
info "terraform destroy (попытка $attempt/$max)..."
if terraform destroy -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 → "No changes".
# Аргументы: любые доп. -var (опционально).
tf_plan_no_changes() {
terraform plan -input=false -no-color "$@" 2>&1 | tee /tmp/vm_tf_plan.log
grep -q 'No changes' /tmp/vm_tf_plan.log
}
# tf_state_count: количество ресурсов в state.
tf_state_count() {
terraform state list 2>/dev/null | wc -l
}
# next_run_id: инкрементировать внутренний счётчик RUN_ID и вернуть новое значение.
# Используется для -var install_run_id=N чтобы форсировать пересоздание jobs.
next_run_id() {
RUN_ID=$((RUN_ID + 1))
echo "$RUN_ID"
}
# ── 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_wait_binary: ждать пока бинарник появится на VM (sless_job работает асинхронно).
# Нужен потому что sless_job запускает kubernetes Job, который сначала собирает образ,
# затем стартует pod, и только потом SSH-устанавливает пакеты на VM — это занимает 1-3 мин.
vm_wait_binary() {
local ip="$1" bin="$2" timeout_sec="${3:-180}"
local deadline=$((SECONDS + timeout_sec))
info "жду появления '$bin' на VM (до ${timeout_sec}s)..."
while [[ $SECONDS -lt $deadline ]]; do
if vm_check_binary "$ip" "$bin"; then return 0; fi
sleep 15
done
return 1
}
# 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
}
# ══════════════════════════════════════════════════════════════════════════════
# ФАЗА 1: BASELINE — apply с текущим tfvars (всё включено)
# Используем -var install_run_id=N чтобы гарантировать свежий запуск.
# terraform.tfvars НЕ ТРОГАЕМ — берём как есть.
# ══════════════════════════════════════════════════════════════════════════════
phase_1_baseline() {
phase_header 1 "BASELINE — apply с полным набором"
local rid
rid=$(next_run_id)
# Все флаги = true (как в tfvars), но run_id инкрементирован
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid"; 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
phase_result "BASELINE" "PASS"
}
# ══════════════════════════════════════════════════════════════════════════════
# ФАЗА 2: IDEMPOTENT — повторный apply с теми же аргументами → 0 changed
# Используем apply (не plan) — это надёжнее: некоторые sless-провайдеры показывают
# ложный drift в plan, но apply при этом возвращает 0 changed. Apply — canonical way.
# ══════════════════════════════════════════════════════════════════════════════
phase_2_idempotent() {
phase_header 2 "IDEMPOTENT — повторный apply без изменений"
# Тот же run_id что применялся в фазе 1 → apply должен вернуть 0 changed
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$RUN_ID"; then
if grep -q '0 added, 0 changed, 0 destroyed' /tmp/vm_tf_apply.log; then
pass "2.1 повторный apply → 0 added, 0 changed, 0 destroyed"
else
fail "2.1 повторный apply изменил ресурсы (ожидали 0 changed)"
grep -E 'added|changed|destroyed' /tmp/vm_tf_apply.log | tail -3 | while read -r line; do
info " $line"
done
fi
else
fail "2.1 повторный apply упал"
fi
phase_result "IDEMPOTENT" "PASS"
}
# ══════════════════════════════════════════════════════════════════════════════
# ФАЗА 3: PARTIAL_DISABLE — выключить nginx + docker
# Используем -var install_nginx=false -var install_docker=false
# terraform.tfvars по-прежнему НЕ ТРОГАЕМ.
# ══════════════════════════════════════════════════════════════════════════════
phase_3_partial_disable() {
phase_header 3 "PARTIAL_DISABLE — выключить nginx + docker"
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=false" \
-var "install_docker=false" \
-var "install_run_id=$RUN_ID"; then
pass "3.1 apply с partial disable завершился"
else
fail "3.1 apply с partial disable упал"
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 — включить обратно всё
# Инкрементируем run_id чтобы jobs пересоздались.
# ══════════════════════════════════════════════════════════════════════════════
phase_4_partial_enable() {
phase_header 4 "PARTIAL_ENABLE — включить обратно всё"
local rid
rid=$(next_run_id)
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid"; 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
phase_result "PARTIAL_ENABLE" "PASS"
}
# ══════════════════════════════════════════════════════════════════════════════
# ФАЗА 5: REORDER_PACKAGES — изменить порядок и состав base_packages
# Через -var 'base_packages=["htop","jq"]' — terraform.tfvars не трогаем.
# ══════════════════════════════════════════════════════════════════════════════
phase_5_reorder() {
phase_header 5 "REORDER_PACKAGES — изменить порядок и состав пакетов"
local rid
rid=$(next_run_id)
# Сокращённый набор пакетов через -var
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid" \
-var 'base_packages=["htop","jq"]'; then
pass "5.1 apply с изменённым набором пакетов завершился"
else
fail "5.1 apply с изменённым набором пакетов упал"
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 -qE '"status"|ok|already'; then
pass "5.2 install_packages вернул ожидаемый результат"
else
fail "5.2 install_packages output неожиданный: $pkg_out"
fi
# Вернуть полный набор пакетов
rid=$(next_run_id)
tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid" \
|| warn "5.3 восстановление полного набора не удалось"
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 rid
rid=$(next_run_id)
info "запускаю переустановку через sless_job..."
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid"; then
pass "6.4 apply после purge завершился"
else
fail "6.4 apply после purge упал"
phase_result "MANUAL_PURGE" "FAIL"
return 1
fi
# Ждать пока sless_job отработает: docker самый долгий (k8s Job + apt-install).
# vm_wait_binary полит каждые 15s до 180s вместо sleep 5.
if vm_wait_binary "$ip" "docker" 180; then
pass "6.5 docker установлен заново"
else
fail "6.5 docker НЕ установлен после re-apply (таймаут 180s)"
fi
if vm_wait_binary "$ip" "nginx" 120; then
pass "6.6 nginx установлен заново"
else
fail "6.6 nginx НЕ установлен после re-apply (таймаут 120s)"
fi
if vm_check_binary "$ip" "jq"; then
pass "6.7 jq установлен заново"
else
fail "6.7 jq НЕ установлен после re-apply"
fi
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 просыпается
# Используем текущий run_id — terraform.tfvars НЕ ТРОГАЕМ.
# ══════════════════════════════════════════════════════════════════════════════
phase_8_resurrect() {
phase_header 8 "RESURRECT — apply после destroy"
local rid
rid=$(next_run_id)
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid"; 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
phase_result "RESURRECT" "PASS"
}
# ══════════════════════════════════════════════════════════════════════════════
# ФАЗА 9: STRESS_CYCLES — N destroy/apply циклов подряд
# ══════════════════════════════════════════════════════════════════════════════
phase_9_stress() {
phase_header 9 "STRESS_CYCLES — $STRESS_CYCLES циклов destroy/apply"
local i rid
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 упал"
# Попробовать восстановиться
rid=$(next_run_id)
tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid" || true
continue
fi
rid=$(next_run_id)
info "[$i] apply (run_id=$rid)..."
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid"; 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)"
# Попробовать восстановить (через -var, БЕЗ изменения файлов)
local rid
rid=$(next_run_id)
info "пытаюсь восстановить baseline..."
tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$rid" || warn "восстановление не удалось"
fi
# Idempotency финально: повторный apply → 0 changed
if tf_apply \
-var "install_packages=true" \
-var "install_nginx=true" \
-var "install_docker=true" \
-var "install_run_id=$RUN_ID"; then
if grep -q '0 added, 0 changed, 0 destroyed' /tmp/vm_tf_apply.log; then
pass "10.2 финальный apply → 0 added, 0 changed, 0 destroyed"
else
fail "10.2 финальный apply изменил ресурсы (ожидали 0 changed)"
grep -E 'added|changed|destroyed' /tmp/vm_tf_apply.log | tail -3 | while read -r line; do
info " $line"
done
fi
else
fail "10.2 финальный apply упал"
fi
# VM доступна
local ip
ip=$(get_vm_ip)
if [[ -n "$ip" ]] && vm_alive "$ip"; then
pass "10.3 VM $ip доступна по SSH"
# Полная проверка пакетов
for pkg in jq htop unzip; do
if vm_check_package "$ip" "$pkg"; then
pass "10.4 пакет $pkg установлен"
else
fail "10.4 пакет $pkg НЕ установлен"
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
# nginx service активен (не просто бинарник, а именно демон)
if vm_ssh "$ip" "systemctl is-active nginx 2>/dev/null" 2>/dev/null | grep -q '^active$'; then
pass "10.7 nginx service активен (systemctl)"
else
fail "10.7 nginx service не активен"
fi
# docker daemon активен
if vm_ssh "$ip" "systemctl is-active docker 2>/dev/null" 2>/dev/null | grep -q '^active$'; then
pass "10.8 docker daemon активен (systemctl)"
else
fail "10.8 docker daemon не активен"
fi
# HTTP probe: nginx отвечает на localhost:80
local http_code
http_code=$(vm_ssh "$ip" "curl -sS -o /dev/null -w '%{http_code}' http://localhost 2>/dev/null" 2>/dev/null)
if [[ "$http_code" == "200" ]]; then
pass "10.9 nginx отвечает HTTP 200"
else
fail "10.9 nginx не отвечает HTTP 200 (code='$http_code')"
fi
# python3 доступен
local py_ver
py_ver=$(vm_ssh "$ip" "python3 --version 2>&1" 2>/dev/null)
if echo "$py_ver" | grep -q 'Python 3'; then
pass "10.10 python3 доступен ($py_ver)"
else
fail "10.10 python3 недоступен"
fi
# docker smoke: запустить контейнер и проверить вывод
if vm_ssh "$ip" "docker run --rm hello-world 2>&1 | grep -q 'Hello from Docker'" 2>/dev/null; then
pass "10.11 docker run hello-world → успешно"
else
fail "10.11 docker run hello-world → не прошёл"
fi
# disk space: убедиться что на VM есть место (>500MB free)
local free_mb
free_mb=$(vm_ssh "$ip" "df -m / 2>/dev/null | awk 'NR==2{print \$4}'" 2>/dev/null)
if [[ -n "$free_mb" && "$free_mb" -gt 500 ]]; then
pass "10.12 свободное место на / = ${free_mb}MB (>500MB)"
else
fail "10.12 мало места на / = ${free_mb}MB (ожидали >500MB)"
fi
else
fail "10.3 VM не доступна по SSH"
fi
phase_result "FINAL_SANITY" "PASS"
}
# ══════════════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════════════
echo -e "${BOLD}${CYAN}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ VM STRESS TEST v2 (READ-ONLY) — examples/VM ║"
echo "$(date '+%Y-%m-%d %H:%M:%S')"
echo "║ Начальный run_id: $RUN_ID"
echo "║ Циклов stress: $STRESS_CYCLES"
echo "║ terraform.tfvars НЕ МОДИФИЦИРУЕТСЯ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${RESET}"
# Проверить что мы в правильной директории
if [[ ! -f "terraform.tfvars" ]]; then
echo -e "${RED}ОШИБКА: terraform.tfvars не найден. Запускайте из examples/VM/${RESET}"
exit 1
fi
if [[ ! -f "$VM_KEY" ]]; then
echo -e "${RED}ОШИБКА: $VM_KEY не найден${RESET}"
exit 1
fi
# ⛔ ПРОВЕРКА ЦЕЛОСТНОСТИ: terraform.tfvars НЕ ДОЛЖЕН БЫТЬ МОДИФИЦИРОВАН
# Сохраняем md5 до запуска и проверяем после каждой фазы
TFVARS_MD5=$(md5sum terraform.tfvars | awk '{print $1}')
info "md5 terraform.tfvars = $TFVARS_MD5 (будет проверяться после каждой фазы)"
check_tfvars_integrity() {
local current_md5
current_md5=$(md5sum terraform.tfvars | awk '{print $1}')
if [[ "$current_md5" != "$TFVARS_MD5" ]]; then
echo -e "${RED}⛔⛔⛔ КРИТИЧЕСКАЯ ОШИБКА: terraform.tfvars был изменён! ⛔⛔⛔${RESET}"
echo -e "${RED}Ожидали md5: $TFVARS_MD5${RESET}"
echo -e "${RED}Текущий md5: $current_md5${RESET}"
echo -e "${RED}АВАРИЙНАЯ ОСТАНОВКА ТЕСТА${RESET}"
exit 99
fi
}
# Запуск фаз с проверкой целостности после каждой
phase_1_baseline; check_tfvars_integrity
phase_2_idempotent; check_tfvars_integrity
phase_3_partial_disable; check_tfvars_integrity
phase_4_partial_enable; check_tfvars_integrity
phase_5_reorder; check_tfvars_integrity
phase_6_manual_purge; check_tfvars_integrity
if [[ "$SKIP_DESTROY" == "1" ]]; then
skip "фазы 7-9 пропущены (SKIP_DESTROY=1)"
PHASE_RESULTS+=("DESTROY:SKIP" "RESURRECT:SKIP" "STRESS_CYCLES:SKIP")
else
phase_7_destroy; check_tfvars_integrity
phase_8_resurrect; check_tfvars_integrity
phase_9_stress; check_tfvars_integrity
fi
phase_10_final; check_tfvars_integrity
# ── ИТОГОВАЯ СВОДКА ──────────────────────────────────────────────────────────
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 -e "${BOLD} Финальный run_id: $RUN_ID${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
check_tfvars_integrity
info "terraform.tfvars НЕ БЫЛ ИЗМЕНЁН (md5 совпадает)"
# 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