cleanup: move all docs to root DOCS
This commit is contained in:
@@ -1,95 +0,0 @@
|
|||||||
# Архитектура Autotest — полный обзор
|
|
||||||
|
|
||||||
v1.1.44, 2026-07-30
|
|
||||||
|
|
||||||
## Назначение
|
|
||||||
|
|
||||||
Платформа для тестирования Nubes Cloud API — ручное и автоматизированное создание/изменение/удаление инстансов любых сервисов (PostgreSQL, Redis, S3, Kafka, ~35 сервисов).
|
|
||||||
|
|
||||||
## Развёртывание
|
|
||||||
|
|
||||||
- **Платформа:** Nubes pythonk8s (managed Flask + gunicorn)
|
|
||||||
- **URL:** `https://atest.pythonk8s.dev.nubes.ru/`
|
|
||||||
- **БД:** PostgreSQL 17 (Zalando Operator), autotest
|
|
||||||
- **Репозиторий:** `/home/naeel/nubes/autotest/app-autotest`
|
|
||||||
- **Родительский репо:** `/home/naeel/nubes/autotest`
|
|
||||||
|
|
||||||
## Ключевые файлы
|
|
||||||
|
|
||||||
### Бэкенд (Flask)
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/app.py` | Точка входа, регистрация blueprint'ов, VERSION |
|
|
||||||
| `site/routes/api_test.py` | Основной: POST /api/test, GET /api/params, поллинг, _finish_op |
|
|
||||||
| `site/routes/main.py` | GET /, /api/operations/{svc_id} |
|
|
||||||
| `site/routes/api.py` | LEGACY эндпоинты (будет удалён) |
|
|
||||||
| `site/api/http_client.py` | HTTP-клиент Nubes API (GET/POST/raw_delete) |
|
|
||||||
| `site/api/auth.py` | Токены, get_client, get_stand |
|
|
||||||
| `site/operations/get_params.py` | get_params_with_current_values, _normalize_value_list, state.out |
|
|
||||||
| `site/operations/get_instances.py` | GET /instances с пагинацией |
|
|
||||||
| `site/operations/get_services.py` | GET /services, /services/{id} |
|
|
||||||
| `site/operations/service_list.py` | Загрузка services_{stand}.txt |
|
|
||||||
| `site/operations/tracker.py` | fcntl.flock файловый трекер инстансов |
|
|
||||||
| `site/db/pool.py` | psycopg2 ThreadedConnectionPool (lazy-init) |
|
|
||||||
| `site/db/init_db.py` | CREATE TABLE runs + миграции |
|
|
||||||
| `site/db/save_run.py` | INSERT в runs (16 колонок) |
|
|
||||||
| `site/runner.py` | LEGACY runner (будет удалён) |
|
|
||||||
| `site/config.yaml` | LEGACY конфиг (будет удалён) |
|
|
||||||
|
|
||||||
### Фронтенд (vanilla JS)
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/templates/index.html` | Jinja2 шаблон, CSS, window.APP |
|
|
||||||
| `site/static/app.js` | Весь фронтенд (460 строк) |
|
|
||||||
| `site/static/style.css` | Доп. стили |
|
|
||||||
|
|
||||||
### Конфиги
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/config/services_test.txt` | Список сервисов TEST (комментировать # для исключения) |
|
|
||||||
| `site/config/services_dev.txt` | Список сервисов DEV |
|
|
||||||
| `requirements.txt` | Python-зависимости |
|
|
||||||
|
|
||||||
## CREATE flow (9 шагов Nubes API)
|
|
||||||
|
|
||||||
1. POST /instances
|
|
||||||
2. POST /instanceOperations {operation:"create"}
|
|
||||||
3. GET /instanceOperations/{opUid}?fields=cfsParams
|
|
||||||
4. resolveRefSvcParamValues
|
|
||||||
5. POST /instanceOperationCfsParams (пользовательские)
|
|
||||||
6. POST /instanceOperationCfsParams (required+default)
|
|
||||||
7. GET /validate-cfs
|
|
||||||
8. POST /run
|
|
||||||
9. Поллинг dtFinish
|
|
||||||
|
|
||||||
## Фронтенд — глобальное состояние
|
|
||||||
|
|
||||||
```
|
|
||||||
svcInstances — кеш инстансов
|
|
||||||
selectedInst — UID выбранного
|
|
||||||
selectedOp — {opId, opName, svcId}
|
|
||||||
pollTimer — setInterval поллинга
|
|
||||||
currentSvcId — ID сервиса
|
|
||||||
currentSvcName — имя сервиса
|
|
||||||
currentSvcShort— краткое имя (redis, postgres)
|
|
||||||
busy — блокировка параллельных операций
|
|
||||||
```
|
|
||||||
|
|
||||||
## План развития (MVP сценариев)
|
|
||||||
|
|
||||||
Файлы: `/home/naeel/nubes/autotest/TASKS/3007.md` (требования), `/home/naeel/nubes/autotest/DOCS/sol-answers.md` (решения)
|
|
||||||
|
|
||||||
1. Багфиксы аудита
|
|
||||||
2. Удалить legacy (api.py, runner.py, config.yaml)
|
|
||||||
3. Сервисные токены для сценариев
|
|
||||||
4. YAML-сценарии + валидатор
|
|
||||||
5. scenario_runs таблица + runner
|
|
||||||
6. UI: сворачиваемая секция «Сценарии»
|
|
||||||
|
|
||||||
## Документация
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` — эталонный Nubes API flow
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/sol-answers.md` — ответы на 14 вопросов по архитектуре
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/questions-to-sol.md` — 14 вопросов к Sol
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/DOCS/sonnet-final-audit.md` — финальный аудит Соннета
|
|
||||||
- `/home/naeel/nubes/autotest/HISTORY/2026-07-29-session.md` — хронология правок
|
|
||||||
@@ -1,417 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. См. ARCHITECTURE.md для текущего состояния.
|
|
||||||
|
|
||||||
# Архитектура app-autotest — полный документ (v1.0.76)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Обзор
|
|
||||||
|
|
||||||
Flask-приложение для автотестов операций сервисов облачной платформы Nubes.
|
|
||||||
Деплой: Nubes pythonk8s (gunicorn), тестовый стенд `atest.pythonk8s.dev.nubes.ru`.
|
|
||||||
Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`.
|
|
||||||
|
|
||||||
Текущий срез потока операций:
|
|
||||||
- CREATE и обычные операции разделены в UI и backend.
|
|
||||||
- CREATE использует фиксированный префикс `autotest-`, non-create работает по выбранному инстансу.
|
|
||||||
- Финальный статус показывает статус, операцию, длительность и имя инстанса.
|
|
||||||
- Список autotest-инстансов строится cloud-first; tracker нужен только как короткий fallback после CREATE.
|
|
||||||
- **Modify** (и другие операции с параметрами) показывает **текущие** значения параметров инстанса: создаётся pending-операция, читаются `cfsParams.paramValue`, форма заполняется реальными данными. При submit операция переиспользуется через `previewOpUid`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
app-autotest/
|
|
||||||
├── site/ ← НЕ пакет (без __init__.py, конфликт с stdlib)
|
|
||||||
│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
|
|
||||||
│ ├── api/
|
|
||||||
│ │ └── http_client.py ← HttpClient + STANDS + detect_endpoint()
|
|
||||||
│ ├── operations/
|
|
||||||
│ │ ├── tracker.py ← Трекер: /tmp/instances.json + fcntl.flock
|
|
||||||
│ │ ├── get_services.py ← get_services(), get_service_detail()
|
|
||||||
│ │ └── get_instances.py ← get_instances(), get_organization()
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── main.py ← GET/POST / (главная), _tmpl()
|
|
||||||
│ │ ├── api.py ← /api/run, /api/status, /api/config
|
|
||||||
│ │ └── api_test.py ← /api/test, /api/test/status, _finish_op
|
|
||||||
│ ├── templates/
|
|
||||||
│ │ └── index.html ← UI: Jinja2 + JS (поллинг)
|
|
||||||
│ └── static/
|
|
||||||
│ └── style.css
|
|
||||||
└── secrets/
|
|
||||||
├── dev.token ← Токен для DEV стенда
|
|
||||||
├── test.token ← Токен для TEST стенда
|
|
||||||
└── test.token.new
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Поток CREATE — полная трассировка (14 шагов)
|
|
||||||
|
|
||||||
### Шаг 1: UI — пользователь начинает CREATE
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: startCreate()
|
|
||||||
function startCreate(){
|
|
||||||
selectedInst=null; // нет выбранного инстанса
|
|
||||||
stopPoll(); // остановить предыдущий поллинг
|
|
||||||
document.querySelectorAll('[data-iuid]').forEach(e=>e.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.inst-ops').forEach(e=>e.classList.remove('open'));
|
|
||||||
showParams(18,'create'); // 18 = svcOperationId для create у сервиса "Болванка"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`selectedOp = {opId: 18, opName: "create", svcId: 1}`
|
|
||||||
|
|
||||||
### Шаг 2: UI — загрузка параметров и показ формы
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: showParams(18, 'create')
|
|
||||||
fetch('/api/params/18') // GET /api/params/18
|
|
||||||
.then(r=>r.json())
|
|
||||||
.then(params=>{
|
|
||||||
// Генерация формы: displayName + поля из params
|
|
||||||
const displayName = 'autotest-1-' + Date.now().toString(36);
|
|
||||||
form.innerHTML = '<input id="param-displayname" value="' + displayName + '">'
|
|
||||||
+ params.map(p => '<input name="p_' + p.svcOperationCfsParamId + '">').join('');
|
|
||||||
|
|
||||||
btn.onclick = () => {
|
|
||||||
// Сбор params из формы
|
|
||||||
const pp = {};
|
|
||||||
document.querySelectorAll('#params-form [name^="p_"]').forEach(el => {
|
|
||||||
pp[el.name.replace('p_','')] = el.value;
|
|
||||||
});
|
|
||||||
executeOp(pp); // ← запуск
|
|
||||||
};
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Бэкенд: `GET /instanceOperations/default/18` → возвращает список `cfsParams`.
|
|
||||||
|
|
||||||
### Шаг 3: UI — executeOp (отправка)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: executeOp(params)
|
|
||||||
const displayName = document.getElementById('param-displayname')?.value || 'autotest-1';
|
|
||||||
// Захват ДО очистки формы! (v1.0.48)
|
|
||||||
|
|
||||||
document.getElementById('params-form').innerHTML = ''; // очистка
|
|
||||||
|
|
||||||
fetch('/api/test', {
|
|
||||||
method:'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
serviceId: 1, // SVC_ID
|
|
||||||
operation: "create",
|
|
||||||
svcOperationId: 18,
|
|
||||||
params: {...}, // собранные параметры
|
|
||||||
instanceUid: "", // пусто для create
|
|
||||||
displayName: displayName
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
JSON: `{"serviceId":1, "operation":"create", "svcOperationId":18, "params":{...}, "instanceUid":"", "displayName":"autotest-1-lq5x3a"}`
|
|
||||||
|
|
||||||
### Шаг 4: Бэкенд — api_test() разбор запроса
|
|
||||||
|
|
||||||
```python
|
|
||||||
# api_test.py: POST /api/test
|
|
||||||
data = request.get_json()
|
|
||||||
svc_id = data["serviceId"] # 1 (int)
|
|
||||||
op_name = data["operation"] # "create" (str)
|
|
||||||
svc_op_id = data["svcOperationId"] # 18 (int)
|
|
||||||
params = data.get("params", {}) # {...} (dict)
|
|
||||||
instance_uid = data.get("instanceUid") # "" (falsy)
|
|
||||||
display_name = data.get("displayName", f"autotest-{svc_id}") # "autotest-1-lq5x3a"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 5: Бэкенд — _client() с автоопределением стенда
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _client():
|
|
||||||
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
|
||||||
endpoint = detect_endpoint(token) or current_app.config["NUBES_API_ENDPOINT"]
|
|
||||||
return HttpClient(endpoint, token)
|
|
||||||
```
|
|
||||||
|
|
||||||
`detect_endpoint(token)` пробует dev→test стенды, возвращает рабочий URL или None.
|
|
||||||
|
|
||||||
### Шаг 6: Бэкенд — создание инстанса в Nubes
|
|
||||||
|
|
||||||
```python
|
|
||||||
payload = {"serviceId": 1, "displayName": "autotest-1-lq5x3a", "descr": ""}
|
|
||||||
resp = client.post("/instances", payload)
|
|
||||||
# Ответ: {"instanceUid": "93b0b65d-..."}
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(...)
|
|
||||||
# → "93b0b65d-e080-416d-bbb8-463f7adbda80"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 7: Бэкенд — создание операции
|
|
||||||
|
|
||||||
```python
|
|
||||||
op_payload = {"instanceUid": "93b0b65d-...", "operation": "create"}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
# Ответ: {"instanceOperationUid": "0b0596d8-..."}
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(...)
|
|
||||||
# _find_uid ищет: instanceOperationUid → instanceUid → uid → Uid
|
|
||||||
# → "0b0596d8-b48d-4694-98c3-65f2f8eeb27d"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 8: Бэкенд — tracker_add (v1.0.54: ДО params!)
|
|
||||||
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
tracker_add("93b0b65d-...", 1, "autotest-1-lq5x3a")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[TRACKER ERROR] ...")
|
|
||||||
```
|
|
||||||
|
|
||||||
`tracker.py`: `_locked_read()` → `data["93b0b65d-..."] = {...}` → `_locked_write(data)`.
|
|
||||||
Файл `/tmp/instances.json` обновлён под `fcntl.LOCK_EX | LOCK_NB`.
|
|
||||||
|
|
||||||
### Шаг 9: Бэкенд — установка параметров и запуск
|
|
||||||
|
|
||||||
```python
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams", {
|
|
||||||
"instanceOperationUid": op_uid,
|
|
||||||
"svcOperationCfsParamId": int(pid),
|
|
||||||
"paramValue": str(pval)
|
|
||||||
})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
```
|
|
||||||
|
|
||||||
Если params пустые (например, повторный клик на «Готово») → `/run` → 422 "required CFS parameter ... is missing".
|
|
||||||
|
|
||||||
### Шаг 10: Бэкенд — фоновый поток
|
|
||||||
|
|
||||||
```python
|
|
||||||
threading.Thread(
|
|
||||||
target=_finish_op,
|
|
||||||
args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True),
|
|
||||||
daemon=True
|
|
||||||
).start()
|
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 11: UI — поллинг статуса
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// index.html: executeOp() — после получения RUNNING
|
|
||||||
pollTimer = setInterval(async () => {
|
|
||||||
const sr = await fetch('/api/test/status/' + opUid);
|
|
||||||
const sd = await sr.json();
|
|
||||||
showStages(sd.stages || []);
|
|
||||||
if (sd.status !== 'RUNNING') {
|
|
||||||
stopPoll();
|
|
||||||
// показать OK/FAIL
|
|
||||||
btn.textContent = 'Готово';
|
|
||||||
btn.disabled = false;
|
|
||||||
// ⚠️ БАГ: btn.onclick всё ещё активен!
|
|
||||||
if (sd.status === 'OK') {
|
|
||||||
await refreshInstances();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 12: Бэкенд — _finish_op (фоновый)
|
|
||||||
|
|
||||||
```python
|
|
||||||
while time.time() < deadline: # 300 секунд
|
|
||||||
data = client.get(f"/instanceOperations/{op_uid}?fields=...")
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
|
|
||||||
_op_results[op_uid] = {"status": "RUNNING", "stages": [...], "duration": ...}
|
|
||||||
|
|
||||||
if dt_finish and str(dt_finish).strip():
|
|
||||||
is_ok = op.get("isSuccessful")
|
|
||||||
# tracker_add уже вызван синхронно!
|
|
||||||
if is_ok and is_delete:
|
|
||||||
tracker_remove(instance_uid)
|
|
||||||
_op_results[op_uid] = {"status": "OK" if is_ok else "FAIL", ...}
|
|
||||||
return
|
|
||||||
time.sleep(5)
|
|
||||||
```
|
|
||||||
|
|
||||||
Весь цикл обёрнут в `try/except: print(traceback)` (v1.0.51).
|
|
||||||
|
|
||||||
### Шаг 13: UI — refreshInstances
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
async function refreshInstances(){
|
|
||||||
const r = await fetch('/api/operations/1');
|
|
||||||
const d = await r.json();
|
|
||||||
const newInsts = d.instances || [];
|
|
||||||
// Найти новые (отсутствуют в svcInstances)
|
|
||||||
const added = newInsts.filter(i => !oldUids.has(i.instanceUid));
|
|
||||||
// Обновить бейджи существующих
|
|
||||||
// Добавить новые в DOM перед кнопкой "+ Создать"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 14: Бэкенд — api_operations (GET /api/operations/1)
|
|
||||||
|
|
||||||
```python
|
|
||||||
tracked = tracker_list() # читает /tmp/instances.json
|
|
||||||
tracked_by_uid = {uid: item for uid in tracked if svcId == 1}
|
|
||||||
instances = get_instances(_client()) # запрос в Nubes API
|
|
||||||
nubes_uids = {i["instanceUid"] for i in instances}
|
|
||||||
|
|
||||||
# Инстансы из Nubes которые есть в трекере (кроме deleted)
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids
|
|
||||||
and i.get("explainedStatus") not in ("deleted",)]
|
|
||||||
|
|
||||||
# Инстансы из трекера которых Nubes ещё не отдаёт → status "creating"
|
|
||||||
for uid, t in tracked_by_uid.items():
|
|
||||||
if uid not in nubes_uids:
|
|
||||||
svc_instances.append({
|
|
||||||
"instanceUid": uid,
|
|
||||||
"displayName": t["displayName"],
|
|
||||||
"explainedStatus": "creating",
|
|
||||||
...
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Трекер инстансов
|
|
||||||
|
|
||||||
### 4.1 Назначение
|
|
||||||
|
|
||||||
Хранит UID инстансов, созданных приложением. Нужен чтобы показывать в UI только «наши» инстансы, а не все в организации.
|
|
||||||
|
|
||||||
### 4.2 Реализация (v1.0.53+)
|
|
||||||
|
|
||||||
Файл: `site/operations/tracker.py`
|
|
||||||
|
|
||||||
- Хранилище: `/tmp/instances.json` (JSON-файл)
|
|
||||||
- Блокировка: `fcntl.flock(fd, LOCK_EX | LOCK_NB)` с retry до 2 сек
|
|
||||||
- Seed: `_INITIAL` (4 инстанса) при пустом/битом файле
|
|
||||||
- Функции: `add(uid, svc_id, name)`, `remove(uid)`, `list_all()`
|
|
||||||
|
|
||||||
### 4.3 Эволюция
|
|
||||||
|
|
||||||
| Версия | Реализация | Проблема |
|
|
||||||
|--------|-----------|----------|
|
|
||||||
| v1.0.45 | `/tmp/instances.json` + `threading.Lock` | `except: pass` скрывал ошибки |
|
|
||||||
| v1.0.49 | In-memory dict | Multi-worker gunicorn: у каждого свой dict |
|
|
||||||
| v1.0.53 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX)` | Без таймаута: потенциальный зависон |
|
|
||||||
| v1.0.54 | `/tmp/instances.json` + `fcntl.flock(LOCK_EX\|LOCK_NB)` + retry 2s | ✅ Текущий |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Автоопределение стенда
|
|
||||||
|
|
||||||
Файл: `site/api/http_client.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
STANDS = [
|
|
||||||
"https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
|
|
||||||
"https://lk-api-gateway-test.ngcloud.ru/api/v1/svc",
|
|
||||||
]
|
|
||||||
|
|
||||||
def detect_endpoint(token):
|
|
||||||
for ep in STANDS:
|
|
||||||
try:
|
|
||||||
c = HttpClient(ep, token)
|
|
||||||
data = c.get("/instances", params={"pageSize": 1, "page": 1})
|
|
||||||
if data.get("results") is not None:
|
|
||||||
return ep
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
return None
|
|
||||||
```
|
|
||||||
|
|
||||||
Используется в:
|
|
||||||
- `main.py`: при загрузке главной страницы
|
|
||||||
- `api_test.py: _client()`: при каждом API-запросе
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Все известные баги (найдено/исправлено)
|
|
||||||
|
|
||||||
### Исправленные
|
|
||||||
|
|
||||||
| # | Версия | Баг | Причина | Исправление |
|
|
||||||
|---|--------|-----|---------|-------------|
|
|
||||||
| 1 | v1.0.46 | `_finish_op()` недостающие параметры | Не передавались `op_name`, `svc_op_id` | Добавлены в сигнатуру |
|
|
||||||
| 2 | v1.0.48 | displayName терялся | `params-form.innerHTML=''` до чтения `param-displayname` | Захват до очистки |
|
|
||||||
| 3 | v1.0.48 | tracker_add в daemon-потоке | Поток умирает под gunicorn | Перенос в синхронный код |
|
|
||||||
| 4 | v1.0.50 | ❌ для этапов в процессе | `isSuccessful===false` для in-progress | Проверка `dtFinish` |
|
|
||||||
| 5 | v1.0.50 | Инстанс не в списке | `explainedStatus:"not created"` фильтровался | Убран фильтр + tracked-сироты |
|
|
||||||
| 6 | v1.0.50 | `selectService()` скрывал stages | Полный перерендер после OK | `refreshInstances()` с диффом |
|
|
||||||
| 7 | v1.0.51 | `instance_groups` UnboundLocalError | Не иниц. до `if active_token:` | `instance_groups = {}` |
|
|
||||||
| 8 | v1.0.51 | `_finish_op` молча умирал | `data.get()` вне try/except | Весь цикл в try/except |
|
|
||||||
| 9 | v1.0.51 | Автостенд только в main.py | `_client()` в api_test.py не использовал | `detect_endpoint()` в http_client.py |
|
|
||||||
| 10 | v1.0.52 | `token_info` UndefinedError | Не передан в шаблон при action=clear | `_tmpl()` helper со всеми переменными |
|
|
||||||
| 11 | v1.0.53 | In-memory dict + multi-worker | У каждого воркера свой `_data` | `/tmp/instances.json` + `fcntl.flock` |
|
|
||||||
| 12 | v1.0.54 | `_find_uid()` возвращал не тот UUID | Итерация по всем значениям dict | Поиск по ключам: instanceOperationUid→instanceUid |
|
|
||||||
| 13 | v1.0.54 | Сироты при ошибке params | `tracker_add` ПОСЛЕ params loop | `tracker_add` ДО params loop |
|
|
||||||
| 14 | v1.0.54 | `flock` без таймаута | `LOCK_EX` блокируется навсегда | `LOCK_EX \| LOCK_NB` + retry 2s |
|
|
||||||
| 15 | v1.0.55 | F5 = предупреждение браузера | clear/stray POST → HTML без редиректа | `redirect("/")` для всех POST |
|
|
||||||
| 16 | v1.0.56 | Кнопка «Готово» запускала повторный CREATE | `btn.onclick` оставался привязанным после завершения операции | `btn.onclick = null` после финального статуса |
|
|
||||||
| 17 | v1.0.57 | Хрупкость финального состояния кнопки | Повторяемый код финализации статуса | `setFinishedState()` централизует финальный UI-состояние |
|
|
||||||
| 18 | v1.0.59 | После F5 список терял tracked-инстанс | `api_operations()` не добирал tracked-сирот обратно из трекера | `tracked_by_uid` + добавление отсутствующих tracked-инстансов |
|
|
||||||
| 19 | v1.0.60 | CREATE не появлялся сразу в списке после OK | UI делал diff и мог не перерисовать список целиком | Полная перерисовка `inst-list` в `refreshInstances()` |
|
|
||||||
| 20 | v1.0.63 | Дубли и чужие инстансы | Принадлежность определялась не по namespace | `autotest-` prefix + уникализация `displayName` при CREATE |
|
|
||||||
| 21 | v1.0.64 | Версия не показывалась в шапке | `load_config()` затирал VERSION | `config["VERSION"] = current_app.config["VERSION"]` |
|
|
||||||
| 22 | v1.0.64 | displayName был editable value, а не placeholder | CREATE подставлял имя в value | Placeholder `autotest-...`, значение вводит пользователь |
|
|
||||||
| 23 | v1.0.65 | `creating` из-за недочитанной страницы | `get_instances()` доверял `total` и мог не перейти на следующую страницу | Остановка по `len(batch) < pageSize` |
|
|
||||||
|
|
||||||
### ✅ Закрыто в v1.0.56
|
|
||||||
|
|
||||||
| Баг | Статус | Что поменяли |
|
|
||||||
|-----|--------|--------------|
|
|
||||||
| Кнопка «Готово» запускала повторный CREATE | закрыто | После финального статуса обработчик снимается через `btn.onclick = null` |
|
|
||||||
| displayName "autotest-1" при повторном клике | закрыто | Повторный клик больше не вызывает `executeOp()` на очищенной форме |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. HTTP-клиент
|
|
||||||
|
|
||||||
Файл: `site/api/http_client.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
class HttpClient:
|
|
||||||
def __init__(self, endpoint, token):
|
|
||||||
# Bearer auth + User-Agent: Mozilla/5.0 (DDoS-Guard)
|
|
||||||
|
|
||||||
def get(self, path, timeout=10):
|
|
||||||
# GET → r.raise_for_status() → r.json()
|
|
||||||
|
|
||||||
def post(self, path, data, timeout=30):
|
|
||||||
# POST → проверка r.ok → парсинг JSON → Location header
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Шаблон Jinja2 — используемые переменные
|
|
||||||
|
|
||||||
| Переменная | Где | Назначение |
|
|
||||||
|-----------|-----|-----------|
|
|
||||||
| `config.VERSION` | Строка 32 | Версия в топбаре |
|
|
||||||
| `token_info.email` | Строка 33 | Email пользователя |
|
|
||||||
| `token_info.company` | Строка 33 | Компания |
|
|
||||||
| `env_token_masked` | Строка 36 | Маскированный токен в placeholder |
|
|
||||||
| `has_user_token` | Строка 36 | Показать токен или маску |
|
|
||||||
| `error` | Строка 40 | Ошибка |
|
|
||||||
| `organization.displayName` | Строка 50 | Название организации |
|
|
||||||
| `organization.explainedStatus` | Строка 51 | Статус организации |
|
|
||||||
| `client_id` | Строка 50 | ID клиента |
|
|
||||||
| `instance_groups` | Строки 55-62 | Группы инфраструктурных инстансов |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Ограничения платформы Nubes pythonk8s
|
|
||||||
|
|
||||||
- `site/` — НЕ пакет (без `__init__.py`, конфликт с stdlib `site.py`)
|
|
||||||
- Импорты: `from api.http_client import ...` (без префикса `site.`)
|
|
||||||
- `app.run(host="0.0.0.0", port=5000)` — обязательно
|
|
||||||
- Gunicorn: запускается платформой, количество воркеров неизвестно
|
|
||||||
- Нет persistent volume — `/tmp/` теряется при редеплое
|
|
||||||
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
|
|
||||||
- Деплой: `git push` → managed service редеплоит
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
# Архитектура 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-экранирование (`"` → `"`, `&` → `&`, `<` → `<`)
|
|
||||||
- `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)
|
|
||||||
-478
@@ -1,478 +0,0 @@
|
|||||||
# История разработки app-autotest
|
|
||||||
|
|
||||||
## v1.1.2 (28.07.2026) — refSvcId: выпадающие списки для cross-service параметров
|
|
||||||
|
|
||||||
- `get_params.py` + `api_test.py`: параметры включают `refSvcId` из API
|
|
||||||
- Фронт: `refSvcId` → `fetch('/api/instances/list')` → фильтр по `serviceId` → `<select>` с `displayName`
|
|
||||||
- Универсально: S3, K8s-кластер, любой сервис — data-driven
|
|
||||||
|
|
||||||
## Инфраструктура
|
|
||||||
- **Кластер**: `iot-naeel` (resourceRealm K8s)
|
|
||||||
- **URL**: `https://atest.pythonk8s.dev.nubes.ru`
|
|
||||||
- **Деплой**: git push → managed service pythonk8s → gunicorn
|
|
||||||
- **PostgreSQL**: `foriot` (509145c3-...), PG 17, `iot-naeel`
|
|
||||||
|
|
||||||
## v1.0.93 (28.07.2026) — отдельные DB_* переменные
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `db/pool.py`: `_dsn()` собирает DSN из отдельных переменных: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSLMODE
|
|
||||||
- `db/init_db.py`: проверка `DB_USER` вместо `DATABASE_URL`
|
|
||||||
- `secrets/pg-foriot.md`: полный jsonEnv для pythonk8s (6 DB-переменных + 2 существующие)
|
|
||||||
- `app.py`: NUBES_API_ENDPOINT по умолчанию TEST
|
|
||||||
|
|
||||||
## v1.0.92 (27.07.2026) — Steps 5-9: динамические сервисы + PostgreSQL история
|
|
||||||
|
|
||||||
### Шаг 5: JS → /static/app.js
|
|
||||||
- JS вынесен из index.html в отдельный файл
|
|
||||||
- `window.APP = {version, stand, hasUserToken}` — передача Jinja2-переменных
|
|
||||||
|
|
||||||
### Шаг 6: Динамические сервисы
|
|
||||||
- `currentSvcId` вместо хардкода `SVC_ID=1`
|
|
||||||
- `initServices()` — загрузка из `/api/services` при старте
|
|
||||||
|
|
||||||
### Шаг 7-8: PostgreSQL
|
|
||||||
- `db/pool.py` — ThreadedConnectionPool (lazy-init, 1-5 соединений)
|
|
||||||
- `db/init_db.py` — таблица `runs` с индексами (идемпотентно)
|
|
||||||
- `db/save_run.py` — запись результатов в БД
|
|
||||||
- `requirements.txt` — добавлен `psycopg2-binary`
|
|
||||||
|
|
||||||
### Шаг 9: /api/history
|
|
||||||
- GET `/api/history` — последние 50 записей (фильтр по client_id + stand)
|
|
||||||
|
|
||||||
## v1.0.90 (27.07.2026) — Steps 1-4: auth.py + TTL + cookie
|
|
||||||
|
|
||||||
### Шаг 1-2: api/auth.py
|
|
||||||
- Новый модуль: `get_token()`, `get_client()`, `get_client_id()`, `get_stand()`, `get_token_info()`, `get_token_masked()`
|
|
||||||
- Убраны дубликаты из main.py и api_test.py
|
|
||||||
|
|
||||||
### Шаг 3: Cookie httponly + samesite
|
|
||||||
- `set_cookie(..., httponly=True, samesite="Strict")`
|
|
||||||
|
|
||||||
### Шаг 4: _op_results TTL
|
|
||||||
- Максимум 500 записей, удаление старше 1 часа
|
|
||||||
- `_ts` timestamp в каждой записи
|
|
||||||
|
|
||||||
## v1.0.99 (28.07.2026) — фикс: Flask-прокси в фоновом потоке
|
|
||||||
|
|
||||||
### Найденные ошибки
|
|
||||||
1. `get_token_info()` → `request.cookies` → в потоке нет request-контекста → RuntimeError
|
|
||||||
2. `current_app.config.get("VERSION")` → Flask-proxy → та же проблема
|
|
||||||
3. `except: pass` глушил ошибки → записи молча не сохранялись
|
|
||||||
|
|
||||||
### Исправление
|
|
||||||
- `user_email` и `app_version` получаются в `api_test()` (где есть request)
|
|
||||||
и передаются параметрами в `_finish_op` → `save_run`
|
|
||||||
- Добавлено логирование ошибок в `save_run` (больше не глухое `pass`)
|
|
||||||
|
|
||||||
### Урок
|
|
||||||
`py_compile` проверяет только синтаксис. Flask-прокси (`request`, `current_app`, `g`, `session`)
|
|
||||||
**не работают** в фоновых потоках. Все нужные значения — до `threading.Thread` параметрами.
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — Sonnet Rounds 1-3 + документирование
|
|
||||||
|
|
||||||
### 1. PostgreSQL connection string
|
|
||||||
- `os.getenv("DATABASE_URL")` — никакого service discovery
|
|
||||||
- Инжектится через `jsonEnv` параметры pythonk8s-сервиса (как NUBES_API_TOKEN)
|
|
||||||
- Порядок: создать PostgreSQL-инстанс в Nubes → взять host/port/user/pass из state_out → собрать DSN → jsonEnv → деплой
|
|
||||||
|
|
||||||
### 2. Миграции при деплое
|
|
||||||
- `init_db()` на уровне модуля в app.py, до register_blueprint
|
|
||||||
- `CREATE TABLE/INDEX IF NOT EXISTS` — идемпотентно, каждый воркер выполнит независимо
|
|
||||||
- PostgreSQL обрабатывает конкурентный DDL корректно
|
|
||||||
- Никаких ручных шагов при деплое
|
|
||||||
- `DROP COLUMN` — не идемпотентно, осторожно
|
|
||||||
|
|
||||||
### 3. Multi-service UI (map-fixed)
|
|
||||||
- `dataDescriptor` уже в API-ответе — инфраструктура готова
|
|
||||||
- `map` (свободный): textarea + JSON-валидация (как сейчас)
|
|
||||||
- `map-fixed`: таблица подполей с отдельными input/select из dataDescriptor
|
|
||||||
- Бэкенд не меняется — paramValue всегда строка
|
|
||||||
- Изменения только во фронте: showParams() + сбор подполей в JSON перед отправкой
|
|
||||||
|
|
||||||
### 4. Тесты
|
|
||||||
- pytest для чистых функций без моков: `_find_uid`, `_uid_from_location`, `_with_prefix`, слияние params, tracker
|
|
||||||
- Структура: `tests/conftest.py`, `test_utils.py`, `test_tracker.py`, `test_get_params.py`
|
|
||||||
- Роуты с Flask test client + mock HttpClient — отложить (высокая стоимость мокирования)
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — Sonnet Round 2: PostgreSQL, auth.py, детальный план
|
|
||||||
|
|
||||||
### Результаты анализа
|
|
||||||
Sonnet дал конкретные ответы на все 7 вопросов Round 2:
|
|
||||||
|
|
||||||
**1. PostgreSQL схема:**
|
|
||||||
- Таблица `runs` с JSONB-полями `params` и `stages`
|
|
||||||
- 3 индекса: `(client_id, stand, created_at DESC)`, `(instance_uid)`, `(created_at)`
|
|
||||||
- Stages в JSONB (не отдельная таблица), instances не нужны (API — source of truth)
|
|
||||||
- Чувствительные params маскировать при сохранении
|
|
||||||
|
|
||||||
**2. Connection pool:**
|
|
||||||
- `psycopg2.pool.ThreadedConnectionPool` — минимум зависимостей
|
|
||||||
- Lazy-init в `db/pool.py` — безопасно для fork-модели gunicorn
|
|
||||||
- `flask.g` + `@app.teardown_appcontext` для возврата соединений
|
|
||||||
|
|
||||||
**3. Миграции:**
|
|
||||||
- `db/init_db.py` с `IF NOT EXISTS` — идемпотентно, авто-применение при старте
|
|
||||||
- Alembic — когда 3+ таблицы или нужен rollback (сейчас не нужно)
|
|
||||||
|
|
||||||
**4. Трекер vs PostgreSQL:**
|
|
||||||
- **НЕ заменять** трекер на БД — это разные слои
|
|
||||||
- Трекер = кеш (секунды), PostgreSQL = история (недели)
|
|
||||||
- Сетевое обращение к БД на критическом пути CREATE недопустимо
|
|
||||||
|
|
||||||
**5. auth.py:**
|
|
||||||
- Явные функции вместо `before_request` — проще, нет скрытых зависимостей
|
|
||||||
- Одна реализация `get_client_id()` вместо двух копий
|
|
||||||
- Кеширование `token → endpoint` (опционально, TTL 5 мин)
|
|
||||||
|
|
||||||
**6. Динамические сервисы:**
|
|
||||||
- `/api/services` из API → фильтр по `enabled: true` из config.yaml
|
|
||||||
- UI: заменить хардкод `SVC_ID=1` на динамическую загрузку в `selectService()`
|
|
||||||
|
|
||||||
**7. JS в файл:**
|
|
||||||
- Классический скрипт, один файл `app.js` (300 строк — не нужно ES modules)
|
|
||||||
- `window.APP = {...}` через `tojson` фильтр Jinja2 для передачи переменных
|
|
||||||
|
|
||||||
### Итоговый порядок реализации (9 шагов)
|
|
||||||
1. `api/auth.py` — вынести get_token/get_client/get_client_id/get_stand
|
|
||||||
2. Заменить дублирующиеся функции в main.py и api_test.py
|
|
||||||
3. Cookie `httponly=True, samesite='Strict'`
|
|
||||||
4. `_op_results` TTL/очистка
|
|
||||||
5. JS вынести в `/static/app.js` + `window.APP`
|
|
||||||
6. Динамические сервисы из `/api/services`
|
|
||||||
7. `db/pool.py` + `db/init_db.py` (PostgreSQL)
|
|
||||||
8. Запись истории в runs при завершении операции
|
|
||||||
9. `/api/history` эндпоинт + UI
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — Sonnet Round 1: архитектурный аудит
|
|
||||||
- Cloud-first подход с tracker-fallback
|
|
||||||
- HTML-экранирование в JS
|
|
||||||
|
|
||||||
**🔴 Критические находки:**
|
|
||||||
1. `_op_results` dict растёт бесконечно — утечка памяти, нужен TTL/очистка
|
|
||||||
2. Cookie токена без `httponly` и `samesite`
|
|
||||||
3. `/api/log` без проверки авторизации
|
|
||||||
|
|
||||||
**🟡 Дублирование:**
|
|
||||||
1. `_client_id()` — идентичный код в main.py и api_test.py
|
|
||||||
2. `_client()` — разное поведение в main.py и api_test.py
|
|
||||||
3. Инлайн `<style>` дублирует `style.css`
|
|
||||||
|
|
||||||
**🟡 Захардкодено:**
|
|
||||||
1. `SVC_ID=1` и список сервисов в HTML — хотя `/api/services` существует
|
|
||||||
2. `create_client()` в main.py vs `_client()` в api_test.py — разные подходы
|
|
||||||
|
|
||||||
**Приоритетный план исправлений:**
|
|
||||||
1. 🔴 `_op_results` — добавить очистку/TTL
|
|
||||||
2. 🔴 Cookie `httponly=True, samesite='Strict'`
|
|
||||||
3. 🟡 Вынести `_client_id()` / `get_token()` в общий модуль `api/auth.py`
|
|
||||||
4. 🟡 Загрузка сервисов из `/api/services` вместо хардкода
|
|
||||||
5. 🟡 Вынести JS в `/static/app.js`
|
|
||||||
6. 🟢 SQLite история запусков
|
|
||||||
7. 🟢 `/api/log` проверка токена
|
|
||||||
|
|
||||||
### Создан запрос Round 2
|
|
||||||
Файл: `DOCS/sonnet-architecture-review-v1.0.89-r2.md` — уточняющие вопросы по реализации.
|
|
||||||
|
|
||||||
## v1.0.89 (27.07.2026) — документация и комментарии кода
|
|
||||||
|
|
||||||
## v1.0.87 (27.07.2026) — документация: ARCHITECTURE.md переписан, legacy-доки помечены
|
|
||||||
|
|
||||||
## v1.0.84 (27.07.2026) — лог-панель скрыта по умолчанию
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- Лог-панель теперь `display:none`, показывается кнопкой `log` (правый нижний угол).
|
|
||||||
- При скрытой панели `/api/log` не поллится.
|
|
||||||
|
|
||||||
## v1.0.83 (27.07.2026) — HTML-escape + JSON-валидация map-полей
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `_esc(s)` — HTML-экранирование значений (`"` → `"`, `&` → `&`, `<` → `<`).
|
|
||||||
- `validateJson(el, quiet)` — проверка `JSON.parse()` при `onblur` (красная рамка + текст ошибки).
|
|
||||||
- Batch-проверка всех map-полей перед отправкой — ошибка → запрос не уходит.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Поломку HTML при значениях с кавычками (напр. `{"f":1}`).
|
|
||||||
- Отправку битого JSON в API.
|
|
||||||
|
|
||||||
## v1.0.82 (27.07.2026) — displayName из API для non-create операций
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `_get_instance_display_name(client, uid)` — `GET /instances/{uid}` → `displayName`.
|
|
||||||
- Используется в не-create ветке `POST /api/test` вместо `instance_uid` как fallback.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- UUID вместо имени инстанса в финальном статусе.
|
|
||||||
|
|
||||||
## v1.0.81 (27.07.2026) — get_params.py: текущие значения из state.params
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- Новый файл `site/operations/get_params.py` — независимый модуль.
|
|
||||||
- `get_params_with_current_values(client, op_id, instance_uid)`:
|
|
||||||
- `GET /instances/{uid}` → `state.params` (текущие значения)
|
|
||||||
- `GET /instanceOperations/default/{opId}` → шаблон
|
|
||||||
- Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
|
|
||||||
- Убран `previewOpUid` полностью (и из бэкенда, и из фронтенда).
|
|
||||||
|
|
||||||
## v1.0.80 (27.07.2026) — попытка fix через svcOperationId (НЕ СРАБОТАЛО)
|
|
||||||
|
|
||||||
## v1.0.79 (27.07.2026) — файловый лог для multi-worker
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- `_log()` пишет в `/tmp/app-autotest.log` под `fcntl.flock` (вместо in-memory deque).
|
|
||||||
- `/api/log` читает из файла последние 200 строк.
|
|
||||||
- Ротация при 512 КБ.
|
|
||||||
- UI-панель логов (180px, автоскролл, поллинг 2s).
|
|
||||||
|
|
||||||
## v1.0.77 (27.07.2026) — debug-логирование в api_params
|
|
||||||
|
|
||||||
## v1.0.76 (27.07.2026) — modify: preview-операция для получения paramValue (НЕ СРАБОТАЛО)
|
|
||||||
|
|
||||||
# v1.0.74 (27.07.2026) — version bump for push
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` create-операция отделена от обычных операций: CREATE использует фиксированный префикс `autotest-`, а non-create берёт имя выбранного инстанса.
|
|
||||||
- Из non-create ветки убран случайный fallback `tut` и общий create-style `displayName`.
|
|
||||||
- В `site/routes/api_test.py` backend больше не подставляет create-имя по умолчанию для обычных операций и возвращает `displayName` для финального статуса.
|
|
||||||
- В `site/routes/main.py` список autotest-инстансов теперь строится cloud-first без старой склейки через map по имени; tracker остался только как временный fallback, если cloud ещё не вернул новый инстанс.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Путаницу между CREATE и обычными операциями.
|
|
||||||
- Ситуацию, когда в UI появлялся лишний `tut`.
|
|
||||||
- Дубли `running/creating` на одном autotest-инстансе.
|
|
||||||
- Неясный финальный статус, где было видно операцию, но не было понятно, над каким инстансом она выполнялась.
|
|
||||||
|
|
||||||
# v1.0.72 (27.07.2026) — service list keeps one autotest row per displayName
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` сервисный список теперь cloud-first и не склеивает разные строки в одну map.
|
|
||||||
- Tracker остаётся только как fallback, если cloud ещё не вернул конкретный autotest-инстанс.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Дубли `running/creating` на одном и том же autotest-инстансе.
|
|
||||||
|
|
||||||
# v1.0.71 (27.07.2026) — finished status shows instance name
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` финальная строка после `OK/FAIL` теперь показывает имя инстанса.
|
|
||||||
- Имя операции и длительность остаются рядом, отдельно от имени инстанса.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ситуацию, когда после завершения delete/modify было видно только название операции, но не ясно, над каким инстансом она выполнялась.
|
|
||||||
|
|
||||||
# v1.0.70 (27.07.2026) — dedupe autotest instances by displayName
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` сервисный список теперь склеивает записи по `displayName`.
|
|
||||||
- Если cloud уже отдал `running`, он выигрывает у tracker-fallback `creating`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Дубли одной и той же autotest-записи после CREATE, когда в списке появлялись и `running`, и `creating`.
|
|
||||||
|
|
||||||
## v1.0.69 (27.07.2026) — autotest prefix fixed in CREATE field
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` префикс `autotest-` отображается отдельным фиксированным текстом.
|
|
||||||
- Рядом с ним остаётся только редактируемый суффикс имени.
|
|
||||||
- При отправке CREATE имя собирается как `autotest-` + введённый хвост.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ситуацию, когда весь `displayName` показывался в одном редактируемом поле.
|
|
||||||
- Риск случайно стереть обязательный префикс `autotest-`.
|
|
||||||
|
|
||||||
## v1.0.68 (27.07.2026) — operation name shown after OK + muted operation buttons
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` финальный статус теперь показывает имя операции рядом со временем.
|
|
||||||
- Кнопки операций получили спокойные неброские оттенки по типу операции.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Неясность после завершения операции, когда было видно только `OK` и время без указания, что именно выполнялось.
|
|
||||||
- Слишком кислотный вид кнопок операций.
|
|
||||||
|
|
||||||
## v1.0.67 (27.07.2026) — autotest instances are shown by cloud prefix
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` список инстансов сервиса больше не отфильтровывается через `/tmp/instances.json`.
|
|
||||||
- Теперь в таблицу попадают все облачные инстансы с префиксом `autotest-`.
|
|
||||||
- Трекер оставлен только как fallback, если новый autotest-инстанс ещё не успел появиться в ответе облака.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ситуацию, когда autotest-инстанс уже есть в облаке, но не показывается в таблице из-за отсутствия записи в json.
|
|
||||||
|
|
||||||
## v1.0.66 (27.07.2026) — backend status resolver for operations
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` добавлен канонический резолвер статуса инстанса для `/api/operations/<svc_id>`.
|
|
||||||
- Backend теперь отдаёт `status` для инстансов, а UI читает именно его вместо угадывания по `explainedStatus`.
|
|
||||||
- В `site/templates/index.html` убрано раннее отображение «Готово» во время выполнения операции.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Путаницу между `creating`, `running` и фактическим статусом в UI.
|
|
||||||
- Ситуацию, когда фронтенд сам интерпретировал статусы и показывал не то состояние.
|
|
||||||
|
|
||||||
## v1.0.65 (27.07.2026) — cloud list pagination fix
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/operations/get_instances.py` пагинация списка инстансов теперь останавливается по размеру страницы, а не по `total`.
|
|
||||||
- Это убирает ситуацию, когда инстанс уже создан в облаке, но приложение не дочитало следующую страницу и показывает `creating`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизирована версия `1.0.65`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Ложный `creating` для уже существующего инстанса, если он попал не на первую страницу `/instances`.
|
|
||||||
|
|
||||||
## v1.0.64 (27.07.2026) — header version + editable displayName placeholder
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` версия теперь гарантированно попадает в `config.VERSION` даже после `load_config()`.
|
|
||||||
- В `site/templates/index.html` поле `displayName` остаётся редактируемым, но автотест-имя показывается как placeholder.
|
|
||||||
- При CREATE пользователь может ввести своё имя, а backend всё равно добавит `autotest-` и проверит уникальность.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` зафиксированы оба изменения.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Пропадающую версию в шапке.
|
|
||||||
- Неправильное поведение поля `displayName`, когда оно выглядело как готовое значение вместо подсказки.
|
|
||||||
|
|
||||||
## v1.0.63 (27.07.2026) — autotest namespace for instances
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` CREATE теперь генерирует `displayName` с префиксом `autotest-` и проверяет его на уникальность среди текущих инстансов.
|
|
||||||
- В `site/routes/api_test.py` backend тоже нормализует `displayName` с этим префиксом и, если нужно, добавляет суффикс для уникальности перед созданием в облаке.
|
|
||||||
- В `site/routes/main.py` список инстансов фильтруется по namespace `autotest-`, чтобы в UI попадали только тестовые инстансы приложения.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` зафиксирован переход на namespace-based фильтрацию.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Дубли `displayName` при CREATE.
|
|
||||||
- Чужие инстансы в списке.
|
|
||||||
- Зависимость от локального трекера как от основного признака принадлежности.
|
|
||||||
|
|
||||||
## v1.0.62 (27.07.2026) — immediate visual refresh after CREATE
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` после завершения CREATE список инстансов перерисовывается полностью.
|
|
||||||
- `refreshInstances()` больше не зависит от DOM-diff и сразу ставит новый инстанс в список.
|
|
||||||
- `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизированы на `1.0.62`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- После появления кнопки «Готово» созданный инстанс должен сразу быть виден в списке.
|
|
||||||
|
|
||||||
## v1.0.61 (27.07.2026) — immediate list rebuild after CREATE
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` `refreshInstances()` теперь всегда полностью перерисовывает список инстансов после завершения операции.
|
|
||||||
- После `OK` новый инстанс должен появляться в списке сразу, без зависимости от DOM-diff и без перезагрузки страницы.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` и `site/app.py` синхронизирована версия `1.0.61`.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Убрана ситуация, когда после CREATE статус уже `OK`, но в UI инстанс ещё не виден.
|
|
||||||
|
|
||||||
## v1.0.60 (27.07.2026) — full list rerender after CREATE
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` `refreshInstances()` теперь полностью перерисовывает список инстансов, а не пытается вставить только diff.
|
|
||||||
- После завершения CREATE новый инстанс должен появляться в списке сразу после `OK`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` добавлен отдельный шаг про полную перерисовку списка.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- Снята зависимость от частичного DOM-diff-а, из-за которого созданный инстанс мог не появляться сразу.
|
|
||||||
|
|
||||||
## v1.0.59 (27.07.2026) — restore tracked instance after reload
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/routes/main.py` tracked-инстансы, которых Nubes уже не отдает в ответе после reload, снова добавляются в список из `/tmp/instances.json`.
|
|
||||||
- В `site/templates/index.html` статус CREATE теперь показывает имя инстанса, а не только `create`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` зафиксирован новый шаг восстановления списка после F5.
|
|
||||||
|
|
||||||
### Что это чинит
|
|
||||||
- После F5 инстанс не пропадает из UI, если он есть в локальном трекере.
|
|
||||||
- В статусе выполнения видно, по какому displayName идет CREATE.
|
|
||||||
|
|
||||||
## v1.0.58 (27.07.2026) — version bump
|
|
||||||
|
|
||||||
- Поднята версия проекта до `1.0.58`.
|
|
||||||
- Синхронизированы заголовки в `site/app.py` и `DOCS/ARCHITECTURE.md`.
|
|
||||||
|
|
||||||
## v1.0.57 (27.07.2026) — UI hardening + history sync
|
|
||||||
|
|
||||||
### Что изменилось
|
|
||||||
- В `site/templates/index.html` финальное состояние кнопки вынесено в `setFinishedState()`, чтобы не держать логику завершения операции в двух местах.
|
|
||||||
- После завершения операции кнопка «Готово» теперь гарантированно сбрасывает обработчик через `btn.onclick = null`.
|
|
||||||
- Повторный CREATE через старый обработчик больше не воспроизводится.
|
|
||||||
- `displayName` больше не теряется в нормальном сценарии завершения CREATE.
|
|
||||||
- В `site/app.py` версия поднята до `1.0.57`.
|
|
||||||
- В `DOCS/ARCHITECTURE.md` отмечено закрытие бага с повторным CREATE и синхронизирован статус по `displayName`.
|
|
||||||
|
|
||||||
### Зачем это было сделано
|
|
||||||
- Убрать хрупкость вокруг состояния кнопки после завершения операции.
|
|
||||||
- Привести документацию и версию к текущему состоянию кода.
|
|
||||||
|
|
||||||
## v1.0.54 (27.07.2026) — Аудит #3: 4 бага исправлено
|
|
||||||
|
|
||||||
### Баг #1: `_client()` не автоопределял стенд (КРИТИЧЕСКИЙ)
|
|
||||||
- **Где:** `api_test.py:_client()` — использовал `current_app.config["NUBES_API_ENDPOINT"]` (raw env var)
|
|
||||||
- **Симптом:** main.py автоопределял стенд для главной, но POST /api/test падал с 401 если токен от другого стенда
|
|
||||||
- **Исправление:** `detect_endpoint()` вынесен в `api/http_client.py`, используется в `_client()`
|
|
||||||
- **Как пропустили:** анализировал main.py и api_test.py в изоляции, не сравнил
|
|
||||||
|
|
||||||
### Баг #2: `_find_uid()` возвращал не тот UUID (КРИТИЧЕСКИЙ)
|
|
||||||
- **Где:** `api_test.py:_find_uid()` — итерировал по ВСЕМ значениям dict, искал 36-символьную строку
|
|
||||||
- **Симптом:** `/instanceOperations` возвращает оба `instanceUid` + `instanceOperationUid`, функция брала первый попавшийся → `run` шёл на неправильный URL
|
|
||||||
- **Исправление:** ищет по конкретным ключам: `instanceOperationUid` → `instanceUid` → `uid` → `Uid`
|
|
||||||
- **Как пропустили:** не проверил реальный формат ответа API
|
|
||||||
|
|
||||||
### Баг #3: Сироты при ошибке params (СРЕДНИЙ)
|
|
||||||
- **Где:** `api_test.py`: порядок — `POST /instances` → `POST /instanceOperations` → params loop → `tracker_add`
|
|
||||||
- **Симптом:** если params падает — инстанс и операция созданы в Nubes, трекер пуст → сироты
|
|
||||||
- **Исправление:** `tracker_add` вызывается ДО params loop, сразу после получения `instanceUid`
|
|
||||||
- **Как пропустили:** не трассировал линейно порядок вызовов
|
|
||||||
|
|
||||||
### Баг #4: `flock` без таймаута (НИЗКИЙ)
|
|
||||||
- **Где:** `tracker.py:_locked_read/_write` — `fcntl.flock(fd, LOCK_EX)` без LOCK_NB
|
|
||||||
- **Симптом:** при зависшем процессе с локом все воркеры блокируются навсегда
|
|
||||||
- **Исправление:** `_acquire_lock()` с `LOCK_EX | LOCK_NB` + retry до 2 секунд
|
|
||||||
- **Как пропустили:** новый код flock не перечитал с нуля после добавления
|
|
||||||
|
|
||||||
### Архитектурное: `detect_endpoint` вынесен в http_client.py
|
|
||||||
- Раньше был в main.py → недоступен для api_test.py
|
|
||||||
- Теперь в `api/http_client.py` → оба модуля используют
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## v1.0.53 (27.07.2026) — Файловый трекер с fcntl.flock
|
|
||||||
- In-memory dict заменён на `/tmp/instances.json` с `fcntl.LOCK_EX`
|
|
||||||
- Multi-worker gunicorn: все воркеры читают/пишут один файл под локом
|
|
||||||
|
|
||||||
## v1.0.52 (27.07.2026) — `_tmpl()` helper
|
|
||||||
- Все переменные шаблона передаются во всех трёх return-путях index()
|
|
||||||
- Исправлен UndefinedError при «Выйти»
|
|
||||||
|
|
||||||
## v1.0.51 (27.07.2026) — Автоопределение стенда + UnboundLocalError
|
|
||||||
- `detect_endpoint()` в main.py: пробует токен против dev и test API
|
|
||||||
- `instance_groups = {}` инициализирован до if
|
|
||||||
- `_finish_op` обёрнут в полный try/except
|
|
||||||
|
|
||||||
## v1.0.50 (27.07.2026) — showStages + api_operations fix
|
|
||||||
- `showStages()`: dtFinish вместо isSuccessful для ⏳/✅/❌
|
|
||||||
- `api_operations()`: не фильтровать "not created", добавлять tracked-сирот
|
|
||||||
|
|
||||||
## v1.0.49 (27.07.2026) — In-memory tracker + refreshInstances
|
|
||||||
- Трекер: in-memory dict вместо файла `/tmp/instances.json`
|
|
||||||
- `refreshInstances()` добавляет новые инстансы в DOM (не только бейджи)
|
|
||||||
- `selectService()` заменён на `refreshInstances()` после OK
|
|
||||||
|
|
||||||
## v1.0.48 (27.07.2026) — tracker_add синхронно
|
|
||||||
- `tracker_add` в `api_test()` до `threading.Thread`
|
|
||||||
- Лог `is_ok` в `_finish_op`
|
|
||||||
- Flexbox-кнопки горизонтально
|
|
||||||
|
|
||||||
## v1.0.47 (27.07.2026) — displayName fix
|
|
||||||
- displayName захватывается до очистки формы
|
|
||||||
- tracker_add до _op_results[OK]
|
|
||||||
- autotest-1 в _INITIAL
|
|
||||||
|
|
||||||
## v1.0.46 (27.07.2026) — _finish_op signature
|
|
||||||
- Добавлены op_name, svc_op_id в _finish_op()
|
|
||||||
|
|
||||||
## v1.0.45 и ранее
|
|
||||||
- Базовый CREATE/MODIFY/SUSPEND/DELETE/RESUME/REDEPLOY
|
|
||||||
- UI с этапами, поллинг, params форма
|
|
||||||
- Множественные баги CREATE flow (документированы в DOCS/api-create-flow.md)
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# GPT-5.6 Sol — План развития autotest MVP
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## 10 шагов
|
|
||||||
|
|
||||||
1. Удалить legacy: api.py, runner.py, CMDB DELETE
|
|
||||||
2. Единый исполнитель операций (operation_executor.py)
|
|
||||||
3. YAML-сценарии (loader + validator)
|
|
||||||
4. Разделение токенов (сервисные vs персональные)
|
|
||||||
5. Prod safety policy (серверные проверки)
|
|
||||||
6. PG как источник состояния (scenario_runs, scenario_steps)
|
|
||||||
7. Runner + API (последовательное выполнение)
|
|
||||||
8. UI: вкладки «Ручные», «Сценарии», «История»
|
|
||||||
9. Аудит: TIMEOUT в БД, refSvc fix, redact secrets, общая история
|
|
||||||
10. Проверка: тесты validator, executor, DB, smoke
|
|
||||||
|
|
||||||
## Мой порядок
|
|
||||||
|
|
||||||
Фаза 1 (база): шаги 1→2→9 (убрать мусор, единый executor, багфиксы)
|
|
||||||
Фаза 2 (ядро): шаги 4→5→6→3 (токены, policy, БД, YAML)
|
|
||||||
Фаза 3 (runner): шаг 7 (исполнение сценариев)
|
|
||||||
Фаза 4 (UI): шаг 8 (интерфейс)
|
|
||||||
Фаза 5: шаг 10 (тесты)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# GPT-5.6 Sol — Идеи развития + поиск багов
|
|
||||||
|
|
||||||
## Что такое autotest
|
|
||||||
|
|
||||||
Flask 3.0 + vanilla JS + PostgreSQL. Развёрнут на Nubes pythonk8s (managed).
|
|
||||||
Ручной инструмент для тестирования Nubes Cloud API — создание/изменение/удаление инстансов любых сервисов (PostgreSQL, Redis, S3, Kafka, ~35 сервисов).
|
|
||||||
|
|
||||||
## Что умеет сейчас (v1.1.44)
|
|
||||||
|
|
||||||
- Ручное создание/изменение/удаление инстансов через UI (как в облаке)
|
|
||||||
- Многопользовательский доступ (свой токен = свой стенд)
|
|
||||||
- История всех операций в PostgreSQL (who/what/when/status)
|
|
||||||
- Полный паритет с Nubes API (все 9 шагов create, валидация, normalize, refSvc)
|
|
||||||
|
|
||||||
## Что хочет заказчик (переписка)
|
|
||||||
|
|
||||||
Файл: `/home/naeel/nubes/autotest/TASKS/3007.md` — прочитай весь.
|
|
||||||
|
|
||||||
Коротко:
|
|
||||||
1. Автоматизированные тесты по конфигурируемым сценариям (не код, а конфиг)
|
|
||||||
2. Последовательности: create → modify → delete для выбранных сервисов
|
|
||||||
3. Общая история операций для всех пользователей
|
|
||||||
4. Учёт карантина (на проде удаление с задержкой 2 недели)
|
|
||||||
5. Тестовая организация (нельзя создавать org на проде)
|
|
||||||
|
|
||||||
## Файлы для анализа
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/TASKS/3007.md` — требования заказчика
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — весь фронтенд (460 строк)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — бэкенд (550 строк)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/app.py` — точка входа
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/db/save_run.py` — сохранение в БД
|
|
||||||
- `/home/naeel/nubes/autotest/HISTORY/2026-07-29-session.md` — что сделано за 2 дня
|
|
||||||
|
|
||||||
## Что нужно
|
|
||||||
|
|
||||||
### 1. Идеи по развитию (главное)
|
|
||||||
|
|
||||||
Исходя из требований заказчика и текущего состояния:
|
|
||||||
- Как должна выглядеть архитектура автотестов? YAML-конфиги → runner → БД?
|
|
||||||
- Как совместить ручной UI и автоматические сценарии?
|
|
||||||
- Как масштабировать на прод (карантин, тестовая организация)?
|
|
||||||
- Что делать с историей — дашборд, алерты при падениях?
|
|
||||||
- Несколько конкретных предложений (от минимального MVP до полной картины)
|
|
||||||
|
|
||||||
### 2. Баги (вторично)
|
|
||||||
|
|
||||||
Просмотри код. Найди что мы пропустили за 44 версии.
|
|
||||||
|
|
||||||
### Требования к ответу
|
|
||||||
|
|
||||||
- Конкретно. Без воды. Без "можно рассмотреть" и "в долгосрочной перспективе".
|
|
||||||
- Каждое предложение — что делать, какой файл менять, какой результат.
|
|
||||||
- Если несколько вариантов — перечислить с плюсами/минусами.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Сравнение: Sonnet 4.6 vs Opus 4.8 — полный код-ревью v1.1.20
|
|
||||||
|
|
||||||
## Совпадения (оба нашли)
|
|
||||||
|
|
||||||
| Баг | Sonnet | Opus |
|
|
||||||
|-----|--------|------|
|
|
||||||
| `_finish_op` args CREATE | 🔴 | 🔴 |
|
|
||||||
| `api_history()` connection leak | 🔴 | 🟠 |
|
|
||||||
| XSS displayName/svc | 🟡 | 🟡 |
|
|
||||||
| Мёртвый код/docstring'и | 🟠 | 🟢 |
|
|
||||||
|
|
||||||
## Только Opus (дополнительно)
|
|
||||||
|
|
||||||
| Баг | |
|
|
||||||
|-----|---|
|
|
||||||
| `detect_endpoint()` — 2-3 лишних HTTP на каждый запрос | 🟠 |
|
|
||||||
| `_op_results` in-memory dict — не переживает multi-worker | 🟠 |
|
|
||||||
| CSRF на POST (частично закрыто SameSite) | 🟡 |
|
|
||||||
| `currentSvcShort` без `let` — неявная глобальная | 🟢 |
|
|
||||||
| Дублирование рендера в selectService/refreshInstances | 🟢 |
|
|
||||||
| CREATE шаг 6: шаблон vs реальная операция | 🟢 |
|
|
||||||
|
|
||||||
## Только Sonnet
|
|
||||||
|
|
||||||
| Баг | |
|
|
||||||
|-----|---|
|
|
||||||
| pool.py `_initialized=True` до `init_db()` | 🔴 |
|
|
||||||
| Cookie без `secure=True` | 🟡 |
|
|
||||||
| MODIFY без required+default | 🟡 |
|
|
||||||
| pool.py race condition без мьютекса | 🟠 |
|
|
||||||
|
|
||||||
## Мой анализ
|
|
||||||
|
|
||||||
Opus 4.8 глубже — проверил эталонный Terraform flow, подтвердил что CREATE-флоу соответствует, нашёл проблемы с `detect_endpoint` и multi-worker. Sonnet нашёл pool.py баги, которые Opus пропустил.
|
|
||||||
|
|
||||||
**План исправлений (по приоритету):**
|
|
||||||
|
|
||||||
1. 🔴 `_finish_op` args — добавить `False` для `is_delete` в CREATE-ветку
|
|
||||||
2. 🔴 `api_history()` — `try/finally: put_conn(conn)`
|
|
||||||
3. 🔴 `pool.py` — `_initialized=True` после успешного `init_db()`
|
|
||||||
4. 🟡 XSS — `_esc()` для displayName/svc в рендере инстансов + истории + stages
|
|
||||||
5. 🟡 Cookie `secure=True`
|
|
||||||
6. 🟠 `currentSvcShort` → `let currentSvcShort`
|
|
||||||
|
|
||||||
Правки 1-3 критические — их делаю первыми. 4-6 следом.
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# Запрос к Claude (Sonnet): архитектурный аудит — Round 2
|
|
||||||
|
|
||||||
> **Контекст**: Round 1 анализ получен, исправления по приоритетам приняты.
|
|
||||||
> **Новое**: проект будет подключать **PostgreSQL** (отдельный сервис Nubes).
|
|
||||||
> **Дата**: 27.07.2026
|
|
||||||
> **Версия**: 1.0.89
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Контекст Round 1
|
|
||||||
|
|
||||||
Sonnet в первом раунде выявил:
|
|
||||||
- 🔴 Утечка `_op_results` — нужно TTL/очистка
|
|
||||||
- 🔴 Cookie без `httponly`/`samesite`
|
|
||||||
- 🟡 Дублирование `_client_id()` в main.py и api_test.py
|
|
||||||
- 🟡 Захардкожен `SVC_ID=1` — хотя `/api/services` существует
|
|
||||||
- 🟢 Рекомендация: SQLite для истории → **теперь PostgreSQL**
|
|
||||||
|
|
||||||
Все исправления из Round 1 приняты к реализации. Начинаем со среднего приоритета (общий модуль `auth.py`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Новый контекст: PostgreSQL
|
|
||||||
|
|
||||||
Проект получает отдельный PostgreSQL-сервис в Nubes. Нужно спроектировать:
|
|
||||||
1. **Схему БД** для хранения истории тестов
|
|
||||||
2. **Подключение** (connection pool для multi-worker gunicorn)
|
|
||||||
3. **Миграции** (как управлять схемой при обновлениях)
|
|
||||||
4. **API** для чтения/записи результатов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Вопросы Round 2
|
|
||||||
|
|
||||||
### 1. PostgreSQL — схема данных
|
|
||||||
|
|
||||||
Нужна схема для истории запусков. Минимальный набор полей:
|
|
||||||
- id, created_at
|
|
||||||
- client_id, stand (изоляция по пользователю и стенду)
|
|
||||||
- svc_id, svc_name
|
|
||||||
- op_name (create/modify/delete/...), svc_op_id
|
|
||||||
- instance_uid, display_name
|
|
||||||
- status (OK/FAIL/TIMEOUT), duration_sec
|
|
||||||
- error_log (текст ошибки)
|
|
||||||
- params (JSON — все параметры запуска)
|
|
||||||
- stages (JSON — этапы выполнения с длительностью)
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Какие индексы нужны? (по client_id+stand, по created_at, по instance_uid?)
|
|
||||||
- Нужна ли отдельная таблица для stages или JSON в основном поле?
|
|
||||||
- Нужна ли таблица `instances` (связь instance_uid → последний статус)?
|
|
||||||
- Как хранить чувствительные params (пароли, токены в map-параметрах)?
|
|
||||||
|
|
||||||
### 2. PostgreSQL — connection pool
|
|
||||||
|
|
||||||
Сейчас gunicorn с несколькими воркерами. Каждый воркер — отдельный процесс.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Какой connection pooler использовать? (psycopg2.pool? SQLAlchemy? asyncpg?)
|
|
||||||
- Где создавать пул? (в app.py при старте? lazy в каждом воркере?)
|
|
||||||
- Как передавать соединение в роуты? (flask.g? внедрение через blueprint?)
|
|
||||||
- Нужен ли `@app.teardown_appcontext` для возврата соединения в пул?
|
|
||||||
|
|
||||||
### 3. PostgreSQL — миграции
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Нужна ли полноценная миграция (Alembic) или простая схема в `init_db.py`?
|
|
||||||
- Где хранить SQL-файлы миграций? (отдельная папка `migrations/`?)
|
|
||||||
- Как применять при деплое? (автоматически при старте приложения?)
|
|
||||||
|
|
||||||
### 4. Объединение с текущим трекером
|
|
||||||
|
|
||||||
Сейчас трекер (`tracker.py`) хранит UID'ы в JSON-файлах `/tmp/`. После появления PostgreSQL:
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Заменяет ли PostgreSQL трекер полностью? (хранить UID'ы в БД вместо /tmp/)
|
|
||||||
- Или оставить трекер для краткосрочного кеша, а БД — для истории?
|
|
||||||
- Если заменить — нужна ли миграция существующих данных из /tmp/ в БД?
|
|
||||||
|
|
||||||
### 5. Унификация `auth.py`
|
|
||||||
|
|
||||||
Из Round 1: вынести `_client_id()`, `get_client()`, `get_stand()`, `get_token_info()` в общий модуль `api/auth.py`.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Использовать `flask.g` + `@bp.before_request` или явные вызовы функций?
|
|
||||||
- Как обрабатывать роуты без обязательного токена (GET / без авторизации)?
|
|
||||||
- Где создавать `HttpClient` — в `before_request` (на каждый запрос) или lazy?
|
|
||||||
|
|
||||||
### 6. Фронтенд — динамические сервисы
|
|
||||||
|
|
||||||
Сейчас `SVC_ID=1` захардкожен. Нужно загружать список из `/api/services`.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Показывать ВСЕ сервисы или только из whitelist в config.yaml?
|
|
||||||
- Какой UI для переключения между сервисами? (выпадающий список? вкладки?)
|
|
||||||
- При смене сервиса — сбрасывать `selectedInst` и форму параметров?
|
|
||||||
|
|
||||||
### 7. JS в отдельный файл
|
|
||||||
|
|
||||||
Вынести весь JS из `<script>` в `/static/app.js`.
|
|
||||||
|
|
||||||
Вопросы:
|
|
||||||
- Нужно ли использовать ES modules (`type="module"`) или классический скрипт?
|
|
||||||
- Если модули — как разбить на файлы (instances.js, params.js, log.js)?
|
|
||||||
- Как передавать Jinja2-переменные (config.VERSION, token_info) в JS-файл?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Файлы для изучения (если нужно)
|
|
||||||
|
|
||||||
Те же что в Round 1, плюс:
|
|
||||||
- `DOCS/HISTORY.md` — обновлён результатами Round 1
|
|
||||||
- `DOCS/sonnet-architecture-review-v1.0.89.md` — запрос Round 1
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# Запрос к Claude (Sonnet): архитектурный аудит app-autotest v1.0.89
|
|
||||||
|
|
||||||
> **Цель**: получить анализ текущей архитектуры и рекомендации по развитию проекта.
|
|
||||||
> **Дата**: 27.07.2026
|
|
||||||
> **Версия**: 1.0.89
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. О проекте
|
|
||||||
|
|
||||||
### Что делает
|
|
||||||
Flask-приложение для ручного тестирования операций облачной платформы Nubes:
|
|
||||||
- CREATE — создать инстанс сервиса с параметрами
|
|
||||||
- MODIFY — изменить параметры существующего инстанса
|
|
||||||
- DELETE / SUSPEND / RESUME / REDEPLOY — управление инстансом
|
|
||||||
|
|
||||||
### Как работает сейчас
|
|
||||||
- Один HTML-файл (index.html) с ванильным JS — без фреймворков, без SPA
|
|
||||||
- Бэкенд: Flask + gunicorn (несколько воркеров)
|
|
||||||
- Все данные — из REST API Nubes (никакого локального кеша, кроме краткосрочного трекера)
|
|
||||||
- Трекер инстансов: JSON-файлы в /tmp/ под fcntl.flock
|
|
||||||
- Автоопределение стенда (dev/test) по JWT-токену
|
|
||||||
|
|
||||||
### Текущая файловая структура
|
|
||||||
```
|
|
||||||
app-autotest/
|
|
||||||
├── site/
|
|
||||||
│ ├── app.py ← Flask(__name__), blueprints, /health
|
|
||||||
│ ├── api/
|
|
||||||
│ │ └── http_client.py ← HttpClient, автостенд
|
|
||||||
│ ├── operations/
|
|
||||||
│ │ ├── get_params.py ← слияние state.params + шаблон
|
|
||||||
│ │ ├── get_services.py ← API-обёртки
|
|
||||||
│ │ ├── get_instances.py ← пагинация
|
|
||||||
│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── main.py ← GET/POST /, /api/operations/<svc_id>
|
|
||||||
│ │ ├── api.py ← [LEGACY] раннер тестов
|
|
||||||
│ │ └── api_test.py ← /api/test, /api/params, /api/log
|
|
||||||
│ ├── templates/
|
|
||||||
│ │ └── index.html ← весь UI (~400 строк)
|
|
||||||
│ └── static/
|
|
||||||
│ └── style.css
|
|
||||||
├── DOCS/
|
|
||||||
│ ├── ARCHITECTURE.md ← актуальная архитектура
|
|
||||||
│ ├── ARCHITECTURE.legacy.md ← старая версия
|
|
||||||
│ ├── HISTORY.md ← история версий
|
|
||||||
│ └── ... ← 12+ legacy-документов
|
|
||||||
└── secrets/ ← токены (не в git)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ключевые детали реализации
|
|
||||||
- **Multi-worker safety**: все общие данные в /tmp/ под fcntl.flock
|
|
||||||
- **CREATE vs non-CREATE**: раздельные потоки в бэкенде и фронтенде
|
|
||||||
- **Текущие значения параметров**: GET /instances/{uid} → state.params + GET /instanceOperations/default/{opId} → слияние
|
|
||||||
- **Логирование**: /tmp/app-autotest.log + /api/log + скрытая UI-панель
|
|
||||||
- **Валидация**: HTML-escape + JSON-валидация map-полей (onblur + pre-submit)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Что нужно проанализировать
|
|
||||||
|
|
||||||
### A. Decoupling и модульность
|
|
||||||
Сейчас всё в одном HTML и одном api_test.py (300+ строк). Нужны рекомендации:
|
|
||||||
- Как разбить index.html на компоненты/модули?
|
|
||||||
- Как разбить api_test.py на независимые модули по операциям?
|
|
||||||
- Стоит ли переходить на микро-фронтенд (HTMX, Alpine.js, что-то ещё) или оставить ванильный JS?
|
|
||||||
- Как организовать роутинг если добавится больше страниц?
|
|
||||||
|
|
||||||
### B. Хранение данных тестов
|
|
||||||
Сейчас никакие результаты тестов не сохраняются. Нужно:
|
|
||||||
- Где и как хранить историю запусков (каждый запуск = запись)?
|
|
||||||
- Какие данные сохранять: параметры, длительность, статус, этапы, ошибки?
|
|
||||||
- Формат хранения: SQLite? JSON-файлы? Внешняя БД?
|
|
||||||
- Как привязать к конкретному инстансу и пользователю?
|
|
||||||
- Нужна ли страница истории/отчётов?
|
|
||||||
|
|
||||||
### C. Оптимизация для развития
|
|
||||||
- Что мешает добавить второй сервис (сейчас только Болванка, svcId=1)?
|
|
||||||
- Как сделать конфигурацию сервисов (какие сервисы показывать, какие операции)?
|
|
||||||
- Нужен ли config.yaml или всё из API?
|
|
||||||
- Как масштабировать на множество пользователей (изоляция данных)?
|
|
||||||
|
|
||||||
### D. Улучшения архитектуры
|
|
||||||
- Сейчас _client() определяется в двух местах (main.py и api_test.py) — как унифицировать?
|
|
||||||
- Стоит ли вынести общие хелперы (_client_id, _stand, _token_info) в отдельный модуль?
|
|
||||||
- Нужен ли middleware для токена (вместо ручного извлечения в каждом роуте)?
|
|
||||||
- Как улучшить обработку ошибок (сейчас много try/except с голым str(e))?
|
|
||||||
|
|
||||||
### E. Безопасность
|
|
||||||
- Токен передаётся в cookie и форме — достаточно ли этого?
|
|
||||||
- Нужна ли CSRF-защита для POST-запросов?
|
|
||||||
- Логи могут содержать чувствительные данные?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Ожидаемый формат ответа
|
|
||||||
|
|
||||||
### 1. Общая оценка
|
|
||||||
- Что сделано хорошо
|
|
||||||
- Что требует внимания
|
|
||||||
- Критические проблемы (если есть)
|
|
||||||
|
|
||||||
### 2. Decoupling — план
|
|
||||||
- Пошаговый план разбиения на модули
|
|
||||||
- Рекомендации по фронтенду (ванильный JS / HTMX / Alpine / SPA)
|
|
||||||
- Рекомендации по бэкенду (структура модулей)
|
|
||||||
|
|
||||||
### 3. Хранение данных тестов — план
|
|
||||||
- Схема данных (какие поля, какие таблицы/файлы)
|
|
||||||
- Технология (SQLite / PostgreSQL / JSON-файлы)
|
|
||||||
- API для чтения/записи
|
|
||||||
- UI для просмотра истории
|
|
||||||
|
|
||||||
### 4. Оптимизация — конкретные шаги
|
|
||||||
- Приоритизированный список улучшений
|
|
||||||
- Что можно сделать быстро, что требует переписывания
|
|
||||||
|
|
||||||
### 5. Риски и ограничения
|
|
||||||
- Что может пойти не так при предлагаемых изменениях
|
|
||||||
- Ограничения платформы (gunicorn, /tmp/, DDoS-Guard)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Файлы для изучения
|
|
||||||
|
|
||||||
**Код (в порядке важности):**
|
|
||||||
1. `site/routes/api_test.py` — основной модуль операций
|
|
||||||
2. `site/templates/index.html` — весь фронтенд
|
|
||||||
3. `site/routes/main.py` — главная страница и API операций
|
|
||||||
4. `site/api/http_client.py` — HTTP-клиент и автостенд
|
|
||||||
5. `site/operations/get_params.py` — слияние параметров
|
|
||||||
6. `site/operations/tracker.py` — файловый трекер
|
|
||||||
7. `site/app.py` — точка входа
|
|
||||||
8. `site/operations/get_services.py` + `get_instances.py` — API-обёртки
|
|
||||||
|
|
||||||
**Документация:**
|
|
||||||
1. `DOCS/ARCHITECTURE.md` — актуальная архитектура
|
|
||||||
2. `DOCS/HISTORY.md` — история версий
|
|
||||||
3. `DOCS/terraform-operations-full-logic.md` — логика API из Terraform-провайдера
|
|
||||||
|
|
||||||
**Данные (HAR-файлы):**
|
|
||||||
1. `development/dummycreate.har` — полный flow CREATE
|
|
||||||
2. `development/dummymodify.har` — полный flow MODIFY
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Sonnet 4.6 — Финальный аудит: баги и действия пользователя
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
Версия: v1.1.43
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
За 2 дня: 43 версии. Множество правок. Только что нашли баг — в `toggleInstance` удалили `const opsEl = ...` при рефакторинге, упало с ReferenceError. Нужно найти ВСЕ подобные баги.
|
|
||||||
|
|
||||||
## Файлы для проверки (ВСЕ)
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — 460 строк, весь фронтенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — бэкенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` — HTML/CSS
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/api/http_client.py` — HTTP-клиент
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — параметры
|
|
||||||
|
|
||||||
## Что искать
|
|
||||||
|
|
||||||
### 1. ReferenceError / undefined variables
|
|
||||||
- Все ли переменные объявлены перед использованием?
|
|
||||||
- `opsEl`, `svcEl`, `btnSpan`, `inst`, `afterEl` — все ли определены в своей области видимости?
|
|
||||||
|
|
||||||
### 2. Все действия пользователя при busy=true
|
|
||||||
Проверь КАЖДУЮ функцию — не вызывает ли она `stopPoll()` до проверки `busy`? Не меняет ли DOM (stages-box, params-area) до проверки?
|
|
||||||
|
|
||||||
Функции для проверки:
|
|
||||||
- `toggleInstance(iuid)` — строка 76
|
|
||||||
- `selectService(svcId)` — строка 39
|
|
||||||
- `startCreate()` — строка 106
|
|
||||||
- `runOp(opName,opId)` — строка 128
|
|
||||||
- `showParams(opId,opName)` — строка 146 (вызывается из runOp, startCreate)
|
|
||||||
- `executeOp(params)` — строка 307
|
|
||||||
- `setFinishedState(...)` — строка 286
|
|
||||||
- `refreshInstances()` — строка 389
|
|
||||||
- `toggleHistory()` — строка 410
|
|
||||||
- `toggleLog()` — строка 456
|
|
||||||
|
|
||||||
### 3. Пустые catch / потерянные ошибки
|
|
||||||
- `showParams` — есть `.catch()` ✅
|
|
||||||
- `executeOp` — есть `catch(e)` + busy ✅
|
|
||||||
- `loadHistory` — есть `catch(e)` ✅
|
|
||||||
- `startCreate` — есть `catch(e)` с fallback ✅
|
|
||||||
- Поллинг в executeOp — `catch(e)` со счётчиком ✅
|
|
||||||
- Есть ли другие `try/catch` или `fetch().then()` без обработки ошибок?
|
|
||||||
|
|
||||||
### 4. busy не сбрасывается
|
|
||||||
Все ли пути выхода из `executeOp` сбрасывают `busy=false`?
|
|
||||||
- FAIL: `busy=false` + return ✅
|
|
||||||
- CMDB OK: `busy=false` + setFinishedState ✅
|
|
||||||
- Поллинг завершился: `setFinishedState` → `busy=false` ✅
|
|
||||||
- catch: `busy=false` ✅
|
|
||||||
- Сетевая ошибка в поллинге: счётчик → `setFinishedState` ✅
|
|
||||||
|
|
||||||
### 5. DOM-элементы
|
|
||||||
- Все ли `getElementById` ссылаются на существующие ID в index.html?
|
|
||||||
- `ops-${iuid}` — генерируется динамически в `selectService` и `refreshInstances`
|
|
||||||
- `param-displayname` — генерируется в `showParams`
|
|
||||||
- Все ли статические ID присутствуют?
|
|
||||||
|
|
||||||
### 6. Параметры операций
|
|
||||||
- `runOp` → `showParams` — правильно ли для ВСЕХ типов операций?
|
|
||||||
- `showParams` → `opHeader` для не-create — показывается всегда ✅
|
|
||||||
- `showParams` → `createHeader` для create — показывается ✅
|
|
||||||
- Пустые params → `opHeader` + кнопка «Запустить» ✅
|
|
||||||
|
|
||||||
### 7. Любые другие баги
|
|
||||||
Что ещё могло сломаться за 43 версии?
|
|
||||||
@@ -1,509 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Полный обзор кода app-autotest — запрос к Sonnet
|
|
||||||
|
|
||||||
Отправлено: 27.07.2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Flask-приложение на Nubes pythonk8s (gunicorn, 1 worker, нет persistent volume).
|
|
||||||
Репозиторий: https://gitea.services.ngcloud.ru/forcloud/app-autotest.git
|
|
||||||
Деплой: git push → managed service редеплоит.
|
|
||||||
Текущая версия: v1.0.48.
|
|
||||||
|
|
||||||
## ПРОБЛЕМА
|
|
||||||
|
|
||||||
CREATE инстанса через UI: инстанс создаётся в Nubes (виден в UI платформы), НО в нашем списке инстансов НЕ появляется. Происходит СТАБИЛЬНО, 3 раза подряд (v1.0.46, v1.0.47, v1.0.48). Пробовали:
|
|
||||||
- v1.0.46: tracker_add в фоновом daemon-потоке _finish_op
|
|
||||||
- v1.0.47: tracker_add в _finish_op, но до установки _op_results[OK] (попытка победить гонку)
|
|
||||||
- v1.0.48: tracker_add синхронно в api_test() до запуска потока, с try/except: pass
|
|
||||||
|
|
||||||
НИ ОДИН вариант не сработал. Инстанс в трекере не появляется.
|
|
||||||
|
|
||||||
Текущий код на проде показывает 4 инстанса из _INITIAL:
|
|
||||||
```
|
|
||||||
curl-final-test (suspended)
|
|
||||||
curl-test-144834 (suspended)
|
|
||||||
autotest-1 (suspended)
|
|
||||||
autotest-1-first (running)
|
|
||||||
```
|
|
||||||
|
|
||||||
Новый инстанс (f192b10d-beb0-4a25-b79f-5dbd7de4712e, создан в 09:16) — отсутствует.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ВЕСЬ КОД
|
|
||||||
|
|
||||||
### 1. site/app.py — точка входа
|
|
||||||
```python
|
|
||||||
import os
|
|
||||||
from flask import Flask
|
|
||||||
from routes.main import bp as main_bp
|
|
||||||
from routes.api import bp as api_bp
|
|
||||||
from routes.api_test import bp as api_test_bp
|
|
||||||
|
|
||||||
VERSION = "1.0.48"
|
|
||||||
|
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
||||||
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
|
|
||||||
app.config["NUBES_API_TOKEN"] = os.getenv("NUBES_API_TOKEN", "")
|
|
||||||
app.config["VERSION"] = VERSION
|
|
||||||
app.register_blueprint(main_bp)
|
|
||||||
app.register_blueprint(api_bp)
|
|
||||||
app.register_blueprint(api_test_bp)
|
|
||||||
|
|
||||||
@app.route("/health")
|
|
||||||
def health():
|
|
||||||
return "OK"
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=5000)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. site/api/http_client.py — HTTP клиент
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
class HttpClient:
|
|
||||||
def __init__(self, endpoint, token):
|
|
||||||
self._endpoint = endpoint.rstrip("/")
|
|
||||||
self._session = requests.Session()
|
|
||||||
self._session.headers.update({
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"User-Agent": "Mozilla/5.0",
|
|
||||||
})
|
|
||||||
|
|
||||||
def get(self, path, **kwargs):
|
|
||||||
kwargs.setdefault("timeout", 10)
|
|
||||||
r = self._session.get(f"{self._endpoint}{path}", **kwargs)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
def post(self, path, data=None, **kwargs):
|
|
||||||
kwargs.setdefault("timeout", 30)
|
|
||||||
url = f"{self._endpoint}{path}"
|
|
||||||
r = self._session.post(url, json=(data if data is not None else {}), **kwargs)
|
|
||||||
if not r.ok:
|
|
||||||
raise Exception(f"POST {path}: {r.status_code} {r.reason}: {r.text[:200]}")
|
|
||||||
result = {}
|
|
||||||
try:
|
|
||||||
parsed = r.json()
|
|
||||||
if isinstance(parsed, dict):
|
|
||||||
result = parsed
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
loc = r.headers.get("Location", "")
|
|
||||||
if loc:
|
|
||||||
result["_location"] = loc
|
|
||||||
return result
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. site/operations/tracker.py — трекер инстансов
|
|
||||||
```python
|
|
||||||
"""Трекер созданных инстансов — только те что породило приложение."""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
_LOCK = threading.Lock()
|
|
||||||
_PATH = "/tmp/instances.json"
|
|
||||||
|
|
||||||
_INITIAL = {
|
|
||||||
"408b7f96-6ed7-4985-be1b-f5bdcb6ab44d": {"svcId": 1, "displayName": "autotest-1", "instanceUid": "408b7f96-6ed7-4985-be1b-f5bdcb6ab44d"},
|
|
||||||
"884356cf-5212-4395-b56b-27c58a5fc1fa": {"svcId": 1, "displayName": "autotest-1-first", "instanceUid": "884356cf-5212-4395-b56b-27c58a5fc1fa"},
|
|
||||||
"6528854d-a4ea-428c-9fa4-68e85fa9b3ac": {"svcId": 1, "displayName": "curl-test-144834", "instanceUid": "6528854d-a4ea-428c-9fa4-68e85fa9b3ac"},
|
|
||||||
"fdcb5887-39f7-4ef8-a9c0-d55e434a55ff": {"svcId": 1, "displayName": "curl-final-test", "instanceUid": "fdcb5887-39f7-4ef8-a9c0-d55e434a55ff"},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _load():
|
|
||||||
try:
|
|
||||||
with open(_PATH) as f:
|
|
||||||
return json.load(f)
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
|
||||||
data = dict(_INITIAL)
|
|
||||||
_save(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _save(data):
|
|
||||||
with open(_PATH, "w") as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
|
|
||||||
def add(instance_uid, svc_id, display_name):
|
|
||||||
with _LOCK:
|
|
||||||
data = _load()
|
|
||||||
data[instance_uid] = {
|
|
||||||
"svcId": svc_id,
|
|
||||||
"displayName": display_name,
|
|
||||||
"instanceUid": instance_uid,
|
|
||||||
}
|
|
||||||
_save(data)
|
|
||||||
|
|
||||||
def remove(instance_uid):
|
|
||||||
with _LOCK:
|
|
||||||
data = _load()
|
|
||||||
data.pop(instance_uid, None)
|
|
||||||
_save(data)
|
|
||||||
|
|
||||||
def list_all():
|
|
||||||
with _LOCK:
|
|
||||||
return list(_load().values())
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. site/routes/api_test.py — основной файл с проблемой
|
|
||||||
```python
|
|
||||||
from flask import Blueprint, current_app, jsonify, request
|
|
||||||
|
|
||||||
from api.http_client import HttpClient
|
|
||||||
from operations.get_services import get_services, get_service_detail
|
|
||||||
from operations.get_instances import get_instances
|
|
||||||
from operations.tracker import add as tracker_add, list_all as tracker_list
|
|
||||||
from operations.tracker import remove as tracker_remove
|
|
||||||
|
|
||||||
bp = Blueprint("api_test", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _client():
|
|
||||||
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
|
||||||
return HttpClient(current_app.config["NUBES_API_ENDPOINT"], token)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/services")
|
|
||||||
def api_services():
|
|
||||||
try:
|
|
||||||
raw = get_services(_client())
|
|
||||||
svc_list = [{"svcId": s["svcId"], "svc": s["svc"], "svcExtendedName": s.get("svcExtendedName", "")} for s in raw]
|
|
||||||
svc_list.sort(key=lambda s: s["svcId"])
|
|
||||||
return jsonify(svc_list)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/instances/list")
|
|
||||||
def api_instances_list():
|
|
||||||
try:
|
|
||||||
raw = get_instances(_client())
|
|
||||||
inst = [{"instanceUid": i["instanceUid"], "displayName": i["displayName"], "serviceId": i["serviceId"], "svc": i["svc"], "explainedStatus": i.get("explainedStatus", "?")} for i in raw]
|
|
||||||
return jsonify(inst)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/operations/<int:svc_id>")
|
|
||||||
def api_operations(svc_id):
|
|
||||||
try:
|
|
||||||
detail = get_service_detail(_client(), svc_id)
|
|
||||||
ops = detail.get("operations", [])
|
|
||||||
# только отслеживаемые инстансы этого сервиса
|
|
||||||
tracked = tracker_list()
|
|
||||||
tracked_uids = {t["instanceUid"] for t in tracked if t["svcId"] == svc_id}
|
|
||||||
instances = get_instances(_client())
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids
|
|
||||||
and i.get("explainedStatus") not in ("deleted", "not created")]
|
|
||||||
return jsonify({
|
|
||||||
"svc": detail.get("svc", ""),
|
|
||||||
"operations": [{"svcOperationId": o["svcOperationId"], "operation": o["operation"]} for o in ops],
|
|
||||||
"instances": svc_instances,
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/params/<int:op_id>")
|
|
||||||
def api_params(op_id):
|
|
||||||
try:
|
|
||||||
data = _client().get(f"/instanceOperations/default/{op_id}")
|
|
||||||
params = data["svcOperation"]["cfsParams"]
|
|
||||||
result = []
|
|
||||||
for p in params:
|
|
||||||
result.append({
|
|
||||||
"svcOperationCfsParamId": p["svcOperationCfsParamId"],
|
|
||||||
"name": p.get("svcOperationCfsParam", ""),
|
|
||||||
"dataType": p.get("dataType", ""),
|
|
||||||
"isRequired": p.get("isRequired", False),
|
|
||||||
"defaultValue": p.get("defaultValue"),
|
|
||||||
"valueList": p.get("valueList"),
|
|
||||||
"dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in p.get("dataDescriptor", {}).items()} if p.get("dataDescriptor") else None,
|
|
||||||
})
|
|
||||||
return jsonify(result)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/test", methods=["POST"])
|
|
||||||
def api_test():
|
|
||||||
"""Запустить операцию — возвращает opUid сразу, выполнение в фоне."""
|
|
||||||
import threading, time
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
svc_id = data["serviceId"]
|
|
||||||
op_name = data["operation"]
|
|
||||||
svc_op_id = data["svcOperationId"]
|
|
||||||
params = data.get("params", {})
|
|
||||||
instance_uid = data.get("instanceUid")
|
|
||||||
display_name = data.get("displayName", f"autotest-{svc_id}")
|
|
||||||
|
|
||||||
client = _client()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if op_name == "create":
|
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""}
|
|
||||||
resp = client.post("/instances", payload)
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
|
||||||
if not instance_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить instanceUid"}), 500
|
|
||||||
|
|
||||||
op_payload = {"instanceUid": instance_uid, "operation": "create"}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
|
||||||
if not op_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
|
||||||
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
|
|
||||||
# Записать в трекер СРАЗУ, до фонового потока
|
|
||||||
try:
|
|
||||||
tracker_add(instance_uid, svc_id, display_name)
|
|
||||||
except Exception:
|
|
||||||
pass # ← МОЛЧА ПРОГЛАТЫВАЕТ ОШИБКУ
|
|
||||||
|
|
||||||
# фоном ждать завершения
|
|
||||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True), daemon=True).start()
|
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
|
|
||||||
|
|
||||||
else:
|
|
||||||
if not instance_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400
|
|
||||||
|
|
||||||
if op_name == "redeploy":
|
|
||||||
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
|
||||||
if not op_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
else:
|
|
||||||
op_payload = {"instanceUid": instance_uid, "svcOperationId": svc_op_id, "operation": op_name}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload)
|
|
||||||
op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", ""))
|
|
||||||
if not op_uid:
|
|
||||||
return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run")
|
|
||||||
|
|
||||||
is_delete = (op_name == "delete")
|
|
||||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, is_delete), daemon=True).start()
|
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
return jsonify({"status": "FAIL", "error": str(e) + " | " + traceback.format_exc()[-200:]})
|
|
||||||
|
|
||||||
|
|
||||||
# Результаты фоновых операций: opUid → {status, error, stages, duration}
|
|
||||||
_op_results = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False):
|
|
||||||
"""Фоном ждать dtFinish и сохранить результат."""
|
|
||||||
import time
|
|
||||||
t0 = time.time()
|
|
||||||
deadline = t0 + 300
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
|
|
||||||
except Exception:
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "RUNNING",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
if dt_finish and str(dt_finish).strip():
|
|
||||||
is_ok = op.get("isSuccessful")
|
|
||||||
err = op.get("errorLog") or ""
|
|
||||||
print(f"[DEBUG] _finish_op op_uid={op_uid} isSuccessful={is_ok!r}", flush=True)
|
|
||||||
if is_ok and is_delete:
|
|
||||||
tracker_remove(instance_uid)
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "OK" if is_ok else "FAIL",
|
|
||||||
"error": str(err) if err else "",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
time.sleep(5)
|
|
||||||
_op_results[op_uid] = {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)}
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/test/status/<op_uid>")
|
|
||||||
def api_test_status(op_uid):
|
|
||||||
"""Получить текущий статус операции (поллинг с UI)."""
|
|
||||||
if op_uid in _op_results:
|
|
||||||
return jsonify(_op_results[op_uid])
|
|
||||||
try:
|
|
||||||
data = _client().get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
done = bool(dt_finish and str(dt_finish).strip())
|
|
||||||
return jsonify({
|
|
||||||
"status": "OK" if (done and op.get("isSuccessful")) else ("FAIL" if done else "RUNNING"),
|
|
||||||
"done": done,
|
|
||||||
"isSuccessful": op.get("isSuccessful"),
|
|
||||||
"isInProgress": op.get("isInProgress"),
|
|
||||||
"duration": op.get("duration"),
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"errorLog": op.get("errorLog"),
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
def _find_uid(d):
|
|
||||||
if isinstance(d, dict):
|
|
||||||
for k in ("instanceUid", "instanceOperationUid", "uid", "Uid"):
|
|
||||||
if k in d:
|
|
||||||
return d[k]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _uid_from_location(loc):
|
|
||||||
parts = loc.rstrip("/").split("/")
|
|
||||||
return parts[-1] if parts else None
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. site/routes/main.py — главная страница (фрагмент с /api/operations)
|
|
||||||
```python
|
|
||||||
@bp.route("/api/operations/<int:svc_id>")
|
|
||||||
def api_operations(svc_id):
|
|
||||||
try:
|
|
||||||
detail = get_service_detail(_client(), svc_id)
|
|
||||||
ops = detail.get("operations", [])
|
|
||||||
tracked = tracker_list()
|
|
||||||
tracked_uids = {t["instanceUid"] for t in tracked if t["svcId"] == svc_id}
|
|
||||||
instances = get_instances(_client())
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids
|
|
||||||
and i.get("explainedStatus") not in ("deleted", "not created")]
|
|
||||||
return jsonify({
|
|
||||||
"svc": detail.get("svc", ""),
|
|
||||||
"operations": [{"svcOperationId": o["svcOperationId"], "operation": o["operation"]} for o in ops],
|
|
||||||
"instances": svc_instances,
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. site/templates/index.html — UI (ключевые функции)
|
|
||||||
```javascript
|
|
||||||
// Инстансы + кнопки операций
|
|
||||||
async function selectService(svcId){
|
|
||||||
selectedInst=null; selectedOp=null; stopPoll();
|
|
||||||
document.getElementById('params-card').style.display='none';
|
|
||||||
document.getElementById('stages-box').style.display='none';
|
|
||||||
const r=await fetch('/api/operations/'+svcId);
|
|
||||||
const d=await r.json();
|
|
||||||
svcInstances=d.instances||[];
|
|
||||||
// ... рендерит список
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeOp(params){
|
|
||||||
stopPoll();
|
|
||||||
const displayName=document.getElementById('param-displayname')?.value||'autotest-1';
|
|
||||||
// ... очищает форму
|
|
||||||
const r=await fetch('/api/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
||||||
serviceId:SVC_ID,
|
|
||||||
operation:selectedOp.opName,
|
|
||||||
svcOperationId:selectedOp.opId,
|
|
||||||
params,
|
|
||||||
instanceUid:selectedInst||'',
|
|
||||||
displayName
|
|
||||||
})});
|
|
||||||
const d=await r.json();
|
|
||||||
if(d.status==='FAIL'){/* ошибка */ return;}
|
|
||||||
// start polling
|
|
||||||
const opUid=d.opUid;
|
|
||||||
pollTimer=setInterval(async()=>{
|
|
||||||
const sr=await fetch('/api/test/status/'+opUid);
|
|
||||||
const sd=await sr.json();
|
|
||||||
showStages(sd.stages||[]);
|
|
||||||
if(sd.status!=='RUNNING'){
|
|
||||||
stopPoll();
|
|
||||||
// ... показать результат
|
|
||||||
if(sd.status==='OK'){
|
|
||||||
if(selectedOp.opName==='create'){await selectService(SVC_ID);} // ← ПЕРЕЗАГРУЖАЕТ ВСЁ
|
|
||||||
else{await refreshInstances();}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshInstances(){
|
|
||||||
const r=await fetch('/api/operations/'+SVC_ID);
|
|
||||||
const d=await r.json();
|
|
||||||
svcInstances=d.instances||[];
|
|
||||||
// обновить только бейджи статусов
|
|
||||||
svcInstances.forEach(i=>{
|
|
||||||
const el=document.querySelector(`[data-iuid="${i.instanceUid}"] .badge`);
|
|
||||||
if(el){
|
|
||||||
el.textContent=i.explainedStatus||'?';
|
|
||||||
el.className='badge '+(i.explainedStatus==='running'?'badge-success':'');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ЧТО ПРОИСХОДИТ ПРИ CREATE (трассировка)
|
|
||||||
|
|
||||||
1. UI: `executeOp()` → POST `/api/test` `{serviceId:1, operation:"create", svcOperationId:18, params:{...}, instanceUid:"", displayName:"autotest-1-lq5x3a"}`
|
|
||||||
2. `api_test()`: `op_name="create"`, `svc_id=1` (int), `display_name="autotest-1-lq5x3a"` (str)
|
|
||||||
3. `client.post("/instances", ...)` → ответ от Nubes → `instance_uid = "f192b10d-beb0-4a25-b79f-5dbd7de4712e"`
|
|
||||||
4. `client.post("/instanceOperations", ...)` → `op_uid = "d489348e-5d92-47cd-97f7-25f1c4d65ffc"`
|
|
||||||
5. Параметры → `client.post("/instanceOperationCfsParams", ...)` для каждого
|
|
||||||
6. `client.post("/instanceOperations/{op_uid}/run")` → запуск
|
|
||||||
7. **`tracker_add("f192b10d-...", 1, "autotest-1-lq5x3a")`** ← ЗДЕСЬ ПРОБЛЕМА
|
|
||||||
8. `threading.Thread(target=_finish_op, ...)` → фон
|
|
||||||
9. Возврат `{status:"RUNNING", opUid:"d489348e-...", instanceUid:"f192b10d-..."}`
|
|
||||||
10. UI поллит `/api/test/status/d489348e-...`
|
|
||||||
11. `_finish_op` получает `isSuccessful=true` → `_op_results[opUid] = {status:"OK", ...}`
|
|
||||||
12. UI получает OK → вызывает `selectService(1)` → `/api/operations/1` → `tracker_list()` → 4 инстанса → **нового НЕТ**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ВОПРОСЫ
|
|
||||||
|
|
||||||
### Критический: почему tracker_add не работает?
|
|
||||||
|
|
||||||
1. Может ли `json.dump` в `/tmp/instances.json` падать молча на pythonk8s? (диск full, fs ro, quota)
|
|
||||||
2. Может ли `_save` записать, но `_load` прочитать старую версию из-за кеша ФС?
|
|
||||||
3. Может ли gunicorn preload создавать несколько копий модуля tracker.py с разными `_LOCK`?
|
|
||||||
4. Может ли `_INITIAL` перезаписывать файл при КАЖДОМ `_load`, если файл повреждён?
|
|
||||||
5. **ГЛАВНЫЙ ВОПРОС: как надёжно сохранять состояние на pythonk8s БЕЗ persistent volume?**
|
|
||||||
|
|
||||||
### Архитектурный
|
|
||||||
6. Не перейти ли на sqlite3 в `/tmp/`? Даст ли это атомарность?
|
|
||||||
7. Не заменить ли `/tmp/instances.json` на in-memory dict + seed из `_INITIAL` при старте? (без файла вообще)
|
|
||||||
8. Как правильно логировать ошибки на pythonk8s чтобы их было видно?
|
|
||||||
|
|
||||||
### UI
|
|
||||||
9. После успешного CREATE `selectService()` скрывает progress card — это бесит. Как обновить только список инстансов не скрывая stages?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что уже пробовали (НЕ ПОМОГЛО)
|
|
||||||
|
|
||||||
- v1.0.46: tracker_add в daemon-потоке _finish_op → поток умирает под gunicorn
|
|
||||||
- v1.0.47: tracker_add в _finish_op, но до _op_results[OK] → не помогло
|
|
||||||
- v1.0.48: tracker_add синхронно в api_test() ДО потока, try/except: pass → ОШИБКА СКРЫТА
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Sonnet 4.6 — Полный код-ревью v1.1.20
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
|
|
||||||
## 🔴 КРИТИЧЕСКИЙ БАГ #1 — `_finish_op`: неправильный порядок аргументов для CREATE
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py
|
|
||||||
|
|
||||||
Сигнатура: `def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, client_id, stand, is_delete=False, params=None, user_email="", app_version=""):`
|
|
||||||
|
|
||||||
Вызов CREATE передаёт 13 аргументов. `params` попадает на позицию `is_delete` → dict truthy → после успешного CREATE вызывается `tracker_remove` — инстанс НЕМЕДЛЕННО удаляется из трекера.
|
|
||||||
|
|
||||||
## 🔴 ВАЖНЫЙ БАГ #2 — `api_history()`: утечка соединения
|
|
||||||
|
|
||||||
Нет `try/finally` — при исключении соединение не возвращается в пул. 5 утечек = пул исчерпан.
|
|
||||||
|
|
||||||
## 🔴 ВАЖНЫЙ БАГ #3 — pool.py: `_initialized = True` до `init_db()`
|
|
||||||
|
|
||||||
При сетевой ошибке `init_db()` не выполняется, но флаг уже True — схема не создастся до перезапуска воркера.
|
|
||||||
|
|
||||||
## 🟡 XSS — `displayName` и `svc` не экранируются в app.js
|
|
||||||
|
|
||||||
`_esc()` не применяется при рендере списка инстансов. Вектор через поле displayName.
|
|
||||||
|
|
||||||
## 🟡 Cookie без `secure=True`
|
|
||||||
|
|
||||||
## 🟡 MODIFY — не досылаются required+default параметры (в отличие от CREATE)
|
|
||||||
|
|
||||||
## 🟠 Мёртвый код: runner.py (LEGACY), дубликат redeploy/else, устаревшие docstring'и
|
|
||||||
|
|
||||||
## 🟠 pool.py — race condition при создании пула без мьютекса
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# Полный код-ревью ВСЕГО проекта autotest
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Flask 3.0 + gunicorn (multi-worker) + vanilla JS + psycopg2 PostgreSQL.
|
|
||||||
Тестирует Nubes REST API — создаёт/удаляет инстансы облачных сервисов.
|
|
||||||
Версия: v1.1.20 (сегодня, после 20 правок за день).
|
|
||||||
За день сделано ~20 правок, накопился технический долг — нужен свежий взгляд.
|
|
||||||
|
|
||||||
## ВСЕ файлы (прочитай КАЖДЫЙ)
|
|
||||||
|
|
||||||
```
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/app.py — точка входа, регистрация blueprint'ов
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/runner.py — загрузка config.yaml
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/api/auth.py — get_token, get_client, get_client_id, get_stand
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/api/http_client.py — HttpClient, detect_endpoint, stand_name
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/db/pool.py — psycopg2 ThreadedConnectionPool (lazy)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/db/init_db.py — CREATE TABLE runs + миграции
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/db/save_run.py — INSERT в runs (16 колонок)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/get_instances.py — GET /instances (пагинация)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py — get_params_with_current_values, _normalize_value_list
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/get_services.py — GET /services, /services/{id}
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/service_list.py — load_service_ids из services_{stand}.txt
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/operations/tracker.py — fcntl.flock file tracker
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/routes/api.py — LEGACY эндпоинты
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py — ОСНОВНОЙ: /api/test, /api/params, /api/log, поллинг, _finish_op
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/routes/main.py — / (главная), /api/operations/{svc_id}
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/static/app.js — ВЕСЬ фронтенд (420 строк)
|
|
||||||
/home/naeel/nubes/autotest/app-autotest/site/templates/index.html — Jinja2 шаблон
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что нужно от тебя
|
|
||||||
|
|
||||||
1. **Найти ВСЕ баги** — синтаксические, логические, race conditions, необработанные исключения
|
|
||||||
2. **Проверить универсальность** — работает ли код для ВСЕХ сервисов (Redis 3 параметра, PostgreSQL 8 map-fixed параметров, Болванка)
|
|
||||||
3. **Проверить CREATE flow** — сверь с эталонным Terraform flow из /home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md
|
|
||||||
4. **Проверить MODIFY/SUSPEND/DELETE/RESUME** — нет ли там таких же проблем
|
|
||||||
5. **Проверить фронтенд** — все ли DOM ID совпадают, нет ли необработанных Promise, правильный ли порядок элементов
|
|
||||||
6. **Проверить на дубликаты** — нет ли мёртвого кода, дублирующихся эндпоинтов
|
|
||||||
7. **Проверить безопасность** — httponly cookie, токены, CORS
|
|
||||||
|
|
||||||
## Известные недавние баги (уже исправлены, но проверь что фиксы корректны)
|
|
||||||
|
|
||||||
- `_client()` → NameError (не было такой функции) → заменили на get_client()
|
|
||||||
- `-строка` → TypeError в сортировке → заменили на два sort()
|
|
||||||
- `_normalize_value_list` не импортирован → добавили импорт
|
|
||||||
- `</div>\`;` осиротевший HTML в JS → удалили
|
|
||||||
- `currentSvcName` вместо реального `i.svc` → исправили
|
|
||||||
- serviceId фильтр (убран, заменён на сортировку)
|
|
||||||
- dataDescriptor defaultValue не прокидывался → добавили
|
|
||||||
- required+default params не досылались (шаг 6 Terraform) → добавили
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Sonnet — почему ты пропустил эти баги?
|
|
||||||
|
|
||||||
Отправлено: 27.07.2026, после v1.0.49 (твой анализ выполнен, НЕ ПОМОГЛО)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ты предложил 4 правки — мы сделали. Инстанс ВСЁ РАВНО не появляется в списке. Плюс новый баг: ❌ вместо ⏳ для этапов в процессе.
|
|
||||||
|
|
||||||
## Ты НЕ заметил баг №1: showStages() — ❌ для выполняющихся этапов
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function showStages(stages){
|
|
||||||
stages.forEach(s=>{
|
|
||||||
const ok=s.isSuccessful;
|
|
||||||
const icon=ok===true?'✅':ok===false?'❌':'⏳'; // ← БАГ
|
|
||||||
```
|
|
||||||
|
|
||||||
API Nubes для этапа в процессе возвращает `isSuccessful: false` (или `null`).
|
|
||||||
Код интерпретирует `false === false` → ❌ (авария).
|
|
||||||
Но этап просто **ещё не завершился** — должен быть ⏳.
|
|
||||||
|
|
||||||
**Правильная логика:** проверять `dtFinish` этапа. Если `dtFinish` нет → ⏳.
|
|
||||||
Если есть и `isSuccessful === true` → ✅. Если есть и `false` → ❌.
|
|
||||||
|
|
||||||
Почему ты это пропустил? Код `showStages()` был в твоём обзоре.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ты НЕ заметил баг №2: api_operations() теряет инстансы
|
|
||||||
|
|
||||||
```python
|
|
||||||
def api_operations(svc_id):
|
|
||||||
tracked = tracker_list() # ← 5 UID (включая новый)
|
|
||||||
tracked_uids = {t["instanceUid"] for t in tracked if t["svcId"] == svc_id}
|
|
||||||
instances = get_instances(_client()) # ← Nubes API: только 4 старых
|
|
||||||
svc_instances = [i for i in instances
|
|
||||||
if i.get("instanceUid") in tracked_uids # ← новый UID НЕ НАЙДЕН
|
|
||||||
and i.get("explainedStatus") not in ("deleted", "not created")]
|
|
||||||
```
|
|
||||||
|
|
||||||
`tracker_add` работает (in-memory, мгновенно). Но `get_instances()` из Nubes API **не сразу** возвращает только что созданный инстанс.
|
|
||||||
Фильтр требует чтобы инстанс был В ОБОИХ источниках. Новый UID есть в `tracked_uids` но отсутствует в `instances` → выпадает.
|
|
||||||
|
|
||||||
Результат: список всегда показывает только старые инстансы, новый — никогда.
|
|
||||||
|
|
||||||
**Правильная логика:** инстансы из трекера, отсутствующие в `get_instances()`, добавлять напрямую со статусом `"creating"`.
|
|
||||||
|
|
||||||
Почему ты это пропустил? Код `api_operations()` был в твоём обзоре, полностью.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Что мы имеем после твоих рекомендаций
|
|
||||||
|
|
||||||
| Версия | Что делали | Результат |
|
|
||||||
|--------|-----------|-----------|
|
|
||||||
| v1.0.48 | tracker_add синхронно, except:pass | ❌ инстанса нет |
|
|
||||||
| v1.0.49 | in-memory dict (твоя рекомендация) | ❌ инстанса нет |
|
|
||||||
| v1.0.49 | refreshInstances вместо selectService | ❌ stages ушли, но инстанса нет |
|
|
||||||
|
|
||||||
Трекер РАБОТАЕТ — in-memory dict, добавление мгновенное.
|
|
||||||
НО фильтр `api_operations()` ВЫБРАСЫВАЕТ новый инстанс потому что Nubes API его ещё не проиндексировал.
|
|
||||||
|
|
||||||
## Вопрос
|
|
||||||
|
|
||||||
Ты проанализировал ВЕСЬ код, включая `api_operations()` и `showStages()`.
|
|
||||||
Как ты мог пропустить оба этих бага, которые видны при простой трассировке CREATE-потока?
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Полный код-ревью и вопрос: CREATE пропускает required-параметры
|
|
||||||
|
|
||||||
## Что такое autotest
|
|
||||||
|
|
||||||
Flask 3.0 + gunicorn + vanilla JS. Тестирует Nubes REST API — создаёт/удаляет инстансы облачных сервисов (Redis, PostgreSQL, S3, etc.). Всё через API: POST /instances, POST /instanceOperations, POST /instanceOperationCfsParams, POST /run.
|
|
||||||
|
|
||||||
## Текущий CREATE-флоу (код)
|
|
||||||
|
|
||||||
Файл: `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py`, функция `api_test()`, строки 209-225:
|
|
||||||
|
|
||||||
```python
|
|
||||||
display_name = _unique_display_name(client, display_name)
|
|
||||||
descr = f"created by autotest v{current_app.config.get('VERSION', '')}"
|
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": descr}
|
|
||||||
resp = client.post("/instances", payload) # шаг 1
|
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) ...
|
|
||||||
|
|
||||||
op_payload = {"instanceUid": instance_uid, "operation": "create"}
|
|
||||||
op_resp = client.post("/instanceOperations", op_payload) # шаг 2
|
|
||||||
op_uid = _find_uid(op_resp) ...
|
|
||||||
|
|
||||||
tracker_add(...)
|
|
||||||
|
|
||||||
for pid, pval in params.items(): # шаг 3 — ТОЛЬКО пользовательские
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
client.post(f"/instanceOperations/{op_uid}/run") # шаг 4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Эталонный флоу (Terraform-провайдер)
|
|
||||||
|
|
||||||
Файл: `/home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md` (скопирован из репы tf_provider)
|
|
||||||
|
|
||||||
```
|
|
||||||
Шаг 1: POST /instances {serviceId, displayName, descr}
|
|
||||||
Шаг 2: POST /instanceOperations {instanceUid, operation:"create"}
|
|
||||||
Шаг 3: GET /instanceOperations/{opUid}?fields=cfsParams ← получить ВСЕ params с defaults
|
|
||||||
Шаг 4: resolveRefSvcParamValues ← резолв refSvcId
|
|
||||||
Шаг 5: POST /instanceOperationCfsParams ← пользовательские params (×N)
|
|
||||||
Шаг 6: POST /instanceOperationCfsParams ← required params с defaultValue, не переданные в шаге 5 (×M) ⬅ НЕТ В AUTOTEST
|
|
||||||
Шаг 7: GET /instanceOperations/{opUid}/validate-cfs
|
|
||||||
Шаг 8: POST /instanceOperations/{opUid}/run {}
|
|
||||||
Шаг 9: поллинг dtFinish
|
|
||||||
```
|
|
||||||
|
|
||||||
## Баг
|
|
||||||
|
|
||||||
PostgreSQL (сервис 90, create opId=19) имеет **8 map-fixed параметров**, все required:
|
|
||||||
- startupConfiguration (id=789)
|
|
||||||
- clusterConfiguration (id=788)
|
|
||||||
- accessConfiguration (id=790)
|
|
||||||
- postgresConfiguration (id=791)
|
|
||||||
- postgresConf (id=792) — array-map-fixed
|
|
||||||
- backupConfiguration (id=793)
|
|
||||||
- autoscaleConfiguration (id=794)
|
|
||||||
- mtlsConfiguration (id=1094)
|
|
||||||
|
|
||||||
Пользователь в UI заполняет первые 3-4. Остальные 4-5 **не отправляются** → Nubes возвращает: «Не передан параметр: serviceInstanceUid».
|
|
||||||
|
|
||||||
Redis (3 параметра), Болванка (все заполняются) — работают.
|
|
||||||
|
|
||||||
## Нужное решение
|
|
||||||
|
|
||||||
Добавить **шаг 6** в CREATE-ветку: после отправки пользовательских `params`, дослать required-параметры с их `defaultValue`, которые не были в пользовательском вводе.
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
|
|
||||||
1. **Где брать список required-параметров с defaultValue?**
|
|
||||||
- Вариант А: сделать `GET /instanceOperations/default/{svcOperationId}` (тот же что для /api/params) — там есть все params с `isRequired` и `defaultValue`. Взять те, где `isRequired=True` и `svcOperationCfsParamId` нет в `params.keys()`.
|
|
||||||
- Вариант Б: сделать `GET /instanceOperations/{opUid}?fields=cfsParams` как Terraform (шаг 3) — после создания операции, получить cfsParams от сервера. Но это доп. запрос.
|
|
||||||
- Какой правильнее?
|
|
||||||
|
|
||||||
2. **Как отличить «пользователь заполнил» от «не заполнил»?** Сейчас params приходят из фронтенда как `{"788": "{\"cpu\":\"500\",...}"}`. Если поле не в params.keys() — значит юзер не заполнил. Но что если юзер оставил пустую строку? Достаточно проверки `pid not in params` или нужно проверять значение?
|
|
||||||
|
|
||||||
3. **Нужны ли шаги 3 (GET cfsParams) и 7 (validate)?** Terraform их делает. Без validate-cfs мы пропускаем серверную валидацию до /run. Это критично или нет?
|
|
||||||
|
|
||||||
4. **Не сломает ли это существующие сервисы?** Если добавить досылку required+default для ВСЕХ сервисов — не навредит ли это Redis/Болванке где и так всё работает?
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# Sonnet 4.6 — Полный анализ: параметризованные операции инстанса + поиск багов
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
Версия: v1.1.36
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Flask 3.0 + gunicorn + vanilla JS. Тестирует Nubes REST API.
|
|
||||||
|
|
||||||
PostgreSQL создался успешно. Но при попытке `create_user` — ошибка: параметры не были запрошены у пользователя, отправлен пустой `{}`.
|
|
||||||
|
|
||||||
## 🔴 Проблема 1: runOp() не показывает форму параметров для не-modify операций
|
|
||||||
|
|
||||||
**Файл:** `/home/naeel/nubes/autotest/app-autotest/site/static/app.js`, функция `runOp()`, строка 127:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function runOp(opName,opId){
|
|
||||||
if(busy) return;
|
|
||||||
stopPoll();
|
|
||||||
selectedOp={opId,opName,svcId:currentSvcId};
|
|
||||||
if(opName==='modify'){
|
|
||||||
showParams(opId,opName); // ← только modify показывает форму
|
|
||||||
}else{
|
|
||||||
// confirm and execute immediately
|
|
||||||
if(!confirm(`Запустить ${opName} для ${findInstName()}?`)) return;
|
|
||||||
executeOp({}); // ← пустые params → API ругается "Missing required parameter"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Проблема:** PostgreSQL имеет много операций с параметрами:
|
|
||||||
- `create_user` — нужен username
|
|
||||||
- `delete_user` — нужен username
|
|
||||||
- `create_database` — нужны параметры БД
|
|
||||||
- `delete_database` — нужны параметры
|
|
||||||
- `restart` — может иметь параметры
|
|
||||||
- `recovery` — параметры восстановления
|
|
||||||
- `create_backup` — параметры бэкапа
|
|
||||||
|
|
||||||
Все они сейчас идут с `executeOp({})` — пустые params.
|
|
||||||
|
|
||||||
**Операции БЕЗ параметров** (можно confirm → execute):
|
|
||||||
- `delete`
|
|
||||||
- `suspend`
|
|
||||||
- `resume`
|
|
||||||
- `redeploy`
|
|
||||||
- `reconcile`
|
|
||||||
|
|
||||||
**Вопрос:** Как универсально определить, нужны ли параметры для операции?
|
|
||||||
- Вариант А: проверять `cfsParams` в `/api/operations/{svcId}` — если есть params → показать форму
|
|
||||||
- Вариант Б: список известных "безпараметровых" операций (delete/suspend/resume/redeploy/reconcile) — для них confirm, для остальных — showParams
|
|
||||||
- Вариант В: всегда показывать форму (showParams сам разберётся если params пустые)
|
|
||||||
|
|
||||||
## 🔴 Проблема 2: refreshInstances() ДОЛЖЕН обновлять список инстансов после create_user и подобных
|
|
||||||
|
|
||||||
После `create_user` инстанс не меняется, но список инстансов всё равно надо обновить (в UI могут быть связанные изменения). Сейчас `refreshInstances()` вызывается только при `sd.status === 'OK'`.
|
|
||||||
|
|
||||||
**Файл:** `/home/naeel/nubes/autotest/app-autotest/site/static/app.js`, executeOp polling (строка ~357):
|
|
||||||
```javascript
|
|
||||||
if(sd.status==='OK'){
|
|
||||||
await refreshInstances(); // только при OK
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Вопрос:** Нужно ли вызывать refreshInstances при любом завершении (OK или FAIL)?
|
|
||||||
|
|
||||||
## Полный аудит всех оставшихся проблем
|
|
||||||
|
|
||||||
Пожалуйста, прочитай ВСЕ файлы и найди ЛЮБЫЕ оставшиеся баги или неустойчивости:
|
|
||||||
|
|
||||||
**Файлы для проверки:**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — весь фронтенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — бэкенд
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` — главная + /api/operations
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/api/http_client.py` — HTTP-клиент
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — параметры
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` — HTML шаблон
|
|
||||||
|
|
||||||
**Что искать:**
|
|
||||||
1. Все места где параметры операций не запрашиваются у пользователя
|
|
||||||
2. Все места где busy lock может застрять
|
|
||||||
3. Необработанные ошибки (пустые catch, пропущенные исключения)
|
|
||||||
4. Несоответствия между JS и бэкендом (разные имена полей, форматы)
|
|
||||||
5. Проблемы с `currentSvcShort` — где объявлена, где используется
|
|
||||||
6. Любые другие баги которые мы пропустили за 35+ версий
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Вопрос к Sonnet — архитектура трекера инстансов + UI кнопок
|
|
||||||
|
|
||||||
Отправлено: 27.07.2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Ты — senior архитектор. Разбери проблему в Flask-приложении на Nubes pythonk8s (gunicorn, 1 worker).
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
|
|
||||||
Приложение app-autotest — веб-интерфейс для автотестов операций облачных сервисов Nubes.
|
|
||||||
Репозиторий: https://gitea.services.ngcloud.ru/forcloud/app-autotest.git
|
|
||||||
Версия: v1.0.47
|
|
||||||
|
|
||||||
## Файлы для изучения
|
|
||||||
|
|
||||||
| Файл | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `site/app.py` | Точка входа Flask, VERSION, blueprints |
|
|
||||||
| `site/routes/api_test.py` | POST /api/test (запуск), GET /api/test/status (поллинг), _finish_op (фоновый поток) |
|
|
||||||
| `site/routes/main.py` | Главная страница, /api/operations/<svcId> (список инстансов из трекера) |
|
|
||||||
| `site/operations/tracker.py` | Запись/чтение /tmp/instances.json (_INITIAL, add, remove, list_all) |
|
|
||||||
| `site/templates/index.html` | UI: executeOp(), showStages(), поллинг, refreshInstances() |
|
|
||||||
| `site/api/http_client.py` | HTTP-клиент (Bearer auth, User-Agent) |
|
|
||||||
|
|
||||||
Полные пути в репозитории:
|
|
||||||
```
|
|
||||||
app-autotest/site/app.py
|
|
||||||
app-autotest/site/routes/api_test.py
|
|
||||||
app-autotest/site/routes/main.py
|
|
||||||
app-autotest/site/operations/tracker.py
|
|
||||||
app-autotest/site/templates/index.html
|
|
||||||
app-autotest/site/api/http_client.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поток CREATE (как работает сейчас)
|
|
||||||
|
|
||||||
1. UI: пользователь жмёт «Создать» → showParams(18, 'create') → заполняет форму → executeOp(params)
|
|
||||||
2. UI отправляет POST /api/test `{serviceId:1, operation:"create", svcOperationId:18, displayName, params, instanceUid:""}`
|
|
||||||
3. Бэкенд api_test():
|
|
||||||
- `client.post("/instances", {serviceId, displayName})` → получает instanceUid
|
|
||||||
- `client.post("/instanceOperations", {instanceUid, operation:"create"})` → получает opUid
|
|
||||||
- `client.post("/instanceOperationCfsParams", ...)` — для каждого параметра
|
|
||||||
- `client.post("/instanceOperations/{opUid}/run")` — запуск
|
|
||||||
- `threading.Thread(target=_finish_op, args=(client, opUid, instanceUid, ...), daemon=True).start()`
|
|
||||||
- возвращает `{status:"RUNNING", opUid, instanceUid}`
|
|
||||||
4. UI начинает поллинг: `GET /api/test/status/<opUid>` каждые 2 секунды
|
|
||||||
5. _finish_op (фоновый daemon-поток):
|
|
||||||
- поллит `GET /instanceOperations/{opUid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages`
|
|
||||||
- обновляет `_op_results[opUid]` (in-memory dict)
|
|
||||||
- когда `dtFinish != null` и `isSuccessful == true`:
|
|
||||||
- **вызывает tracker_add(instanceUid, svcId, displayName)** ← ПРОБЛЕМА ЗДЕСЬ
|
|
||||||
- устанавливает `_op_results[opUid] = {status:"OK", stages, duration}`
|
|
||||||
- таймаут 300 секунд
|
|
||||||
6. UI при status=="OK": вызывает `selectService(SVC_ID)` → `GET /api/operations/1` → читает tracker → показывает список
|
|
||||||
|
|
||||||
## ПРОБЛЕМА
|
|
||||||
|
|
||||||
Инстанс создаётся в Nubes (виден в UI платформы), НО в списке инстансов приложения НЕ появляется.
|
|
||||||
`tracker_add` не вызывается → `/tmp/instances.json` не обновляется → инстанс не в списке.
|
|
||||||
|
|
||||||
**Происходило 2 раза подряд** (v1.0.46 и v1.0.47). Оба раза инстанс в Nubes есть, в трекере — нет.
|
|
||||||
|
|
||||||
## Моя гипотеза
|
|
||||||
|
|
||||||
tracker_add вызывается внутри daemon-потока _finish_op. В gunicorn:
|
|
||||||
- Воркер может быть перезапущен платформой в любой момент
|
|
||||||
- Daemon-потоки умирают вместе с воркером — молча, без логов
|
|
||||||
- Python не пишет traceback при смерти daemon-потока
|
|
||||||
|
|
||||||
## Текущий код _finish_op (api_test.py, строки 153-195)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, is_create, is_delete=False):
|
|
||||||
"""Фоном ждать dtFinish и сохранить результат."""
|
|
||||||
import time
|
|
||||||
t0 = time.time()
|
|
||||||
deadline = t0 + 300
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages")
|
|
||||||
except Exception:
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
op = data.get("instanceOperation", {})
|
|
||||||
dt_finish = op.get("dtFinish")
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "RUNNING",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
if dt_finish and str(dt_finish).strip():
|
|
||||||
is_ok = op.get("isSuccessful")
|
|
||||||
err = op.get("errorLog") or ""
|
|
||||||
# Сначала трекер — чтобы UI при poll уже видел инстанс
|
|
||||||
if is_ok:
|
|
||||||
if is_create:
|
|
||||||
tracker_add(instance_uid, svc_id, display_name)
|
|
||||||
elif is_delete:
|
|
||||||
tracker_remove(instance_uid)
|
|
||||||
_op_results[op_uid] = {
|
|
||||||
"status": "OK" if is_ok else "FAIL",
|
|
||||||
"error": str(err) if err else "",
|
|
||||||
"stages": op.get("stages", []),
|
|
||||||
"duration": round(time.time() - t0, 1),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
time.sleep(5)
|
|
||||||
_op_results[op_uid] = {"status": "TIMEOUT", "duration": round(time.time() - t0, 1)}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
|
|
||||||
### 1. Где вызывать tracker_add?
|
|
||||||
|
|
||||||
Вариант А: синхронно в api_test(), сразу после получения instanceUid от API (шаг 3), до запуска потока.
|
|
||||||
- Плюс: гарантированная запись, инстанс в трекере мгновенно
|
|
||||||
- Минус: если операция потом упадёт — в трекере «мусорный» инстанс (но это лучше чем отсутствие)
|
|
||||||
|
|
||||||
Вариант Б: subprocess.Popen вместо threading.Thread.
|
|
||||||
- Плюс: процесс живёт независимо от gunicorn worker
|
|
||||||
- Минус: сложнее, нужен IPC для _op_results
|
|
||||||
|
|
||||||
Вариант В: sqlite3 с WAL-режимом.
|
|
||||||
- Плюс: атомарная запись, конкурентный доступ
|
|
||||||
- Минус: без persistent volume данные теряются при редеплое (как и /tmp/)
|
|
||||||
|
|
||||||
### 2. Что делать с _op_results?
|
|
||||||
|
|
||||||
Сейчас это module-level dict. При нескольких gunicorn workers — каждый worker имеет свой dict. Нужен ли переход на sqlite/file-based storage для _op_results?
|
|
||||||
|
|
||||||
### 3. Надёжность под gunicorn
|
|
||||||
|
|
||||||
Как правильно организовать фоновую работу под gunicorn на pythonk8s (1 worker, нет PV, нет Redis/RabbitMQ)?
|
|
||||||
|
|
||||||
## Дополнительно: UI кнопок
|
|
||||||
|
|
||||||
Нужен CSS чтобы кнопки операций шли горизонтально в 1-2 ряда, как в Nubes UI:
|
|
||||||
|
|
||||||
```
|
|
||||||
delete | modify | suspend | resume | redeploy | reconcile
|
|
||||||
```
|
|
||||||
|
|
||||||
Сейчас они в vertical списке внутри `div.inst-ops`. Предложи стиль.
|
|
||||||
|
|
||||||
## Ограничения платформы
|
|
||||||
|
|
||||||
- `site/` — НЕ пакет (без __init__.py, конфликт с stdlib site.py)
|
|
||||||
- Импорты без префикса site.: `from api.http_client import ...`
|
|
||||||
- `app.run(host="0.0.0.0", port=5000)` — обязательно
|
|
||||||
- Gunicorn запускается платформой, предположительно 1 worker
|
|
||||||
- Нет persistent volume — данные в /tmp/ теряются при редеплое
|
|
||||||
- Деплой: git push → managed service подхватывает → редеплой
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Sonnet 4.6 — Финальный аудит
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## 5 багов в app.js
|
|
||||||
|
|
||||||
| # | Баг | Строка |
|
|
||||||
|---|-----|--------|
|
|
||||||
| 🔴1 | `d.operations.filter` без null-guard | toggleInstance |
|
|
||||||
| 🔴2 | toggleInstance без try/catch | toggleInstance |
|
|
||||||
| 🔴3 | selectService без try/catch | selectService |
|
|
||||||
| 🔴4 | refreshInstances без try/catch | refreshInstances |
|
|
||||||
| 🟡5 | CMDB OK + refreshInstances → ошибка перезаписывает успех | executeOp |
|
|
||||||
|
|
||||||
## redeploy без _send_params_terraform
|
|
||||||
Намеренно — redeploy переиспользует текущие params из state.
|
|
||||||
|
|
||||||
## Бэкенд чист
|
|
||||||
api_test.py, main.py, get_params.py, http_client.py — багов нет.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Sonnet 4.6 — Параметры операций + аудит багов
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## 🔴 Баг 1: runOp() — форма только для modify
|
|
||||||
app.js:127 — все кроме modify идут с executeOp({}). create_user, delete_user и др. получают пустые params.
|
|
||||||
|
|
||||||
**Решение (Вариант C):** Всегда showParams. Если params пустые — показать заголовок + кнопку без полей.
|
|
||||||
|
|
||||||
## 🔴 Баг 2: CMDB-delete не чистит трекер
|
|
||||||
api_test.py — после успешного CMDB инстанс в трекере → показывается как "creating" вечно.
|
|
||||||
|
|
||||||
## 🟠 Баг 3: Сетевая ошибка в поллинге → busy навсегда
|
|
||||||
app.js — fetch в setInterval без try/catch → silent reject → busy stuck.
|
|
||||||
|
|
||||||
## 🟡 Баг 4: refreshInstances только при OK
|
|
||||||
app.js:357 — после FAIL список не обновляется.
|
|
||||||
|
|
||||||
## 🟢 Баг 5: currentSvcId=1 хардкод
|
|
||||||
Первый сервис ≠ 1 → рассинхрон UI и данных.
|
|
||||||
|
|
||||||
## 🟢 Баг 6: detect_endpoint ×3-4 на запрос
|
|
||||||
auth.py — кэшировать через flask.g.
|
|
||||||
|
|
||||||
## Приоритет
|
|
||||||
1. Баг 2 — 1 строка
|
|
||||||
2. Баг 1 — основная задача (форма для всех операций)
|
|
||||||
3. Баг 3 — busy stuck
|
|
||||||
4. Баг 4 — одна строка
|
|
||||||
5. Баги 5-6 — отдельно
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Sonnet 4.6 — Round 2 Response
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
|
|
||||||
## Ответы
|
|
||||||
|
|
||||||
### 1. Фильтр serviceId ✅
|
|
||||||
Добавить `if i.get("serviceId") != svc_id: continue` — безопасно.
|
|
||||||
Tracker-fallback уже отфильтрован по svc_id, не сломается.
|
|
||||||
Даже улучшит дедупликацию cloud_names.
|
|
||||||
|
|
||||||
### 2. Генерация 3 символов
|
|
||||||
Надёжнее: `(Math.random() * 46656 | 0).toString(36).padStart(3, '0')`
|
|
||||||
Ровно 3 символа, равномерно по 46 656 комбинациям.
|
|
||||||
|
|
||||||
### 3. descr
|
|
||||||
Не нужно тащить через JS! `app_version` уже есть в той же функции.
|
|
||||||
Одна строка: `"descr": f"created by autotest v{app_version}"`
|
|
||||||
|
|
||||||
### 4. Пропущенный баг: stand-ключ трекера
|
|
||||||
main.py → `stand_name(endpoint)`
|
|
||||||
api_test.py → `get_stand()`
|
|
||||||
Могут разойтись → tracker_add и tracker_list под разными ключами → creating инстансы не видны.
|
|
||||||
|
|
||||||
### 5. Дубликат эндпоинта
|
|
||||||
Удалить `/api/operations/{svc_id}` из api_test.py (строки 108-148).
|
|
||||||
Недостижим, старый код, техдолг.
|
|
||||||
|
|
||||||
## Итоговый план исправлений
|
|
||||||
|
|
||||||
| # | Файл | Что |
|
|
||||||
|---|------|-----|
|
|
||||||
| 1 | main.py:194 | `if i.get("serviceId") != svc_id: continue` |
|
|
||||||
| 2 | main.py:227 | `"svcShort": detail.get("svcShort", "")` |
|
|
||||||
| 3 | app.js:53 | `currentSvcShort = d.svcShort\|\|''` |
|
|
||||||
| 4 | app.js:113-116 | Переписать `makeCreateDisplayName()` |
|
|
||||||
| 5 | api_test.py:212 | `f"created by autotest v{app_version}"` |
|
|
||||||
| 6 | api_test.py:108-148 | Удалить дубликат эндпоинта |
|
|
||||||
| 7 | main.py + api_test.py | Унифицировать stand |
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Sonnet 4.6 — Root Cause: serviceInstanceUid
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
|
|
||||||
## Найдена корневая причина
|
|
||||||
|
|
||||||
**Разница в источнике данных для шага 6 (required+default params):**
|
|
||||||
|
|
||||||
| | Terraform | Autotest |
|
|
||||||
|---|---|---|
|
|
||||||
| Источник | `GET /instanceOperations/{opUid}?fields=cfsParams` — **реальная операция** | `GET /instanceOperations/default/{svc_op_id}` — **шаблон** |
|
|
||||||
| Данные | `paramValue` (автоматически заполнен Nubes) + `defaultValue` | Только `defaultValue` |
|
|
||||||
|
|
||||||
`serviceInstanceUid` — параметр, который Nubes **автоматически добавляет** в реальную операцию (шаг 3 Terraform). В шаблоне его НЕТ. Поэтому наш код его не видит → не отправляет → validate-cfs падает.
|
|
||||||
|
|
||||||
## Исправление
|
|
||||||
|
|
||||||
В `api_test.py` CREATE-ветка (строка ~210): заменить
|
|
||||||
```python
|
|
||||||
tmpl = client.get(f"/instanceOperations/default/{svc_op_id}")
|
|
||||||
```
|
|
||||||
на
|
|
||||||
```python
|
|
||||||
op_details = client.get(f"/instanceOperations/{op_uid}?fields=cfsParams")
|
|
||||||
```
|
|
||||||
И брать `paramValue` первым, `defaultValue` как fallback — как Terraform.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Sonnet 4.6 — state.out valueList fix
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
|
|
||||||
## Ответ
|
|
||||||
|
|
||||||
### Решение верное, но с ошибкой
|
|
||||||
- `"user" in code` → `state_out["users"]` ✅
|
|
||||||
- `"owner" in code` → `state_out["users"]` ✅ (НЕ databases!)
|
|
||||||
- `"db" in code` → `state_out["databases"]` ✅
|
|
||||||
|
|
||||||
### Попутный баг
|
|
||||||
MERGED-путь (get_params.py) не нормализует `valueList` через `_normalize_value_list`. DEFAULT-путь (api_test.py) — нормализует.
|
|
||||||
|
|
||||||
## План (2 изменения в get_params.py)
|
|
||||||
|
|
||||||
1. Читать `state_out`, заполнять пустые `valueList`: user/owner→users, db→databases
|
|
||||||
2. Нормализовать `value_list` через `_normalize_value_list()`
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Sonnet 4.6 — Code Review v1.1.11
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
Модель: Claude Sonnet 4.6
|
|
||||||
|
|
||||||
## Ответ
|
|
||||||
|
|
||||||
### 🔴 Критично: NameError в api_test.py
|
|
||||||
|
|
||||||
Строки 159, 178, 180 вызывают:
|
|
||||||
- `get_params_with_current_values(...)`
|
|
||||||
- `_normalize_value_list(...)`
|
|
||||||
|
|
||||||
Оба символа определены в `operations/get_params.py`, но **не импортированы** в `routes/api_test.py`.
|
|
||||||
|
|
||||||
Цепочка падения:
|
|
||||||
1. `startCreate()` → `fetch('/api/params/{opId}')`
|
|
||||||
2. Сервер: `_normalize_value_list(p.get("valueList"))` → `NameError`
|
|
||||||
3. `except` → `{"error": "name '_normalize_value_list' is not defined"}`, HTTP 500
|
|
||||||
4. JS: `data.params` = undefined → fallback к `data` = `{"error": "..."}`
|
|
||||||
5. `params.map(...)` → `TypeError: params.map is not a function`
|
|
||||||
6. Нет `.catch()` → промис падает молча
|
|
||||||
|
|
||||||
Также сломаны MODIFY-запросы (строка 159 — `get_params_with_current_values`).
|
|
||||||
|
|
||||||
**Фикс:** одна строка импорта.
|
|
||||||
|
|
||||||
### 🟡 Вторичные баги в JS
|
|
||||||
|
|
||||||
1. `showParams()` — нет `.catch()` на fetch-цепочке → ошибки скрыты
|
|
||||||
2. `data.params||data` без `Array.isArray()` → TypeError при ошибке API
|
|
||||||
|
|
||||||
### ⚪ Косметика
|
|
||||||
|
|
||||||
`create-btn-area` после `params-area` в DOM — неудобно, но не ломает.
|
|
||||||
|
|
||||||
## Действия
|
|
||||||
|
|
||||||
- [x] Добавить импорт в api_test.py
|
|
||||||
- [x] Исправить клиентский код
|
|
||||||
- [x] Добавить `.catch()` в showParams
|
|
||||||
- [x] Проверить Array.isArray перед .map()
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Sonnet 4.6 — Round 2: оставшиеся баги и план исправлений
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
Контекст: v1.1.12 уже починил NameError и JS error handling. Сейчас v1.1.13 (мелкие UI-фиксы).
|
|
||||||
|
|
||||||
## Баги к исправлению
|
|
||||||
|
|
||||||
### 🔴 Баг 1: Инстансы всех сервисов показываются под любым выбранным
|
|
||||||
|
|
||||||
**Файл:** `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py`, строка 193-194, функция `api_operations(svc_id)`
|
|
||||||
|
|
||||||
```python
|
|
||||||
for i in instances:
|
|
||||||
display_name = str(i.get("displayName", "") or "")
|
|
||||||
if not display_name.startswith(AUTOTEST_PREFIX):
|
|
||||||
continue # не наш инстанс
|
|
||||||
# ← НЕТ проверки i.get("serviceId") != svc_id
|
|
||||||
```
|
|
||||||
|
|
||||||
Фильтр только по префиксу `autotest-`, без проверки что `instance.serviceId == svc_id`.
|
|
||||||
Результат: под сервисом «Container Registry» показываются S3 бакеты, под «Redis» — Flask инстансы.
|
|
||||||
|
|
||||||
**Предлагаемый фикс:** добавить `if i.get("serviceId") != svc_id: continue`
|
|
||||||
|
|
||||||
### 🟡 Задача 2: displayName → autotest-<svcShort>-<3rand>
|
|
||||||
|
|
||||||
Сейчас: `autotest-ms4gzydq` (timestamp base36)
|
|
||||||
Нужно: `autotest-redis-x7k`
|
|
||||||
|
|
||||||
**Данные:** API `/services/{id}` возвращает `svcShort=redis`
|
|
||||||
|
|
||||||
**План:**
|
|
||||||
1. `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` `/api/operations/{svc_id}` — добавить `svcShort` в JSON-ответ
|
|
||||||
2. `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` `selectService()` — сохранить `currentSvcShort = d.svcShort`
|
|
||||||
3. `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` `makeCreateDisplayName()` — `autotest-${currentSvcShort}-${3rand}`
|
|
||||||
4. Коллизии обрабатываются в `_unique_display_name()` на бэкенде
|
|
||||||
|
|
||||||
### 🟡 Задача 3: description (descr) при создании
|
|
||||||
|
|
||||||
HAR-файл (`development/dummycreate.har`) показывает что Nubes API принимает `descr`:
|
|
||||||
```json
|
|
||||||
POST /instances
|
|
||||||
{"serviceId":1, "displayName":"dummy-11255555", "descr":""}
|
|
||||||
```
|
|
||||||
|
|
||||||
В autotest `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` строка 212 уже шлёт `"descr": ""` — пустое. Нужно заполнить:
|
|
||||||
```
|
|
||||||
"descr": "created by autotest v1.1.13"
|
|
||||||
```
|
|
||||||
|
|
||||||
**План:**
|
|
||||||
1. `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` `executeOp()` — передать `descr` в тело запроса
|
|
||||||
2. `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` `/api/test` — принять `descr` из JSON, подставить в payload
|
|
||||||
|
|
||||||
## Вопросы к Sonnet
|
|
||||||
|
|
||||||
1. Верен ли план для бага 1? Не сломает ли фильтр по serviceId tracker-fallback логику?
|
|
||||||
2. Для задачи 2: как лучше генерировать 3 случайных символа? `Math.random().toString(36).slice(2,5)`? Не будет ли коллизий?
|
|
||||||
3. Для задачи 3: `descr` уже есть в payload. Достаточно ли просто подставить строку, или нужно что-то ещё?
|
|
||||||
4. Есть ли ещё какие-то баги, которые мы упустили?
|
|
||||||
5. `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` содержит ДУБЛИКАТ эндпоинта `/api/operations/{svc_id}` (строка 108-148), который никогда не вызывается из-за порядка регистрации blueprint'ов. Удалить ли его, или он нужен для чего-то?
|
|
||||||
|
|
||||||
**Файлы для проверки (только эти):**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` (строки 155-230)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` (строки 108-148, 195-280)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` (строки 85-130, 240-270)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Sonnet 4.6 — Round 3: missing required params in CREATE
|
|
||||||
|
|
||||||
Дата: 2026-07-29
|
|
||||||
Версия: v1.1.18
|
|
||||||
|
|
||||||
## Проблема
|
|
||||||
PostgreSQL create падает с ошибкой Nubes: «Не передан параметр: serviceInstanceUid».
|
|
||||||
Redis работает, Болванка работает, PG — нет.
|
|
||||||
|
|
||||||
## Что нашли
|
|
||||||
Terraform-провайдер делает дополнительный шаг после отправки пользовательских параметров:
|
|
||||||
отправляет ВСЕ required-параметры с их defaultValue, даже если юзер их не заполнял.
|
|
||||||
|
|
||||||
Наш код (api_test.py /api/test, CREATE-ветка, примерно строка 230):
|
|
||||||
```python
|
|
||||||
for pid, pval in params.items():
|
|
||||||
client.post("/instanceOperationCfsParams",
|
|
||||||
{"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)})
|
|
||||||
```
|
|
||||||
Шлёт ТОЛЬКО то, что пользователь заполнил в форме.
|
|
||||||
|
|
||||||
## PG имеет 8 map-fixed параметров, все required:
|
|
||||||
- startupConfiguration (id=789)
|
|
||||||
- clusterConfiguration (id=788)
|
|
||||||
- accessConfiguration (id=790)
|
|
||||||
- postgresConfiguration (id=791)
|
|
||||||
- postgresConf (id=792) — array-map-fixed
|
|
||||||
- backupConfiguration (id=793)
|
|
||||||
- autoscaleConfiguration (id=794)
|
|
||||||
- mtlsConfiguration (id=1094)
|
|
||||||
|
|
||||||
Если пользователь заполнил только первые 3 — остальные 5 НЕ отправляются → Nubes ругается.
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
1. Как лучше реализовать досылку required-параметров с defaultValue?
|
|
||||||
- Вариант А: до отправки params сделать GET /instanceOperations/default/{svcOpId}, взять все required с их defaultValue, смержить с пользовательскими
|
|
||||||
- Вариант Б: после отправки пользовательских params, дослать оставшиеся required с defaultValue
|
|
||||||
2. Terraform делает GET /instanceOperations/{opUid}?fields=cfsParams ПОСЛЕ создания операции. Нужно ли нам тоже?
|
|
||||||
3. Не сломает ли это существующие сервисы (Redis, Болванка) где параметров мало и все заполняются?
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Sonnet Code Review — app-autotest v1.1.11
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
Flask 3.0 + gunicorn (multi-worker) + vanilla JS фронтенд. Тестирует Nubes API (создание/удаление инстансов).
|
|
||||||
|
|
||||||
## Файлы для проверки (только эти, не надо лазить по всей репе)
|
|
||||||
|
|
||||||
1. **Фронтенд (основной баг!)**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — ВСЁ
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/templates/index.html` — структура DOM
|
|
||||||
|
|
||||||
2. **Бэкенд**
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — эндпоинты /api/params, /api/test
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/main.py` — эндпоинт /api/operations/<svc_id>
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — _normalize_value_list
|
|
||||||
|
|
||||||
3. **API-документация (для понимания что отдаёт Nubes)**
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/api-access.md`
|
|
||||||
- `/home/naeel/nubes/autotest/DOCS/api-create-flow.md`
|
|
||||||
|
|
||||||
## Конкретный баг
|
|
||||||
Кнопка "+ Создать новый инстанс" (функция `startCreate()` в app.js) не работает:
|
|
||||||
- При клике — `params-area` открывается пустой (display:block)
|
|
||||||
- Загрузка параметров молча не происходит
|
|
||||||
- Кнопка остаётся на месте, юзер думает что ничего не случилось
|
|
||||||
|
|
||||||
## Что нужно от Sonnet
|
|
||||||
1. Найти КОНКРЕТНУЮ причину почему `showParams` не рендерит форму. Проверить ВСЕ пути выполнения:
|
|
||||||
- `startCreate()` → fetch /api/operations/{svcId} → find create op → showParams(opId,'create')
|
|
||||||
- `showParams()` → fetch /api/params/{opId} → рендер createHeader + form
|
|
||||||
2. Нет ли race condition: `currentSvcName` может быть не установлен?
|
|
||||||
3. Нет ли ошибки в `params.map()` если API возвращает ошибку вместо массива?
|
|
||||||
4. `create-btn-area` сейчас ПОСЛЕ `params-area` в DOM — это нормально?
|
|
||||||
|
|
||||||
## Версия для справки
|
|
||||||
v1.1.11 — сегодняшние изменения: нормализация valueList CSV→array для вложенных dataDescriptor параметров.
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# Sonnet 4.6 — valueList из state.out для внутренних операций инстанса
|
|
||||||
|
|
||||||
Дата: 2026-07-30
|
|
||||||
Версия: v1.1.38
|
|
||||||
|
|
||||||
## Проблема
|
|
||||||
|
|
||||||
PostgreSQL создан. `create_user` отработал успешно (HTTP 201). Но при вызове `delete_user` — поле `username` показывает пустой выпадающий список (`valueList=[]`). Созданный юзер `uuu8` не виден.
|
|
||||||
|
|
||||||
## Что нашли
|
|
||||||
|
|
||||||
**GET /instances/{uid}** возвращает `state.out` с актуальными данными:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"state": {
|
|
||||||
"out": {
|
|
||||||
"users": {"uuu8": {"username": "uuu8", "role": "ddl_user", "rights": ["createdb", "createrole"]}},
|
|
||||||
"databases": {},
|
|
||||||
"backups": [],
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Шаблон** (`/instanceOperations/default/244` — delete_user) возвращает `valueList=[]`.
|
|
||||||
|
|
||||||
**Текущий код** — файл `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
def get_params_with_current_values(client, op_id, instance_uid):
|
|
||||||
inst_data = client.get(f"/instances/{instance_uid}")
|
|
||||||
state_params = inst_data.get("instance", {}).get("state", {}).get("params", {}) or {}
|
|
||||||
|
|
||||||
tmpl_data = client.get(f"/instanceOperations/default/{op_id}")
|
|
||||||
tmpl_params = tmpl_data.get("svcOperation", {}).get("cfsParams", []) or []
|
|
||||||
|
|
||||||
for p in tmpl_params:
|
|
||||||
...
|
|
||||||
result.append({
|
|
||||||
...
|
|
||||||
"valueList": value_list, # ← из шаблона, всегда []
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Читает `state.params` (конфигурация кластера), но НЕ читает `state.out` (пользователи, базы).
|
|
||||||
|
|
||||||
## Предлагаемое решение
|
|
||||||
|
|
||||||
После получения `inst_data`, извлечь `state.out`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
state_out = inst_data.get("instance", {}).get("state", {}).get("out", {}) or {}
|
|
||||||
```
|
|
||||||
|
|
||||||
В цикле слияния, если `valueList` пустой — попробовать заполнить из `state.out`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
value_list = p.get("valueList")
|
|
||||||
# Заполнить пустые valueList из state.out
|
|
||||||
if value_list is not None and not value_list:
|
|
||||||
code = p.get("svcOperationCfsParam", "").lower()
|
|
||||||
if "user" in code and isinstance(state_out.get("users"), dict):
|
|
||||||
value_list = sorted(state_out["users"].keys())
|
|
||||||
elif ("db" in code or "owner" in code) and isinstance(state_out.get("databases"), dict):
|
|
||||||
value_list = sorted(state_out["databases"].keys())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Вопросы
|
|
||||||
|
|
||||||
1. **Правильный ли подход?** Смотреть в `state.out.users` / `state.out.databases` для заполнения пустых valueList? Или есть другой источник?
|
|
||||||
|
|
||||||
2. **Какие ещё поля из state.out могут понадобиться?** Для других сервисов (MongoDB, Redis, Kafka) — есть ли там аналогичные подресурсы?
|
|
||||||
|
|
||||||
3. **Универсальность:** Достаточно ли проверки по имени параметра (`"user" in code`, `"db" in code`)? Или нужен более общий механизм (например, проверять все ключи state.out и матчить по имени)?
|
|
||||||
|
|
||||||
4. **Кеширование:** `get_params_with_current_values` вызывается при каждом открытии формы. GET /instances/{uid} — дорогой запрос. Стоит ли кешировать state.out?
|
|
||||||
|
|
||||||
5. **Есть ли другие баги** в этой же области, которые мы упустили?
|
|
||||||
|
|
||||||
## Файлы для анализа
|
|
||||||
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py` — основной файл
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py` — api_params (строка 120-155)
|
|
||||||
- `/home/naeel/nubes/autotest/app-autotest/site/static/app.js` — showParams (строка 145-260)
|
|
||||||
- Пример API-ответа: `curl -H "Authorization: Bearer $TOKEN" /instances/0a42bef3-7ce1-42b7-aa80-4274c4d9568f`
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Sonnet 4.6 — СВЕРКА С ТЕРРАФОРМОМ: почему autotest не работает а Terraform работает
|
|
||||||
|
|
||||||
## СУТЬ ПРОБЛЕМЫ
|
|
||||||
|
|
||||||
Terraform-провайдер создаёт PostgreSQL БЕЗ ошибок. Autotest (наш код) — падает с «Не передан параметр: serviceInstanceUid». Мы скопировали «похожую» логику, но где-то расхождение. Нужно найти ВСЕ расхождения, пошагово.
|
|
||||||
|
|
||||||
## ЭТАЛОН — Terraform CREATE flow
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md (читай ВЕСЬ, особенно строки 30-90)
|
|
||||||
|
|
||||||
```
|
|
||||||
Шаг 1: POST /instances {serviceId, displayName, descr}
|
|
||||||
Шаг 2: POST /instanceOperations {instanceUid, operation:"create"}
|
|
||||||
Шаг 3: GET /instanceOperations/{opUid}?fields=cfsParams ← получить ВСЕ params с defaults
|
|
||||||
Шаг 4: resolveRefSvcParamValues ← резолв refSvcId параметров
|
|
||||||
Шаг 5: POST /instanceOperationCfsParams ← пользовательские params (×N)
|
|
||||||
Шаг 6: POST /instanceOperationCfsParams ← required params с defaultValue, НЕ переданные в шаге 5 (×M)
|
|
||||||
Шаг 7: GET /instanceOperations/{opUid}/validate-cfs
|
|
||||||
Шаг 8: POST /instanceOperations/{opUid}/run {}
|
|
||||||
Шаг 9: поллинг dtFinish
|
|
||||||
```
|
|
||||||
|
|
||||||
## НАШ КОД — CREATE flow
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py, функция api_test(), строки 170-245
|
|
||||||
|
|
||||||
Прочитай ВЕСЬ файл, особенно:
|
|
||||||
- Строки 170-195 — создание instance + operation
|
|
||||||
- Строки 207-245 — отправка params + досылка required+default + validate + run
|
|
||||||
|
|
||||||
## HAR-файл — реальные API-вызовы Terraform
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/development/dummycreate.har
|
|
||||||
Это HAR с реальными запросами Terraform при создании Болванки (сервис 1).
|
|
||||||
Найди ВСЕ POST /instanceOperationCfsParams — посмотри КАКИЕ именно params отправляются, в КАКОМ порядке, с КАКИМИ svcOperationCfsParamId.
|
|
||||||
|
|
||||||
## PostgreSQL YAML — структура параметров
|
|
||||||
|
|
||||||
Файл: /home/naeel/nubes/autotest/STANDS/dev/resources_yaml/90_postgres.yaml
|
|
||||||
Читай строки с operations.create.params — там 8 map-fixed параметров с sub_params.
|
|
||||||
|
|
||||||
Также проверь через API что реально возвращается:
|
|
||||||
```
|
|
||||||
curl -H "Authorization: Bearer <TOKEN>" /instanceOperations/default/19
|
|
||||||
```
|
|
||||||
(create opId для PostgreSQL = 19)
|
|
||||||
|
|
||||||
## ЧТО НУЖНО НАЙТИ
|
|
||||||
|
|
||||||
1. **ШАГ 3 Terraform vs наш код**: Terraform делает GET /instanceOperations/{opUid}?fields=cfsParams ПОСЛЕ создания операции чтобы получить параметры С РЕЗОЛВОМ refSvcId. Мы берём из /instanceOperations/default/{svc_op_id} (шаблон). В чём разница? Может ли шаблон не содержать нужных значений которые появляются только в реальной операции?
|
|
||||||
|
|
||||||
2. **ШАГ 4 Terraform vs наш код**: resolveRefSvcParamValues — что это делает? Как Terraform резолвит refSvcId (например backupConfiguration.s3Uid ссылается на serviceId=12)? Есть ли у нас такой резолв?
|
|
||||||
|
|
||||||
3. **ШАГ 6 Terraform vs наш код**: Terraform досылает required+default. Мы тоже (v1.1.19). НО: Terraform берёт defaults из cfsParams РЕАЛЬНОЙ операции (шаг 3), а не из шаблона. Может ли быть расхождение?
|
|
||||||
|
|
||||||
4. **ПОРЯДОК отправки params**: В HAR, в каком порядке идут POST /instanceOperationCfsParams? Важен ли порядок?
|
|
||||||
|
|
||||||
5. **serviceInstanceUid**: Этого параметра НЕТ ни в YAML, ни в шаблоне API. Откуда он берётся? Может это внутренний параметр Nubes который появляется при резолве refSvcId?
|
|
||||||
|
|
||||||
6. **ПРОВЕРЬ ВЕСЬ НАШ КОД** на предмет любых других расхождений с Terraform flow. Не только CREATE — MODIFY, DELETE, SUSPEND, RESUME тоже.
|
|
||||||
|
|
||||||
## ИСХОДНИКИ TERRAFORM-ПРОВАЙДЕРА (читать только эти файлы!)
|
|
||||||
|
|
||||||
Это Go-код Terraform-провайдера Nubes. В нём ЭТАЛОННАЯ логика которая РАБОТАЕТ.
|
|
||||||
|
|
||||||
- /home/naeel/tf_provider/provider/internal/core/client.go — **главный**: HTTP-клиент, CreateGenericInstanceUniversalV6 (весь CREATE flow), RunInstanceOperationUniversal (MODIFY/DELETE/SUSPEND), doRequest (retry, заголовки)
|
|
||||||
- /home/naeel/tf_provider/provider/internal/core/refsvc_resolve.go — **resolveRefSvcParamValues**: как резолвятся refSvcId (s3Uid → UUID S3-инстанса)
|
|
||||||
- /home/naeel/tf_provider/provider/internal/core/instance_params.go — обработка параметров
|
|
||||||
|
|
||||||
## Файлы autotest (наш код — сравнивать с Terraform)
|
|
||||||
|
|
||||||
- /home/naeel/nubes/autotest/app-autotest/site/routes/api_test.py — ВЕСЬ: /api/test, /api/params, _finish_op
|
|
||||||
- /home/naeel/nubes/autotest/app-autotest/site/operations/get_params.py — get_params_with_current_values, _normalize_value_list
|
|
||||||
- /home/naeel/nubes/autotest/DOCS/terraform-operations-full-logic.md — эталонный flow (уже скопирован из client.go)
|
|
||||||
- /home/naeel/nubes/autotest/development/dummycreate.har — реальные запросы Terraform
|
|
||||||
- /home/naeel/nubes/autotest/STANDS/dev/resources_yaml/90_postgres.yaml — PG параметры
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# ⚠️ LEGACY — НЕАКТУАЛЬНО. Исторический документ.
|
|
||||||
|
|
||||||
# Пошаговый аудит кода — 27.07.2026 (v1.0.50)
|
|
||||||
|
|
||||||
## Методология
|
|
||||||
|
|
||||||
Полная трассировка CREATE-потока: UI → api_test() → _finish_op() → api_test_status() → refreshInstances() → api_operations(). Каждый if, try, except, присваивание.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Результат: найдено 3 бага
|
|
||||||
|
|
||||||
### 🔴 Баг #1 (КРИТИЧЕСКИЙ): Multi-worker gunicorn — in-memory dict не работает
|
|
||||||
|
|
||||||
**Файл:** `site/operations/tracker.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
_data = {
|
|
||||||
"408b7f96-...": {...}, # 4 initial items
|
|
||||||
}
|
|
||||||
|
|
||||||
def add(instance_uid, svc_id, display_name):
|
|
||||||
with _LOCK:
|
|
||||||
_data[instance_uid] = {...}
|
|
||||||
```
|
|
||||||
|
|
||||||
`_data` — module-level dict. У каждого gunicorn worker своя копия модуля → свой `_data`.
|
|
||||||
|
|
||||||
**Трассировка:**
|
|
||||||
1. POST /api/test → попадает на **воркер A**
|
|
||||||
2. `tracker_add(...)` → `_data` воркера A = 5 элементов ✅
|
|
||||||
3. GET /api/operations/1 → попадает на **воркер B**
|
|
||||||
4. `tracker_list()` → `_data` воркера B = 4 элемента ❌
|
|
||||||
|
|
||||||
**Почему это объясняет ВСЕ симптомы:**
|
|
||||||
- После F5 — рандомный воркер → опять 4
|
|
||||||
- v1.0.46-1.0.49 — ни одно решение не помогало (in-memory принципиально не跨-process)
|
|
||||||
- Инстанс в Nubes есть, в трекере нет — разные воркеры
|
|
||||||
|
|
||||||
**Решение:** вернуть файловый трекер (`/tmp/instances.json`) с межпроцессной блокировкой (`fcntl.flock` вместо `threading.Lock`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔴 Баг #2 (КРИТИЧЕСКИЙ): _finish_op() умирает молча на не-словаре
|
|
||||||
|
|
||||||
**Файл:** `site/routes/api_test.py`, строки 176-179
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _finish_op(...):
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
data = client.get(...) # ← except ловит ТОЛЬКО это
|
|
||||||
except Exception:
|
|
||||||
time.sleep(5)
|
|
||||||
continue
|
|
||||||
op = data.get("instanceOperation", {}) # ← если data не dict → AttributeError!
|
|
||||||
```
|
|
||||||
|
|
||||||
Если Nubes API возвращает `list`, `str`, `None` или любой не-словарь — `data.get()` → **AttributeError**. Этот except НЕ покрывает строку 179. Поток умирает молча. Python daemon-потоки не пишут traceback.
|
|
||||||
|
|
||||||
**Решение:** обернуть ВСЁ тело цикла (строки 166-195) в `try/except Exception: print(traceback)`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟡 Баг #3 (НЕКРИТИЧНЫЙ): _op_results — та же multi-worker проблема
|
|
||||||
|
|
||||||
**Файл:** `site/routes/api_test.py`, строка 152
|
|
||||||
|
|
||||||
```python
|
|
||||||
_op_results = {} # module-level
|
|
||||||
|
|
||||||
# В _finish_op (воркер A):
|
|
||||||
_op_results[op_uid] = {"status": "OK", ...}
|
|
||||||
|
|
||||||
# В api_test_status (воркер B):
|
|
||||||
if op_uid in _op_results: # ← False! (другой воркер)
|
|
||||||
return jsonify(_op_results[op_uid])
|
|
||||||
# fallback → прямой запрос в Nubes API
|
|
||||||
```
|
|
||||||
|
|
||||||
**Некритично** потому что есть fallback: если `_op_results` не содержит op_uid, `api_test_status()` делает прямой GET в Nubes API и возвращает статус. UI получает stages напрямую из Nubes, не из `_op_results`.
|
|
||||||
|
|
||||||
**НО:** fallback не обновляет `tracker_remove` для delete. При multi-worker delete не удалит инстанс из трекера.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Полная трассировка CREATE (все шаги)
|
|
||||||
|
|
||||||
| Шаг | Код | Результат | Статус |
|
|
||||||
|-----|-----|-----------|--------|
|
|
||||||
| 1 | `data = request.get_json()` | `svc_id=1, op_name="create", display_name="autotest-1-xxx"` | ✅ |
|
|
||||||
| 2 | `client.post("/instances", ...)` | `instance_uid = "f192b10d-..."` | ✅ |
|
|
||||||
| 3 | `client.post("/instanceOperations", ...)` | `op_uid = "d489348e-..."` | ✅ |
|
|
||||||
| 4 | `for pid,pval: client.post("/instanceOperationCfsParams", ...)` | Параметры установлены | ✅ |
|
|
||||||
| 5 | `client.post("/instanceOperations/{op_uid}/run")` | Операция запущена | ✅ |
|
|
||||||
| 6 | `tracker_add(instance_uid, svc_id, display_name)` | `_data[uid] = {...}` (in-memory) | ✅ |
|
|
||||||
| 7 | `threading.Thread(target=_finish_op, ...)` | Поток запущен | ✅ |
|
|
||||||
| 8 | `return {status:"RUNNING", opUid, instanceUid}` | Ответ UI | ✅ |
|
|
||||||
| 9 | UI poll `/api/test/status/<opUid>` | `_op_results[opUid]` или fallback API | ⚠️ разн. воркеры |
|
|
||||||
| 10 | `_finish_op` детектит `dtFinish` | `_op_results[opUid] = {status:"OK"}` | ⚠️ воркер A |
|
|
||||||
| 11 | UI: `refreshInstances()` → `/api/operations/1` | `tracker_list()` → 4 элемента | ❌ воркер B |
|
|
||||||
| 12 | `api_operations()` возвращает 4 инстанса | Нового нет | ❌ |
|
|
||||||
| 13 | F5 → `selectService(1)` → `/api/operations/1` | Опять 4 | ❌ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Дополнительные находки (не критические)
|
|
||||||
|
|
||||||
### ⚠️ params loop — orphaned resources при ошибке
|
|
||||||
Если `client.post("/instanceOperationCfsParams", ...)` падает на mid-param:
|
|
||||||
- Инстанс УЖЕ создан в Nubes (сирота)
|
|
||||||
- Операция УЖЕ создана (сирота)
|
|
||||||
- `tracker_add` НЕ вызван (он после цикла)
|
|
||||||
- Ответ: FAIL
|
|
||||||
|
|
||||||
### ⚠️ `data["svcOperationId"]` — KeyError если поле отсутствует
|
|
||||||
Вызов API без `svcOperationId` в JSON → KeyError → FAIL. Обработано внешним try/except.
|
|
||||||
|
|
||||||
### ⚠️ `int(pid)` — ValueError если param ID не число
|
|
||||||
`int("abc")` → ValueError → FAIL. Обработано внешним try/except.
|
|
||||||
|
|
||||||
### ⚠️ `_find_uid()` итерация по ВСЕМ значениям
|
|
||||||
Может случайно найти uid во вложенном объекте. Низкий риск, но нечисто.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## План исправлений для v1.0.51
|
|
||||||
|
|
||||||
| # | Что | Как |
|
|
||||||
|---|-----|-----|
|
|
||||||
| 1 | Файловый трекер с fcntl.flock | Вернуть `/tmp/instances.json`, заменить `threading.Lock` на `fcntl.flock` |
|
|
||||||
| 2 | _finish_op: try/except на всё тело | Обернуть строки 166-195 в `try/except: print(traceback)` |
|
|
||||||
| 3 | (опционально) `svc_id = int(data["serviceId"])` | Защита от строкового "1" в JSON |
|
|
||||||
@@ -8,7 +8,7 @@ Flask-приложение для автотестов сервисов Nubes.
|
|||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
||||||
**Полный обзор:** [DOCS/ARCHITECTURE-FULL.md](DOCS/ARCHITECTURE-FULL.md) — все файлы, CREATE flow, глобальное состояние, план развития.
|
**Полный обзор:** [../DOCS/ARCHITECTURE-FULL.md](../DOCS/ARCHITECTURE-FULL.md) — все файлы, CREATE flow, глобальное состояние, план развития.
|
||||||
|
|
||||||
## Сервис
|
## Сервис
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user