28 KiB
2026-07-31 — Сессия (v1.1.57 → v1.2.1)
Контекст
Обсуждение архитектуры: унификация ручного и сценарного режимов, гибкие ссылки на инстансы, новый UI редактора сценариев.
Agent consultations
Промпт для Opus
Составлен DOCS/opus-architecture-prompt.md — полное описание проекта, дублирование CREATE-флоу, ограничение instance_map, вопросы по архитектуре.
Ответ Opus — 19 уточняющих вопросов
A. Единый executor (operations/executor.py)
- Граница ответственности: executor делает всё до /run, поллинг — снаружи?
- Обработка ошибок: исключения с типом или dict {ok, error, failed_step}?
- Трекер: вызывать tracker_add внутри executor для create? Нужны ли сценарные инстансы в трекере?
- _finish_op: остаётся в api_test.py или выносим в общий модуль поллинга?
B. Гибкие ссылки на инстансы 5. Приоритет резолвинга: instance_uid > instance_ref > (для create — новый) 6. Где хранить output→uid: в памяти (instance_map) или в scenario_runs.instance_bindings? 7. Валидация _validate_steps: проверять ссылки на output, уникальность, обязательность 8. Обратная совместимость: поддерживать старый формат (service_id) или мигрировать?
C. Схема БД 9. Формат в steps JSONB без изменений схемы? (подтверждено) 10. Использовать существующую колонку scenario_runs.instance_bindings?
D. UI редактора 11. Модал vs inline — рекомендация: модал на весь экран 12. Переиспользовать renderParamRow из operations.js в общем модуле? 13. Pre-fill параметров: авто или по кнопке? 14. Формат параметров: символические имена в БД (как сейчас) или иное?
E. Оптимизация 15. Вынести _find_uid/_uid_from_location в общий utils 16. Механизм восстановления зависших scenario_runs? 17. Ограничение lock_check: только один RUNNING сценарий — осознанное? 18. _op_results в памяти → мигрировать в БД/Redis? 19. Приоритет: бэкенд или UI редактора в первую очередь?
Ключевые решения (отвечено DeepSeek)
A. Executor:
- A1. Граница: до /run включительно, без поллинга. Возвращает {instance_uid, op_uid, display_name}
- A2. Ошибки: dict {ok, error, failed_step}, не исключения
- A3. Трекер: да, tracker_add внутри executor для всех create
- A4. _finish_op: оставить в api_test.py, цикл поллинга → operations/poll.py
B. Ссылки:
- B5. Приоритет: instance_uid > instance_ref > новый create
- B6. Хранение: память + scenario_runs.instance_bindings
- B7. Валидация: проверять ссылки, уникальность, обязательность
- B8. Совместимость: оба формата, без миграции, fallback на service_id
C. БД:
- C9. Без изменений схемы
- C10. Использовать instance_bindings
D. UI:
- D11. Модал на весь экран
- D12. Общий модуль params-render.js
- D13. Авто pre-fill, все параметры с defaults
- D14. Символические имена в БД
E. Оптимизация:
- E15. _find_uid → api/utils.py
- E16. Startup check: TIMEOUT для зависших >1ч
- E17. lock_check оставить
- E18. _op_results не в scope
- E19. Порядок: executor → формат → api_test/scenario → UI
Финальный план (Опус, утверждён)
Сохранён в DOCS/opus-plan-2026-07-31.md. Ветка: opus-architecture-2026-07-31.
4 фазы, 13 шагов:
Фаза 1 — Общие модули:
api/utils.py(NEW) — find_uid(), uid_from_location()operations/poll.py(NEW) — poll_until_done()operations/executor.py(NEW) — execute_operation() до /run, без поллинга
Фаза 2 — Формат шагов:
routes/api_scenario_defs.py— _validate_steps с output/instance_ref/instance_uidoperations/scenario.py— резолвинг instance_uid > instance_ref > service_id
Фаза 3 — Миграция вызывающих:
routes/api_test.py— CMDB delete early return, остальное через executoroperations/scenario.py— через executor + poll_until_donedb/init_db.py— startup cleanup зависших scenario_runs
Фаза 4 — UI редактора:
static/js/params-render.js(NEW) — общий рендер параметровstatic/js/operations.js— использовать params-render.jsstatic/js/scenario-form.js— модальный редакторtemplates/index.html— разметка модала
CMDB delete
Жёсткое удаление через DELETE cmdb-api.deck.nubes.ru/instances/{uid} (без авторизации).
Нужно для недосозданных инстансов (not created). Остаётся в api_test.py, не в executor.
Добавлено после переписки с Георгием Родионовым 29.07.2026.
Реализация (v1.2.0 — v1.2.1)
v1.2.0 — Unified executor + flexible refs
Новые файлы: api/utils.py, operations/poll.py, operations/executor.py, static/js/params-render.js Изменено: api_test.py (через executor + poll), scenario.py (output/ref/bindings), api_scenario_defs.py (_validate_steps), init_db.py (cleanup), operations.js (→ params-render), index.html (load order)
- Дублирование CREATE-флоу устранено: ручной и сценарный → один executor
- instance_uid > instance_ref > service_id (гибкие ссылки)
- Общий поллинг poll_until_done()
- +254 / −277 строк (меньше кода)
v1.2.1 — Opus review fixes
Критическое: tracker_add внутрь executor (защита от сирот, A3) Исправлено: labelCls в renderMapFixedRow, descr с контекстом, порядок скриптов 5 файлов: executor.py, api_test.py, scenario.py, params-render.js, app.py
Проверка в поде (v1.2.1)
- Все 4 новых файла на месте
- 11 JS → 200, порядок правильный
- Schema OK, seed OK, API отвечает
Что дальше
Фаза 4 — модальный редактор сценариев (✅ v1.2.2-v1.2.4):
- ✅ Модал с дропдаунами сервисов/операций
- ✅ output/instance_ref с облачными инстансами
- ✅ Кнопки CRUD крупнее, справка «📖 Как заполнять»
v1.2.16 — instance_meta JSONB
После каждого прогона сохраняется полная информация об инстансе (GET /instances/{uid}). Все поля кроме instanceUid/displayName/svc/serviceId/explainedStatus.
Идеи на будущее (НЕ ДЕЛАТЬ, обдумать)
Context snapshot: сохранять снапшот ВСЕХ инстансов пользователя на момент запуска
(instanceUid, displayName, serviceId, svc, explainedStatus, specification).
При анализе FAIL — видеть контекст: «было 3 running Болванки, возможно конфликт ресурсов».
Хранить в runs.context_snapshot JSONB и scenario_runs.context_snapshot JSONB.
Данные обезличенные, не гигабайты. Отложено до реальной необходимости.
Аудит безопасности GPT-5.3-Codex (2026-07-31)
Проведён полный code review 29 файлов (~6000 строк). Найдено 11 проблем. Результаты зафиксированы в DOCS/ARCHITECTURE.md (раздел 8).
КРИТИЧЕСКИЕ (исправлены)
-
XSS через params в scenario-list.js:119 — k/v параметров в innerHTML без
_esc. Stored XSS через БД сценариев. → v1.2.19 -
JS injection в onclick (scenario-list.js:126-128) —
def.nameв'...'без JS-escape._escне экранирует'→ разрыв строки. → v1.2.19 -
Гонка
_op_results(api_test.py:264-280) — dict без lock, читается/пишется/чистится из нескольких потоков. → v1.2.20:threading.Lock()+pop(k, None) -
Неатомарный lock сценариев (scenario_defs.py + api_scenario_run.py) —
lock_check(SELECT) иINSERT RUNNINGразделены. → v1.2.20:pg_try_advisory_lock
СРЕДНИЕ (исправлены)
-
Lost update трекера (tracker.py) —
_locked_read+_locked_writeв разных lock. → v1.2.19:_atomic_update()под одним lock -
Зависание UI поллинга (scenario-list.js:211) — пустой catch,
busyне сбрасывается. → v1.2.19: счётчик ошибок +stopScenarioPoll+busy=false -
has_targetне проверяется (api_scenario_defs.py:58) — вычисляется и игнорируется. → v1.2.19: явная проверка -
_ensure_schemasilent (pool.py:64) —except Exception: pass. → v1.2.20:traceback.print_exc()
ПОТЕНЦИАЛЬНЫЕ (исправлены)
-
Stale async в редакторе (scenario-form.js) —
loadStepParamsпослеrenderEditorможет перезаписать новый DOM. → v1.2.20:_renderGengeneration token -
validate-cfs хрупкий (terraform.py:250-254) — фильтрация по тексту исключения. → v1.2.20: явный
except json.JSONDecodeError
НЕ ИСПРАВЛЕНО (архитектурное ограничение)
- In-memory
_op_resultsна воркер — не shared между gunicorn-воркерами. Статус иногда читается из API fallback. Решение: Redis/БД для статусов. Отложено — низкая вероятность проблемы на практике (2 воркера, stickiness).
Повторный аудит Codex (2026-07-31, вторая итерация)
Codex проверил исправления и нашёл критические ошибки в моих же фиксах:
ОШИБКА AI #1: Advisory lock сломан (v1.2.20)
Что я сделал: lock_check() брал pg_try_advisory_lock на соединении conn1,
возвращал True, и conn1 уходил обратно в пул. unlock_scenario() вызывал
get_conn() → получал conn2 (другое соединение!) → unlock на conn2 не снимал
lock с conn1. Плюс ранние return в api_scenario_run.py после успешного
lock_check вообще не вызывали unlock.
Почему ошибся: не учёл что PostgreSQL advisory lock привязан к сессии (соединению),
а соединения возвращаются в пул. Передача соединения между lock_check и потоком
сценария требовала бы сложной оркестрации.
Как исправлено (v1.2.21): заменён на 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 без гонок.
lock_check возвращён к простому SELECT (быстрая предпроверка для красивого 409).
unlock_scenario удалён полностью. try/finally из run_scenario убран.
ОШИБКА AI #2: escName не экранирует " (v1.2.19)
Что я сделал: def.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'") —
экранировал \ и ' для JS-строки, но забыл " для HTML-атрибута onclick="...".
Почему ошибся: фокусировался только на JS-контексте (строка в '...'),
не учёл что она внутри HTML-атрибута в "...".
Как исправлено (v1.2.21): добавлено .replace(/"/g,'"') в escName.
НОВАЯ находка Codex: instances.js:78
instanceUid в onclick="toggleInstance('${i.instanceUid}')" — теоретически уязвим,
но на практике UUID всегда [a-f0-9-]+ → безопасен. Отмечен как низкий риск,
исправление не требуется.
ИТОГ
| # | Слой | Статус |
|---|---|---|
| Advisory lock | Python | ❌ СЛОМАН → ✅ partial unique index |
escName " |
JS | ❌ Неполный → ✅ добавлен " |
| instances.js onclick | JS | ⚠️ Низкий риск, UUID безопасен |
lock_check fallback |
Python | ✅ True→False при ошибке БД |
Третий аудит Codex + трёхсостояночный lock_check (v1.2.22-v1.2.23)
Codex проверил v1.2.21 и нашёл 3 проблемы. Две исправлены, одну — обсудили и пришли к правильному решению:
Исправлено
-
UniqueViolation → 500 (не 409) —
api_scenario_run.py:78. INSERT ловился общимexcept→ 500. Теперь:e.pgcode == '23505'→ 409. (v1.2.22) -
escName без
&—scenario-list.js:128.'декодируется браузером в'до JS → разрыв строки. Добавлен.replace(/&/g,'&')ПЕРВЫМ шагом. (v1.2.22)
Обсуждено и исправлено правильно
-
lock_checkfallback — трёхсостояночный подход (v1.2.23):Исходно Codex предложил
True→Falseпри no-db. AI слепо сделал. Пользователь возразил:Falseломает запуск при деградации БД.Codex согласился и предложил трёхсостояночный возврат:
True— можно запускать (нет RUNNING)False— нельзя (есть RUNNING) → 409None— БД недоступна → 503
api_scenario_run.pyобрабатываетNoneкак 503 DB unavailable.
Созданы тесты (Codex, только сохранены, не запущены)
tests/ — 4 файла, покрывают критические фиксы:
| Файл | Что тестирует |
|---|---|
conftest.py |
Flask test client + sys.path |
test_api_scenario_run.py |
503 при lock_check=None, 409 при False, 409 при UniqueViolation(pgcode=23505) |
test_db_scenario_defs.py |
lock_check → None при no-db и ошибке БД |
test_static_regressions.py |
Статическая проверка idx_one_running в init_db.py и цепочки escName в scenario-list.js |
План: эмулятор Nubes API для интеграционных тестов
Зачем
Реальные тесты медленные (поллинг до 30 минут) и жрут ресурсы облака. Эмулятор даст: быстрые тесты (< 1 сек), детерминизм, краевые случаи, CI/CD.
Архитектура
site/
├── app.py # основное приложение
└── mock/
└── nubes_mock.py # эмулятор API Nubes (Flask, порт 5001)
В app.py — переключение по NUBES_MOCK=1 → эндпоинт http://localhost:5001/api/v1/svc.
Эндпоинты для эмуляции (Болванка, сервис 1)
| Метод | Путь | Ответ |
|---|---|---|
| POST | /instances |
201 + {instanceUid} |
| GET | /instances?pageSize=200 |
{results: [...]} |
| GET | /instances/{uid} |
{instance: {state: {params: {...}}}} |
| GET | /services |
{results: [{svcId: 1, svc: "dummy"}]} |
| GET | /services/1 |
{svc: {operations: [...]}} |
| POST | /instanceOperations |
{instanceOperationUid} |
| POST | /instanceOperationCfsParams |
{} |
| POST | /instanceOperations/{uid}/run |
{} |
| GET | /instanceOperations/{uid}?fields=... |
{instanceOperation: {dtFinish, isSuccessful, ...}} |
| GET | /instanceOperations/default/{id} |
{svcOperation: {cfsParams: [...]}} |
| GET | /instanceOperations/{uid}/validate-cfs |
{} (200 OK) |
Что сложнее
cfsParams— у каждого сервиса своя структура, придётся хардкодить под БолванкуrefSvcId— ссылки на другие сервисы (External IP и т.д.) — отложитьstages— этапы выполнения (plan→apply→...) — отдавать фейковые
Порядок создания
mock/nubes_mock.py— Flask-заглушка (~150 строк)- Интеграционный тест: сценарий
create→modify→deleteчерезapp_client NUBES_MOCK=1вapp.pyдля переключения эндпоинта
Архитектура мок-полигона (Опус, 2026-07-31)
Принятые решения (10/10)
| Q | Решение | Обоснование |
|---|---|---|
| Q1 | Отдельный процесс :5001 (A) | http_client делает реальные GET/POST — blueprint не проверит |
| Q2 | Папка polygon/services/*.yaml |
Каждый сервис в своём файле |
| Q3 | Мин. поля (id+код+тип), остальное достраивается | 20+ параметров вручную — ад |
| Q4 | Ленивый dtFinish (A) | Без потоков, детерминированно, MOCK_OP_DELAY=0.1 |
| Q5 | Единая стейт-машина | create→running→suspended→deleted |
| Q6 | Реальный мерж params | Иначе тест modify→проверить state.params бессмысленен |
| Q7 | Статический stateOut из YAML | Для MVP, генерация потом |
| Q8 | /_mock/reset |
Без сброса тесты влияют друг на друга |
| Q9 | Тесты через app_client |
Проверяет реальную связку, не только эмулятор |
| Q10 | refSvcId игнорируем в MVP | validate-cfs всегда OK |
Критические точки интеграции (из кода)
-
Location обязателен.
HttpClient.postдостаёт UUID из заголовкаLocation. Мок ОБЯЗАН отдаватьLocation: ./<uuid>на POST /instances и POST /instanceOperations. -
Поллинг спит 5с.
poll_until_doneделает GET, затемtime.sleep(5). ПриMOCK_OP_DELAY=0dtFinish появится на первом же GET — без задержки. -
Точка переключения — auth.py.
get_client()→detect_endpoint(). Нужен short-circuit: приNUBES_MOCK=1возвращатьhttp://localhost:5001/api/v1/svcи НЕ вызыватьdetect_endpoint. -
validate-cfs = пустое тело. Мок отдаёт 200 с пустым телом.
-
state.params по коду, cfsParams по числовому id. YAML должен связывать
svcOperationCfsParamId↔ код параметра.
План реализации (3 фазы, 12 шагов)
Фаза 1 — MVP (create + поллинг):
polygon/defaults.py—default_for(dataType)по типуpolygon/config_loader.py— загрузкаservices/*.yaml, достройка defaultspolygon/state.py—MockState: instances, operations, create, run, ленивый dtFinish, resetpolygon/server.py— Flask, префикс/api/v1/svc, Location-заголовкиpolygon/services/dummy.yaml— Болванка (id параметров из HAR)- Интеграция в
auth.py: short-circuit поNUBES_MOCK
Фаза 2 — полный CRUD + сервисы:
7. GET /instances/{uid} (state.params/state.out), GET /instances (пагинация)
8. GET /instanceOperations/default/{id}, GET /services, GET /services/{id}
9. apply_effect: modify→мерж params, delete→удаление, suspend/resume→статус
10. /_mock/reset + polygon/services/postgresql.yaml
Фаза 3 — тесты:
11. tests/conftest.py — фикстура поднятия мока + автосброс
12. tests/test_mock_integration.py — 5 сценариев через app_client
Структура файлов
app-autotest/
├── site/
│ ├── api/auth.py # +short-circuit localhost (НЕ NUBES_MOCK)
│ └── ...
└── polygon/
├── server.py # Flask-приложение эмулятора
├── state.py # MockState (в памяти)
├── config_loader.py # загрузка YAML + достройка defaults
├── defaults.py # default_for(dataType)
└── services/
├── dummy.yaml # Болванка
└── postgresql.yaml # PostgreSQL (stateOut: users, databases)
Short-circuit: localhost вместо NUBES_MOCK
Опус ПОДТВЕРДИЛ что проверка localhost лучше отдельного флага:
detect_endpoint()хардкодит dev/test и не читаетNUBES_API_ENDPOINT— всегда лезет в реальные стенды- При
NUBES_API_ENDPOINT=http://localhost:5001detect может «угадать» реальный стенд и увести мимо мока - Решение: в
get_client()иget_stand()— если endpoint начинается сhttp://localhostилиhttp://127.0.0.1→ пропустить detect, сразу использовать endpoint stand_name("http://localhost:5001")→"?"— поэтомуget_stand()при localhost возвращает"mock"- Ноль новых env-переменных, существующий
NUBES_API_ENDPOINTуже в конфиге
Реальные ID из HAR для dummy.yaml
Опус распарсил development/dummycreate.har и development/dummymodify.har:
Операции Болванки (serviceId=1):
| operation | svcOperationId |
|---|---|
| create | 18 |
| delete | 71 |
| modify | 92 |
| suspend | 93 |
| resume | 94 |
| redeploy | 240 |
Параметры create (svcOperationCfsParamId):
| id | код | тип |
|---|---|---|
| 242 | resourceRealm | string (valueList=["dummy"]) |
| 198 | durationMs | integer |
| 199 | (код не извлечён) | boolean |
| 200 | — | boolean |
| 201 | — | integer/enum |
| 286 | — | string |
| 321 | — | map + dataDescriptor |
| 322 | — | string |
| 396 | — | string |
| 647 | — | map |
| 654 | — | array(map) |
| 863 | — | array |
Для мока коды некритичны (мок сам отдаёт и шаблон и state.params — они должны совпадать между собой). Для реалистичности — код в mock-architecture-prompt.md достанет полные коды одной строкой Python.
Универсальный генератор моков из STANDS YAML (Опус, 2026-07-31)
Вердикт: универсальный конвертер возможен для всех 37 сервисов
Опус изучил 6 STANDS YAML (dummy, postgres, k8s, mariadb, VM v2, vDC). Структура единообразна — генерятся одним приложением. STANDS — авторитетный источник (сверен с HAR: create=18, параметры совпадают 1:1).
Ключевой маппинг STANDS → polygon
| STANDS | Polygon | Тип |
|---|---|---|
service_id→serviceId, service_short_name→svcShort |
1:1 | |
operations[].id/name/action → svcOperationId/operation/isCreate |
1:1 | |
params[].id/code/default/required → svcOperationCfsParamId/svcOperationCfsParam/defaultValue/isRequired |
1:1 | |
params[].data_type → dataType |
html.unescape |
|
params[].sub_params (list) → dataDescriptor (dict по code) |
трансформация | |
operations[].kind: subresource → stateOut[plural(subresource)] |
трансформация | |
name/service_man/lifecycle/outputs/func/descr/man/sort |
игнор |
Алгоритм convert_one()
- Базовые поля + операции (фильтр
kind: instance/action) cfsParamsиз create-параметров: unescape, sub_params→dataDescriptor (рекурсивно), default→default_for(dataType)stateParams: scalar→строка, map-fixed→json.dumps({sub_key: sub_val})stateOut: subresource-операции →{plural(subresource): {}}
4 границы универсальности
- Контент stateOut — структура (users/databases) генерится, имена (pgadmin/mydb) — нет. Решение: мок реализует subresource-операции
- redeploy не у всех — конвертер включает что есть
- Динамический valueList (
func: getAvailableResourceRealms) — берём статический - Битые valueList — pass-through
Архитектура polygon/from_stands.py
convert_all(stands_dir) → dict[int, config]
convert_one(raw) → config
_map_param, _build_dd, _gen_state_params, _gen_state_out
Поток: STANDS/*.yaml → yaml.safe_load → convert_one → MockState.
Вызывается при СТАРТЕ полигона. Ручные YAML (polygon/services/) больше не нужны.
Subresource-операции в MockState (Опус)
Обычные instanceOperations, без отдельного типа. Разница только в apply_effect:
action=="create"+ флагsubresource→ взять имя из params (username/dbName),instance.stateOut[subresource][name] = {}action=="delete"+subresource→del instance.stateOut[subresource][name]- иначе → прежняя логика (modify→stateParams, delete→удаление, suspend/resume→статус)
Конвертер помечает такие операции: {svcOperationId, operation, subresource, action}.
Один универсальный apply_effect, без сервис-специфичного кода.