Files
autotest/DOCS/ARCHITECTURE.md

12 KiB
Raw Permalink Blame History

Архитектура 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", dictjson.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
    • С instanceUidget_params_with_current_values()
  • POST /api/test — запуск операции:
    • CREATE: POST /instancesinstanceUidPOST /instanceOperationsopUidtracker_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_resultsthreading.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 старыми данными от async loadStepParams().

Известные ограничения

  • _op_results in-memory на воркер — не shared между gunicorn-воркерами. При отсутствии stickiness статус может читаться из API fallback вместо кеша. Решение (отложено): Redis или общая таблица в БД для статусов операций.