Files
autotest/DOCS/ARCHITECTURE.md
T

227 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура app-autotest — текущее состояние
v1.0.93, 28.07.2026
---
## 1. Обзор
Flask-приложение (один HTML-файл, без SPA-фреймворка) для ручного тестирования операций
сервисов облачной платформы Nubes. Работает через REST API Nubes, автоопределяет стенд
(dev/test) по JWT-токену.
**Деплой**: Nubes pythonk8s (gunicorn, несколько воркеров).
**Кластер**: `iot-naeel` (K8s resourceRealm, DEV-стенд)
**URL**: `https://atest.pythonk8s.dev.nubes.ru`
**Репозиторий**: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`
### Ключевые принципы
1. **Источник правды — Nubes API**. Все данные (инстансы, параметры, статусы) получаются
только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера).
2. **Разделение CREATE и non-CREATE**. Это два принципиально разных потока.
3. **Multi-worker safety**. Все общие ресурсы (/tmp файлы) защищены `fcntl.flock`.
4. **Мелкие функции**. Каждое действие — отдельная функция.
---
## 2. Структура файлов
```
app-autotest/
├── site/ ← НЕ пакет (без __init__.py)
│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
│ ├── runner.py ← [LEGACY] Раннер тестов
│ ├── config.yaml ← [LEGACY] Конфиг раннера
│ ├── api/
│ │ └── http_client.py ← HttpClient, автостенд
│ ├── operations/
│ │ ├── get_services.py ← get_services(), get_service_detail()
│ │ ├── get_instances.py ← get_instances() (пагинация), get_organization()
│ │ ├── get_params.py ← get_params_with_current_values() — слияние state.params + шаблон
│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json + flock
│ ├── routes/
│ │ ├── main.py ← GET/POST /, /api/operations/<svc_id>
│ │ ├── api.py ← [LEGACY] /api/run, /api/status, /api/config
│ │ └── api_test.py ← /api/test, /api/params, /api/log
│ ├── templates/
│ │ └── index.html ← Весь UI (Jinja2 + ванильный JS)
│ └── static/
│ ├── style.css
│ ├── logo.svg
│ └── favicon.svg
└── secrets/
├── dev.token
└── test.token
```
---
## 3. Бэкенд: HTTP-клиент и автостенд
### `http_client.py`
- `HttpClient(endpoint, token)` — обёртка над `requests.Session`:
- Заголовки: `Authorization: Bearer`, `User-Agent: Mozilla/5.0` (DDoS-Guard)
- `get(path)``raise_for_status()``.json()`
- `post(path, data)` → проверка `r.ok`, парсинг JSON, извлечение `Location`
- `detect_endpoint(token)` — пробует dev→test стенды, возвращает рабочий URL
- `stand_name(endpoint)` — "dev"/"test" по URL
- `create_client(token, fallback)` — HttpClient + endpoint
### `get_instances.py`
- `get_instances(client)``GET /instances` с пагинацией (pageSize=200). Остановка по `len(batch) < pageSize`.
- `get_organization(client)` — первый инстанс с `serviceId == 19`
### `get_services.py`
- `get_services(client)``GET /services` → список
- `get_service_detail(client, svc_id)``GET /services/{id}` → детали + операции
### `get_params.py` — КЛЮЧЕВОЙ модуль
`get_params_with_current_values(client, op_id, instance_uid)`:
1. `GET /instances/{uid}``state.params` (текущие значения: `{"whereFail":"1",...}`)
2. `GET /instanceOperations/default/{opId}` → шаблон (коды, типы, valueList, dataDescriptor)
3. Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
4. Приведение типов: `bool``"true"/"false"`, `dict``json.dumps()`
### `tracker.py`
- Файл: `/tmp/instances-{clientId}-{stand}.json` (изолирован по пользователю и стенду)
- Блокировка: `fcntl.flock(LOCK_EX | LOCK_NB)` + retry 2s
- `add(client_id, stand, uid, svc_id, name)`, `remove(...)`, `list_all(...)`
- Нужен ТОЛЬКО как fallback — когда инстанс только создан и ещё не в `/instances`
---
## 4. Бэкенд: Роуты
### `main.py`
- **`GET/POST /`** — главная: организация, инфраструктура (serviceId 2,12,19,21,22,25,26,29,110,150), сервисы
- `action=save` → сохранить токен в cookie
- `action=clear` → удалить cookie
- **`GET /api/operations/<svc_id>`** — операции + autotest-инстансы:
- Cloud-first: фильтр по префиксу `autotest-`
- Tracker-fallback: статус "creating" для ещё невидимых
### `api_test.py` — основной модуль операций
- **`GET /api/params/<op_id>[?instanceUid=xxx]`**:
- Без `instanceUid` → шаблонные `defaultValue`
- С `instanceUid``get_params_with_current_values()`
- **`POST /api/test`** — запуск операции:
- **CREATE**: `POST /instances``instanceUid``POST /instanceOperations``opUid``tracker_add` → params → `/run`
- **non-CREATE**: `POST /instanceOperations` → params → `/run` (redeploy: сразу `/run`)
- `displayName` = `_get_instance_display_name()` (из API, не UUID!)
- Фоновый поток: `_finish_op()` — поллинг до `dtFinish` (300s таймаут)
- **`GET /api/test/status/<op_uid>`** — поллинг: in-memory `_op_results` → прямой API
- **`GET /api/log`** — последние 200 строк из `/tmp/app-autotest.log`
### Логирование
`_log(msg)` в `api_test.py`:
- stdout (gunicorn) + `/tmp/app-autotest.log` (flock, общий для воркеров)
- Ротация при 512 КБ
- UI: скрытая панель, кнопка `log` (правый нижний угол)
---
## 5. Фронтенд
### Структура
Один HTML-файл, три колонки:
- Левая (280px): инфраструктура
- Средняя (240px): сервисы
- Правая (flex): инстансы + параметры + кнопка + этапы
### Глобальное состояние (JS)
```
svcInstances — кеш инстансов (из /api/operations)
selectedInst — UID выбранного инстанса
selectedOp — {opId, opName, svcId}
pollTimer — таймер поллинга
SVC_ID = 1 — фиксированный сервис (Болванка)
```
### Потоки операций
**CREATE**: `startCreate()``showParams(18,'create')` → форма → `executeOp(pp)` → POST /api/test → поллинг → refreshInstances
**MODIFY**: `toggleInstance()` → кнопки → `runOp('modify',opId)``showParams(opId,'modify')` → GET /api/params?instanceUid= → форма с ТЕКУЩИМИ значениями → валидация map → `executeOp(pp)` → POST /api/test → поллинг
**Без параметров** (suspend/delete/resume/redeploy): `runOp()` → confirm → `executeOp({})` → POST /api/test → поллинг
### Валидация JSON
- `_esc(s)` — HTML-экранирование (`"``&quot;`, `&``&amp;`, `<``&lt;`)
- `validateJson(el, quiet)``JSON.parse()` на `onblur` (красная рамка + текст)
- Batch-проверка перед отправкой — ошибка → запрос не уходит
---
## 6. Nubes API — используемые endpoint'ы
| Метод | Путь | Назначение |
|-------|------|-----------|
| GET | `/instances?pageSize=200&page=N` | Все инстансы (пагинация) |
| GET | `/instances/{uid}` | Детали + state.params |
| GET | `/services` | Список сервисов |
| GET | `/services/{id}` | Детали + операции |
| GET | `/instanceOperations/default/{opId}` | Шаблон параметров |
| POST | `/instances` | Создать инстанс |
| POST | `/instanceOperations` | Создать операцию |
| POST | `/instanceOperationCfsParams` | Установить параметр |
| POST | `/instanceOperations/{opUid}/run` | Запустить операцию |
| GET | `/instanceOperations/{opUid}?fields=...` | Поллинг статуса |
---
## 7. Ограничения платформы
- `site/` — НЕ пакет (без `__init__.py`), импорты без префикса `site.`
- `app.run(host="0.0.0.0", port=5000)`
- Gunicorn multi-worker → общие данные через файлы + flock
- `/tmp/` теряется при редеплое
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
---
## 8. Безопасность и конкуренция (аудит GPT-5.3-Codex, 2026-07-31)
Полный аудит 29 файлов (~6000 строк Python + vanilla JS). Исправлено в v1.2.19-v1.2.20.
### Защита от XSS (Frontend)
- **`_esc(s)` в utils.js** — HTML-escape: `&``&amp;`, `"``&quot;`, `<``&lt;`
Применяется ко ВСЕМ данным из API перед `innerHTML`.
- **params в scenario-list.js** — `_esc(k)+'='+_esc(v)` (было `k+'='+v` без экранирования).
- **JS injection в onclick** — имена сценариев с `'` теперь `replace(/'/g, "\\'")` перед
вставкой в JS-строку внутри HTML-атрибута.
### Защита от гонок (Backend)
- **`_op_results`** — `threading.Lock()` вокруг всех операций чтения/записи/cleanup.
`pop(k, None)` вместо `del dict[k]` — безопасно при конкурентном доступе.
- **Advisory lock сценариев** — `pg_try_advisory_lock(hashtext('scenario:{clientId}:{stand}'))`
атомарно проверяет и захватывает блокировку. `finally: unlock_scenario()` в `run_scenario()`.
- **Tracker** — `_atomic_update()`: read→mutate→write под одним `fcntl.flock`.
Исключает lost-update между `add()` и `remove()` из разных воркеров gunicorn.
### Защита от зависания (Frontend polling)
- **Счётчик ошибок** в `scenarioPollTimer` — после 5 последовательных ошибок:
`stopScenarioPoll()` + `busy=false` + сообщение об ошибке.
- **Generation token** в `scenario-form.js``_renderGen` предотвращает перезапись
нового DOM старыми данными от async `loadStepParams()`.
### Известные ограничения
- **`_op_results` in-memory на воркер** — не shared между gunicorn-воркерами.
При отсутствии stickiness статус может читаться из API fallback вместо кеша.
Решение (отложено): Redis или общая таблица в БД для статусов операций.