# Архитектура 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/ │ │ ├── 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/`** — операции + autotest-инстансы: - Cloud-first: фильтр по префиксу `autotest-` - Tracker-fallback: статус "creating" для ещё невидимых ### `api_test.py` — основной модуль операций - **`GET /api/params/[?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/`** — поллинг: 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-экранирование (`"` → `"`, `&` → `&`, `<` → `<`) - `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: `&` → `&`, `"` → `"`, `<` → `<` Применяется ко ВСЕМ данным из 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 или общая таблица в БД для статусов операций.