853 lines
36 KiB
Bash
Executable File
853 lines
36 KiB
Bash
Executable File
#!/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
|