diff --git a/site/app.py b/site/app.py index 620531f..179c804 100644 --- a/site/app.py +++ b/site/app.py @@ -6,7 +6,7 @@ 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.80" +VERSION = "1.0.81" 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") diff --git a/site/operations/get_params.py b/site/operations/get_params.py new file mode 100644 index 0000000..6aec8a7 --- /dev/null +++ b/site/operations/get_params.py @@ -0,0 +1,82 @@ +""" +Операция: получить параметры операции с ТЕКУЩИМИ значениями инстанса. + +Источник правды — Nubes API: + 1. GET /instances/{instanceUid} → state.params (текущие значения, ключ = код параметра) + 2. GET /instanceOperations/default/{opId} (шаблон: код, тип, valueList, dataDescriptor) + 3. Смержить: defaultValue = state.params["код"] ?? template.defaultValue +""" + + +def get_params_with_current_values(client, op_id, instance_uid): + """ + Возвращает список параметров (dict) для заполнения формы операции. + + Каждый элемент: + svcOperationCfsParamId — числовой ID параметра + name — код параметра (напр. "durationMs") + dataType — тип (напр. "integer >= 0", "boolean", "string") + isRequired — обязательный? + defaultValue — ТЕКУЩЕЕ значение из state.params или шаблонный default + valueList — список допустимых значений (если есть) + dataDescriptor — вложенная структура для map-параметров (если есть) + """ + + # --- Шаг 1: текущие значения из инстанса --- + inst_data = client.get(f"/instances/{instance_uid}") + state_params = inst_data.get("instance", {}).get("state", {}).get("params", {}) or {} + # state_params — dict вида {"whereFail": "1", "durationMs": "0", "bodymessage": "mess", ...} + + # --- Шаг 2: шаблон параметров операции --- + tmpl_data = client.get(f"/instanceOperations/default/{op_id}") + tmpl_params = tmpl_data.get("svcOperation", {}).get("cfsParams", []) or [] + + # --- Шаг 3: слияние --- + result = [] + for p in tmpl_params: + pid = p["svcOperationCfsParamId"] # числовой ID (напр. 198) + code = p.get("svcOperationCfsParam", "") # код/имя (напр. "durationMs") + data_type = p.get("dataType", "") + required = p.get("isRequired", False) + tmpl_default = p.get("defaultValue") # шаблонный default + value_list = p.get("valueList") + dd = p.get("dataDescriptor") # вложенная структура (map-параметры) + + # Берём текущее значение из state.params по коду, + # если нет — используем шаблонный defaultValue. + current = state_params.get(code) + if current is not None: + # Приводим к строке — фронт ожидает строки в полях ввода. + # bool → "true"/"false", dict → JSON, остальное → str() + if isinstance(current, bool): + current = "true" if current else "false" + elif isinstance(current, dict): + import json + current = json.dumps(current) + else: + current = str(current) + merged_value = current + else: + merged_value = tmpl_default + + result.append({ + "svcOperationCfsParamId": pid, + "name": code, + "dataType": data_type, + "isRequired": required, + "defaultValue": merged_value, + "valueList": value_list, + "dataDescriptor": ( + { + k: { + "dataType": v.get("dataType", ""), + "valueList": v.get("valueList", ""), + "isRequired": v.get("isRequired", False), + } + for k, v in dd.items() + } + if isinstance(dd, dict) else None + ), + }) + + return result diff --git a/site/routes/api_test.py b/site/routes/api_test.py index 3086b50..2499c15 100644 --- a/site/routes/api_test.py +++ b/site/routes/api_test.py @@ -8,6 +8,7 @@ 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 +from operations.get_params import get_params_with_current_values bp = Blueprint("api_test", __name__) @@ -129,45 +130,20 @@ def api_operations(svc_id): @bp.route("/api/params/") def api_params(op_id): + """Параметры операции. Если передан instanceUid — с текущими значениями из API.""" try: instance_uid = request.args.get("instanceUid") - op_name = request.args.get("opName") - _log(f"[api_params] op_id={op_id} instanceUid={instance_uid!r} opName={op_name!r}") + _log(f"[api_params] op_id={op_id} instanceUid={instance_uid!r}") - if instance_uid and op_name: - _log(f"[api_params] PREVIEW branch: creating operation for instance={instance_uid}") - # ВАЖНО: без svcOperationId — API сам подтянет текущие paramValue из инстанса - # (с svcOperationId — возвращает None для всех paramValue) - op_payload = {"instanceUid": instance_uid, "operation": op_name} - op_resp = _client().post("/instanceOperations", op_payload) - preview_op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", "")) - _log(f"[api_params] preview_op_uid={preview_op_uid!r}") - if not preview_op_uid: - return jsonify({"error": "Не удалось создать preview-операцию"}), 500 + if instance_uid: + # Текущие значения: GET /instances/{uid} → state.params + шаблон → слияние + result = get_params_with_current_values(_client(), op_id, instance_uid) + _log(f"[api_params] MERGED returning {len(result)} params from state.params") + for p in result: + _log(f"[api_params] {p['name']}: defaultValue={p['defaultValue']!r}") + return jsonify({"params": result}) - op_data = _client().get(f"/instanceOperations/{preview_op_uid}") - cfs_params = op_data.get("instanceOperation", {}).get("cfsParams", []) - _log(f"[api_params] cfsParams count={len(cfs_params)}") - - result = [] - for p in cfs_params: - dd = p.get("dataDescriptor") - pv = p.get("paramValue") - dv = p.get("defaultValue") - _log(f"[api_params] {p.get('svcOperationCfsParam')}: paramValue={pv!r} defaultValue={dv!r}") - result.append({ - "svcOperationCfsParamId": p["svcOperationCfsParamId"], - "name": p.get("svcOperationCfsParam", ""), - "dataType": p.get("dataType", ""), - "isRequired": p.get("isRequired", False), - "defaultValue": pv, # текущее значение из инстанса - "valueList": p.get("valueList"), - "dataDescriptor": {k: {"dataType": v.get("dataType",""), "valueList": v.get("valueList",""), "isRequired": v.get("isRequired", False)} for k, v in dd.items()} if isinstance(dd, dict) else None, - }) - _log(f"[api_params] PREVIEW returning {len(result)} params") - return jsonify({"params": result, "previewOpUid": preview_op_uid}) - - # Без instanceUid — шаблонные значения по умолчанию + # Без instanceUid — шаблонные значения по умолчанию (CREATE) _log(f"[api_params] DEFAULT branch: template defaults") data = _client().get(f"/instanceOperations/default/{op_id}") params = data["svcOperation"]["cfsParams"] @@ -204,7 +180,6 @@ def api_test(): params = data.get("params", {}) instance_uid = data.get("instanceUid") display_name = data.get("displayName") or "" - preview_op_uid = data.get("previewOpUid") client = _client() @@ -247,14 +222,7 @@ def api_test(): if not instance_uid: return jsonify({"status": "FAIL", "error": "Нет instanceUid"}), 400 - if preview_op_uid: - # Переиспользовать preview-операцию (уже создана в api_params) - op_uid = preview_op_uid - 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") - elif op_name == "redeploy": + 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", "")) diff --git a/site/templates/index.html b/site/templates/index.html index 27f6703..96f3bdd 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -206,10 +206,9 @@ function showParams(opId,opName){ document.getElementById('test-status').textContent=''; const isCreate=opName==='create'; - const qs=isCreate?'' : `?instanceUid=${selectedInst}&opName=${opName}`; + const qs=isCreate?'' : `?instanceUid=${selectedInst}`; fetch('/api/params/'+opId+qs).then(r=>r.json()).then(data=>{ - const params=data.params||data; // data.params в новом формате, fallback на старый массив - if(data.previewOpUid) selectedOp.previewOpUid=data.previewOpUid; + const params=data.params||data; const form=document.getElementById('params-form'); const displayNameValue=isCreate?makeCreateDisplayName():''; form.innerHTML=(opName==='create'?`
${AUTOTEST_PREFIX}
`:'') @@ -288,8 +287,7 @@ async function executeOp(params){ svcOperationId:selectedOp.opId, params, instanceUid:selectedInst||'', - displayName:isCreate ? displayName : '', - previewOpUid:selectedOp.previewOpUid||'' + displayName:isCreate ? displayName : '' })}); const d=await r.json(); if(d.status==='FAIL'){document.getElementById('test-status').innerHTML=` FAIL ${d.error||''}`;btn.disabled=false;return;}