932 lines
42 KiB
Bash
Executable File
932 lines
42 KiB
Bash
Executable File
#!/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
|
||
# sless_job — эфемерный ресурс, каждый apply пересоздаёт job-ресурсы (add+destroy).
|
||
# Идемпотентность = "0 changed" (ноль in-place изменений), а не "0 add/destroy".
|
||
# VM и vApp не должны изменяться никогда.
|
||
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 changed,' /tmp/vm_tf_apply.log; then
|
||
pass "2.1 повторный apply → 0 changed (sless_job пересоздан — ожидаемо)"
|
||
grep -E 'Resources:' /tmp/vm_tf_apply.log | tail -1 | while read -r line; do
|
||
info " $line"
|
||
done
|
||
else
|
||
fail "2.1 повторный apply изменил persistent ресурсы (ожидали 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 ~200MB).
|
||
# docker-ce требует до 5 минут на первой установке — ставим 360s.
|
||
if vm_wait_binary "$ip" "docker" 360; then
|
||
pass "6.5 docker установлен заново"
|
||
else
|
||
fail "6.5 docker НЕ установлен после re-apply (таймаут 360s)"
|
||
fi
|
||
|
||
if vm_wait_binary "$ip" "nginx" 240; then
|
||
pass "6.6 nginx установлен заново"
|
||
else
|
||
fail "6.6 nginx НЕ установлен после re-apply (таймаут 240s)"
|
||
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.
|
||
# sless_job пересоздаются (add+destroy) — это нормально для job-ресурса.
|
||
# Проверяем только "0 changed" — VM и vApp не должны изменяться.
|
||
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 changed,' /tmp/vm_tf_apply.log; then
|
||
pass "10.2 финальный apply → 0 changed (sless_job пересоздан — ожидаемо)"
|
||
grep -E 'Resources:' /tmp/vm_tf_apply.log | tail -1 | while read -r line; do
|
||
info " $line"
|
||
done
|
||
else
|
||
fail "10.2 финальный apply изменил persistent ресурсы (ожидали 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
|