fix(vm_stress_test): make read-only workflow, add instructions and docs
This commit is contained in:
@@ -2,6 +2,70 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-30 — Переход на READ-ONLY подход в тестах (v2)
|
||||
|
||||
### Решение
|
||||
Полностью переписан `vm_stress_test.sh`. Убраны все функции записи в файлы
|
||||
(`write_tfvars`, `backup_tfvars`, `restore_tfvars`). Переопределения переменных
|
||||
теперь через `-var` в terraform CLI. Добавлена проверка md5sum terraform.tfvars.
|
||||
|
||||
### Почему
|
||||
Функция `write_tfvars()` в v1 уничтожила `terraform.tfvars`, потеряв JWT-токен
|
||||
`api_token`. Пайплайн `grep | cut | xargs | sed` не смог корректно обработать
|
||||
JWT строку длиной 1200+ символов. Восстановление потребовало ручного вмешательства.
|
||||
|
||||
### Ключевые принципы v2:
|
||||
- Скрипт **НИКОГДА** не пишет в файлы (read-only)
|
||||
- Все переопределения — через `terraform apply -var "key=value"`
|
||||
- md5sum проверка после каждой фазы, аварийный стоп при изменении
|
||||
- `terraform.tfvars` содержит секреты (api_token) → нельзя трогать
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-30 — Автономный тестовый фреймворк для VM
|
||||
|
||||
### Решение
|
||||
Внедрить bash-скрипт `vm_stress_test.sh` как основной инструмент для долгого, самовосстанавливающегося тестирования примера `examples/VM`.
|
||||
|
||||
### Почему
|
||||
- Предыдущие ручные тесты подтвердили корректность логики, но для выявления редких race conditions и обеспечения преемственности между разными сессиями агентов нужен воспроизводимый сценарий.
|
||||
- Скрипт инкапсулирует все "знания" о VM (IP, ключи, логика очистки `apt`), позволяя любому агенту запустить тест одной командой.
|
||||
- Использование `timeout` на уровне команд `terraform` и `ssh` внутри скрипта предотвращает зависание автоматизации.
|
||||
|
||||
## 2026-03-29 — Матрица тестов VM example подтверждена прогоном
|
||||
|
||||
### Решение
|
||||
|
||||
Оставить текущую модель тестирования [examples/VM](examples/VM) как комбинацию из ручного cleanup, destroy/apply цикла, частичного отключения job-ресурсов и короткого stress loop.
|
||||
|
||||
### Почему
|
||||
|
||||
- Эта матрица проверяет и lifecycle VM, и идемпотентность job-ресурсов, и реакцию на изменение количества/порядка установок.
|
||||
- Отдельный destroy/apply прогон подтвердил suspend/wake поведение без необходимости писать новый тестовый фреймворк.
|
||||
- Stress loop из двух циклов дал полезную нагрузку без чрезмерного времени прогона.
|
||||
|
||||
## 2026-03-29 — Матрица тестов для VM example
|
||||
|
||||
### Решение
|
||||
|
||||
Для проверки поведения [examples/VM](examples/VM) использовать не один прогон, а набор сценариев:
|
||||
|
||||
1. обычный `apply` как базовый контроль;
|
||||
2. удаление всего установленного ПО внутри ВМ перед `destroy`;
|
||||
3. `destroy` с проверкой перехода ВМ в `suspend`;
|
||||
4. повторный `apply` с проверкой wake-up и повторной установки;
|
||||
5. изменение количества и порядка установок;
|
||||
6. стресс-прогоны с несколькими повторениями.
|
||||
|
||||
### Почему так
|
||||
|
||||
- Один проход не показывает идемпотентность и не ловит проблемы порядка ресурсов.
|
||||
- Сценарий с `destroy` проверяет, что инфраструктура не удаляет ВМ физически, а переводит её в `suspend`.
|
||||
- Повторный `apply` после `suspend` проверяет восстановление состояния без ручного вмешательства.
|
||||
- Перестановки и изменение количества установок нужны, чтобы проверить устойчивость к дрейфу и к разным графам зависимостей.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-21 — Оценка трудозатрат на проект
|
||||
|
||||
| Компонент | Оценка |
|
||||
|
||||
+87
-58
@@ -1,9 +1,95 @@
|
||||
# Прогресс разработки
|
||||
|
||||
Последнее обновление: 2026-03-23
|
||||
Последнее обновление: 2026-03-30
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-30 — Автоматизация VM stress-тестирования
|
||||
|
||||
### v2 (READ-ONLY) — переписан после инцидента с потерей tfvars
|
||||
|
||||
**Инцидент:** v1 скрипта содержал `write_tfvars()` которая перезаписывала `terraform.tfvars`.
|
||||
Функция использовала `grep | cut | xargs | sed` для извлечения JWT-токена api_token.
|
||||
Пайплайн не справился с длинным JWT (1200+ символов) и токен был потерян.
|
||||
Это сломало весь terraform и потребовало ручного восстановления.
|
||||
|
||||
**Решение (v2):**
|
||||
- Полностью убраны `write_tfvars()`, `backup_tfvars()`, `restore_tfvars()`, `ensure_baseline()`
|
||||
- Все переопределения переменных — через `-var` в terraform CLI
|
||||
- Файл `terraform.tfvars` НИКОГДА не модифицируется
|
||||
- Добавлена проверка md5sum terraform.tfvars после каждой фазы
|
||||
- Если файл изменился — АВАРИЙНАЯ ОСТАНОВКА (exit 99)
|
||||
|
||||
### Что сделано
|
||||
- Переписан скрипт [examples/VM/vm_stress_test.sh](examples/VM/vm_stress_test.sh) (v2, 847 строк, 10 фаз)
|
||||
- Создана инструкция [examples/VM/VM_TEST_README.md](examples/VM/VM_TEST_README.md)
|
||||
- Старая версия сохранена как `.vm_stress_test.sh.OLD`
|
||||
|
||||
### Сценарии (10 фаз):
|
||||
1. **Baseline**: apply с полным набором (packages+nginx+docker)
|
||||
2. **Idempotent**: plan → "No changes"
|
||||
3. **Partial Disable**: выключить nginx+docker через `-var`
|
||||
4. **Partial Enable**: включить обратно
|
||||
5. **Reorder Packages**: изменить base_packages через `-var`
|
||||
6. **Manual Purge**: удалить пакеты с VM по SSH → переустановить
|
||||
7. **Destroy**: terraform destroy → VM suspend
|
||||
8. **Resurrect**: apply после destroy
|
||||
9. **Stress Cycles**: N циклов destroy/apply
|
||||
10. **Final Sanity**: проверка VM + пакеты + plan
|
||||
|
||||
### Текущий статус
|
||||
- Скрипт проверен на синтаксис (`bash -n`): OK
|
||||
- Ожидает запуска первой итерации
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-29 — VM stress/chaos matrix: результаты
|
||||
|
||||
### Что прогнали
|
||||
|
||||
1. Ручной cleanup на целевой VM: удаление `jq`, `python3-pip`, `htop`, `unzip`, `nginx`, `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-compose-plugin`.
|
||||
2. `terraform destroy` на [examples/VM](examples/VM).
|
||||
3. Проверка SSH-доступа после destroy.
|
||||
4. `terraform apply` после destroy с восстановлением VM и jobs.
|
||||
5. Повторный `terraform apply` без изменений для проверки идемпотентности.
|
||||
6. Частичный сценарий с изменением количества ресурсов: `install_nginx=false`, `install_docker=false`, `base_packages=["htop", "jq"]`, `install_run_id=7`.
|
||||
7. Возврат к полной матрице с другим порядком пакетов: `base_packages=["unzip", "python3-pip", "jq", "htop"]`, `install_run_id=8`.
|
||||
8. Stress loop из 2 подряд идущих циклов `destroy -> apply`.
|
||||
|
||||
### Результаты
|
||||
|
||||
- Ручной cleanup на VM прошёл: пакеты и бинарники `docker`/`nginx` исчезли.
|
||||
- `terraform destroy` завершился успешно и вывел `Destroy complete! Resources: 5 destroyed.`
|
||||
- SSH после destroy не поднялся и ушёл в `Connection timed out`, что соответствует suspend-поведению.
|
||||
- `terraform apply` после destroy восстановил `vApp + VM + 3 job` и вернул прежние IDs ресурсов.
|
||||
- Повторный `apply` без изменений дал `No changes`.
|
||||
- Частичный сценарий с двумя jobs (`install_packages` only) прошёл: лишние job-ресурсы были уничтожены, `install_packages` пересоздан.
|
||||
- Полная матрица с перестановкой пакетов прошла: `install_packages`, `install_nginx`, `install_docker` восстановились.
|
||||
- Stress loop из 2 циклов `destroy -> apply` завершился без ошибок.
|
||||
|
||||
### Наблюдения
|
||||
|
||||
- `install_packages_result` отражает уже установленные пакеты на VM; после частичного сценария он вернул только текущий состав списка из `base_packages`.
|
||||
- `install_nginx_result` и `install_docker_result` при повторном применении в полном состоянии показывают `already_installed`, что подтверждает идемпотентность джобов.
|
||||
- IDs `vapp_id` и `vm_id` остались прежними после destroy/apply, что согласуется с adopt/suspend моделью.
|
||||
|
||||
## 2026-03-29 — VM stress/chaos matrix: destroy, suspend, reapply, reorder
|
||||
|
||||
### Что планируется
|
||||
|
||||
- Прогнать серию разных тестов на [examples/VM](examples/VM) через `terraform` на удалённой VM.
|
||||
- Проверить сценарий с удалением всего установленного ПО внутри ВМ, затем `destroy`, после чего убедиться, что ВМ уходит в `suspend`, а не удаляется.
|
||||
- Проверить `apply` после `destroy`: ВМ должна проснуться, а приложения должны установиться заново.
|
||||
- Прогнать вариации порядка и количества установок, чтобы увидеть поведение при перестановках ресурсов и изменении состава.
|
||||
- Отдельно запустить стресс-прогоны и документировать все результаты, включая ошибки и нестабильности.
|
||||
|
||||
### Что будет фиксироваться
|
||||
|
||||
- Команды и их итоговый статус.
|
||||
- Любые расхождения между планом и фактическим состоянием ВМ.
|
||||
- Ошибки `terraform`, `ssh` и установки пакетов.
|
||||
- Поведение suspend/resume и повторной установки после `apply`.
|
||||
|
||||
## 2026-03-28 — Handoff: функции-джобы для установки ПО в ВМ (курс на PostgreSQL)
|
||||
|
||||
### Контекст и цель
|
||||
@@ -1417,60 +1503,3 @@ G15 перезапущен → **21/21 PASS ✅**
|
||||
### Версия оператора
|
||||
`v0.1.51` — задеплоен, работает
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Шаг 6: VM provisioning через sless_job (2026-03-29)
|
||||
|
||||
### Цель
|
||||
Установить пакеты, nginx и docker на свежую Nubes VM через sless_job.
|
||||
|
||||
### Что реализовано
|
||||
- `examples/VM/sless.tf` — три `sless_job` ресурса с `depends_on` цепочкой:
|
||||
- `vm-install-packages` → jq, python3-pip, htop, unzip
|
||||
- `vm-install-nginx` → nginx (depends_on install-packages)
|
||||
- `vm-install-docker` → Docker CE 29.3.1 + Compose v5.1.1 (depends_on install-nginx)
|
||||
- `examples/VM/variables.tf` — переменная `install_run_id` для управления rebuildом
|
||||
|
||||
---
|
||||
|
||||
## Шаг 6: VM provisioning через sless_job (2026-03-29)
|
||||
|
||||
### Цель
|
||||
Установить пакеты, nginx и docker на свежую Nubes VM через sless_job.
|
||||
|
||||
### Что реализовано
|
||||
- `examples/VM/sless.tf` — три `sless_job` ресурса с `depends_on` цепочкой:
|
||||
- `vm-install-packages` → jq, python3-pip, htop, unzip
|
||||
- `vm-install-nginx` → nginx (depends_on install-packages)
|
||||
- `vm-install-docker` → Docker CE 29.3.1 + Compose v5.1.1 (depends_on install-nginx)
|
||||
- `examples/VM/variables.tf` — переменная `install_run_id` для управления rebuild'ом
|
||||
- `examples/VM/outputs.tf` — вывод результатов job'ов
|
||||
- `examples/VM/functions/install-packages/handler.py` — SSH установка пакетов
|
||||
- `examples/VM/functions/install-nginx/handler.py` — SSH установка nginx
|
||||
- `examples/VM/functions/install-docker/handler.py` — SSH установка Docker CE
|
||||
|
||||
### Баги найдены и исправлены
|
||||
|
||||
#### Баг 1: Ubuntu 22.04 — cloud-init держит apt lock при старте VM
|
||||
- **Симптом**: `E: Could not get lock /var/lib/dpkg/lock-frontend (held by process 2191)`
|
||||
- **Причина**: `unattended-upgrades` держит lock через cloud-init 15-20 мин после старта VM
|
||||
- **Фикс**: `_wait_apt_lock()` начинает с `cloud-init status --wait` (timeout 300s), затем `systemctl mask` сервисов авто-обновления
|
||||
|
||||
#### Баг 2: Оператор запускает СТАРЫЙ образ после upload
|
||||
- **Симптом**: `image already exists in registry, skipping build` — новый код не применялся
|
||||
- **Причина**: Go DEFLATE создавал идентичные байты zip для разных версий handler.py, sha256(zip) не менялся
|
||||
- **Фикс**: изменение requirements.txt гарантирует уникальный zip hash
|
||||
|
||||
#### Баг 3: WaitJobDone мгновенно возвращал ошибку на старый Failed статус
|
||||
- **Симптом**: terraform apply падает мгновенно без ожидания нового build
|
||||
- **Причина**: FunctionJob CRD хранил phase=Failed от предыдущего запуска
|
||||
- **Воркараунд**: удалить FunctionJob CRD перед apply
|
||||
|
||||
### Результат
|
||||
- vm-install-packages: status=ok, installed=[jq, python3-pip, htop, unzip]
|
||||
- vm-install-nginx: status=already_installed, version=1.18.0, http_check=200
|
||||
- vm-install-docker: status=ok, docker=29.3.1, compose=v5.1.1
|
||||
|
||||
### nubes провайдер
|
||||
Обновлён с 5.0.49 до 5.0.51 (fix: findInstanceUidByDisplayNameRefSvc возвращал deleted instance UUID)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# VM Stress Test — Инструкция по запуску
|
||||
# 2026-03-30
|
||||
|
||||
## ⛔⛔⛔ КРИТИЧЕСКИЕ ПРАВИЛА ⛔⛔⛔
|
||||
|
||||
### ЗАПРЕЩЕНО (без исключений):
|
||||
- **НЕ РЕДАКТИРОВАТЬ** `terraform.tfvars` — там JWT-токен, потеря = катастрофа
|
||||
- **НЕ РЕДАКТИРОВАТЬ** `*.tf` файлы
|
||||
- **НЕ РЕДАКТИРОВАТЬ** `vm_stress_test.sh`
|
||||
- **НЕ ЗАПУСКАТЬ** `terraform` напрямую — только через скрипт
|
||||
- **НЕ СОЗДАВАТЬ** новые файлы в этой директории
|
||||
- **НЕ ДЕЛАТЬ** `sed`, `awk`, `cat >`, `tee` в terraform.tfvars
|
||||
|
||||
### ПОЧЕМУ:
|
||||
Предыдущая версия скрипта содержала функцию `write_tfvars()` которая
|
||||
перезаписывала `terraform.tfvars`. В процессе перезаписи был потерян
|
||||
JWT-токен `api_token` (1200+ символов). Это привело к полному отказу
|
||||
terraform и потере рабочего состояния. Восстановление заняло час.
|
||||
|
||||
### КАК РАБОТАЕТ НОВЫЙ СКРИПТ:
|
||||
Переменные переопределяются через `-var` в terraform CLI.
|
||||
Файл `terraform.tfvars` читается terraform автоматически,
|
||||
но **НИКОГДА не перезаписывается** скриптом.
|
||||
|
||||
После каждой фазы проверяется md5sum terraform.tfvars.
|
||||
Если файл изменился — **АВАРИЙНАЯ ОСТАНОВКА** (exit code 99).
|
||||
|
||||
---
|
||||
|
||||
## Запуск
|
||||
|
||||
### На VM (naeel@5.172.178.213):
|
||||
|
||||
```bash
|
||||
cd ~/terra/sless/examples/VM
|
||||
bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress_$(date +%Y%m%d_%H%M).log
|
||||
```
|
||||
|
||||
### Быстрый прогон (без destroy/resurrect — фазы 7-9 пропускаются):
|
||||
|
||||
```bash
|
||||
SKIP_DESTROY=1 bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log
|
||||
```
|
||||
|
||||
### Количество stress-циклов (default: 2):
|
||||
|
||||
```bash
|
||||
STRESS_CYCLES=3 bash vm_stress_test.sh 2>&1 | tee /tmp/vm_stress.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Анализ результатов
|
||||
|
||||
### Быстрый обзор:
|
||||
```bash
|
||||
grep -E '\[(PASS|FAIL|SKIP)\]' /tmp/vm_stress.log
|
||||
```
|
||||
|
||||
### Только ошибки:
|
||||
```bash
|
||||
grep '\[FAIL\]' /tmp/vm_stress.log
|
||||
```
|
||||
|
||||
### Итоговая сводка — последние 20 строк лога:
|
||||
```bash
|
||||
tail -20 /tmp/vm_stress.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Фазы теста
|
||||
|
||||
| # | Имя | Что делает |
|
||||
|---|-----------------|---------------------------------------------------|
|
||||
| 1 | BASELINE | apply с полным набором (packages+nginx+docker) |
|
||||
| 2 | IDEMPOTENT | plan → "No changes" (проверка идемпотентности) |
|
||||
| 3 | PARTIAL_DISABLE | отключить nginx + docker через -var |
|
||||
| 4 | PARTIAL_ENABLE | включить обратно nginx + docker |
|
||||
| 5 | REORDER_PACKAGES| изменить набор base_packages через -var |
|
||||
| 6 | MANUAL_PURGE | удалить пакеты с VM по SSH → переустановить |
|
||||
| 7 | DESTROY | terraform destroy → VM в suspend |
|
||||
| 8 | RESURRECT | apply после destroy → VM просыпается |
|
||||
| 9 | STRESS_CYCLES | N циклов destroy/apply подряд |
|
||||
|10 | FINAL_SANITY | финальная проверка VM + пакеты + plan |
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние (baseline)
|
||||
|
||||
5 ресурсов в state:
|
||||
- `nubes_vapp.vapp`
|
||||
- `nubes_vc_vm_v3.vm`
|
||||
- `sless_job.install_packages[0]`
|
||||
- `sless_job.install_nginx[0]`
|
||||
- `sless_job.install_docker[0]`
|
||||
|
||||
---
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Значение |
|
||||
|------|---------------------------------------------|
|
||||
| 0 | Все тесты PASS |
|
||||
| 1 | Есть FAIL (см. лог) |
|
||||
| 99 | terraform.tfvars был изменён — АВАРИЙНЫЙ СТОП |
|
||||
Executable
+847
@@ -0,0 +1,847 @@
|
||||
#!/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_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 — повторный plan без изменений
|
||||
# Передаём тот же run_id → ничего не изменилось → "No changes".
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
phase_2_idempotent() {
|
||||
phase_header 2 "IDEMPOTENT — повторный plan без изменений"
|
||||
|
||||
# Тот же run_id что в предыдущей фазе → "No changes"
|
||||
if tf_plan_no_changes \
|
||||
-var "install_packages=true" \
|
||||
-var "install_nginx=true" \
|
||||
-var "install_docker=true" \
|
||||
-var "install_run_id=$RUN_ID"; 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
|
||||
# Используем -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
|
||||
|
||||
# Подождать и проверить что пакеты вернулись
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# Plan = no changes (с тем же run_id что и последний apply)
|
||||
if tf_plan_no_changes \
|
||||
-var "install_packages=true" \
|
||||
-var "install_nginx=true" \
|
||||
-var "install_docker=true" \
|
||||
-var "install_run_id=$RUN_ID"; 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"
|
||||
|
||||
# Полная проверка пакетов
|
||||
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
|
||||
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
|
||||
Reference in New Issue
Block a user