12 KiB
Архитектура 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
Ключевые принципы
- Источник правды — Nubes API. Все данные (инстансы, параметры, статусы) получаются только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера).
- Разделение CREATE и non-CREATE. Это два принципиально разных потока.
- Multi-worker safety. Все общие ресурсы (/tmp файлы) защищены
fcntl.flock. - Мелкие функции. Каждое действие — отдельная функция.
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 стенды, возвращает рабочий URLstand_name(endpoint)— "dev"/"test" по URLcreate_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):
GET /instances/{uid}→state.params(текущие значения:{"whereFail":"1",...})GET /instanceOperations/default/{opId}→ шаблон (коды, типы, valueList, dataDescriptor)- Слияние:
defaultValue = state.params["код"] ?? template.defaultValue - Приведение типов:
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→ сохранить токен в cookieaction=clear→ удалить cookie
GET /api/operations/<svc_id>— операции + autotest-инстансы:- Cloud-first: фильтр по префиксу
autotest- - Tracker-fallback: статус "creating" для ещё невидимых
- Cloud-first: фильтр по префиксу
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 таймаут)
- CREATE:
GET /api/test/status/<op_uid>— поллинг: in-memory_op_results→ прямой APIGET /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]— безопасно при конкурентном доступе.- Partial unique index для сценариев —
CREATE UNIQUE INDEX idx_one_running ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'. ДелаетINSERT INTO scenario_runs ... status='RUNNING'атомарной проверкой: вторая параллельная вставка получает unique violation → 409. Это заменило сломанную реализацию наpg_try_advisory_lock(v1.2.20), где lock брался на одном соединении, а unlock — на другом (из пула). - 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 старыми данными от asyncloadStepParams().
Известные ограничения
_op_resultsin-memory на воркер — не shared между gunicorn-воркерами. При отсутствии stickiness статус может читаться из API fallback вместо кеша. Решение (отложено): Redis или общая таблица в БД для статусов операций.