Files

256 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Terraform-совместимые операции с параметрами: нормализация, refSvc-резолв, отправка.
Эти шаги воспроизводят логику Terraform-провайдера Nubes при создании/изменении
инстанса через личный кабинет:
Шаг 3: Получить cfsParams шаблон операции
Шаг 4: resolveRefSvc — подставить UUID инстансов для refSvcId-параметров
Шаг 5: Отправить пользовательские значения параметров (те что ввёл пользователь)
Шаг 6: Отправить default-значения для незаполненных параметров
Шаг 7: validate-cfs — проверить что все обязательные параметры заполнены
Используется из executor.py (через send_params_terraform) — и ручной режим, и сценарий.
"""
import json
from operations.get_instances import get_instances
# Параметры, значения которых НЕЛЬЗЯ сохранять в БД в открытом виде.
# При сохранении в runs.params их значения заменяются на "***".
REDACT_KEYS = {"password", "secret", "token", "key", "privatekey", "private_key", "passwd", "pass"}
def redact_params(params):
"""Заменить значения секретных полей на *** перед сохранением в БД.
Проверяет КЛЮЧИ параметров (не значения!) на вхождение секретных слов.
Пример: params={"adminPassword": "secret123"} → {"adminPassword": "***"}
Args:
params: dict — {param_id: value}
Returns:
dict с замаскированными секретами."""
if not params:
return params
result = {}
for k, v in params.items():
# Проверяем ключ (имя параметра) на敏感ные слова
if any(r in str(k).lower() for r in REDACT_KEYS):
result[k] = "***"
else:
result[k] = v
return result
def normalize_value(val, data_type, data_descriptor):
"""normalizeUniversalValueV6 — Terraform-эквивалент нормализации значения.
Что делает: приводит пустые значения к «нулю» соответствующего типа.
Terraform не принимает пустую строку для integer-параметра — нужно "0".
Для map-параметров собирает JSON из sub-param defaults.
Args:
val: str — значение параметра (может быть "", "null", ...)
data_type: str — тип параметра ("integer >= 0", "boolean", "map(...)", ...)
data_descriptor: dict|None — вложенная структура для map-параметров
Returns:
str — нормализованное значение."""
# Очистка: strip + специальные значения null / ""
val = (val or "").strip()
if val.lower() == "null" or val == '""':
val = ""
dt = (data_type or "").lower()
# Map-параметры с dataDescriptor:
# Если значение пустое — собираем JSON из sub-param defaults.
# Пример: dataDescriptor={"host": {defaultValue: "localhost"}, "port": {defaultValue: "5432"}}
# → "{\"host\":\"localhost\",\"port\":\"5432\"}"
if ("map" in dt) and isinstance(data_descriptor, dict) and data_descriptor:
if not val or val == "{}":
sub_obj = {}
for sk, sv in data_descriptor.items():
sd = sv.get("defaultValue")
# default может быть любым типом — приводим к строке для JSON
sub_obj[sk] = str(sd) if sd is not None else ""
return json.dumps(sub_obj)
return val # значение уже заполнено — не трогаем
# Значение не пустое — возвращаем как есть
if val:
return val
# Пустое значение — подставляем «ноль» типа:
if "array" in dt:
return "[]"
if "map" in dt or "json" in dt:
return "{}"
if "integer" in dt or "int" in dt:
return "0"
if "boolean" in dt or "bool" in dt:
return "false"
# string или неизвестный тип — пустая строка
return val
def resolve_ref_svc(client, cfs_params, params):
"""resolveRefSvcParamValues — Terraform step 4.
Некоторые параметры имеют тип «ссылка на другой сервис» (refSvcId).
Например: параметр «externalIp» ссылается на сервис External IP (svcId=25).
Если пользователь оставил такой параметр пустым — мы должны найти
существующий инстанс этого сервиса и подставить его UUID.
Это работает в двух местах:
1. Верхнеуровневый refSvcId — прямо в параметре
2. Вложенный refSvcId — внутри dataDescriptor (для map-параметров)
Args:
client: HttpClient
cfs_params: list — cfsParams из шаблона операции
params: dict — {param_id: value} — МУТИРУЕТСЯ (добавляем UUID)
Returns: None (мутирует params)."""
# instances загружаем ЛЕНИВО — только если нужен refSvc
instances = None
for p in cfs_params:
pid = str(p["svcOperationCfsParamId"])
# ── Уровень 1: верхнеуровневый refSvcId ──
top_ref = p.get("refSvcId")
if top_ref:
# Проверяем: параметр пустой?
cur = (params.get(pid) or "").strip()
if not cur or cur == '""':
# Загружаем инстансы если ещё не загружены
if instances is None:
try:
instances = get_instances(client)
except Exception:
return # не можем загрузить — пропускаем
# Ищем первый НЕ deleted инстанс с нужным serviceId
match = next((i for i in instances
if i.get("serviceId") == top_ref
and i.get("explainedStatus") not in ("deleted",)), None)
if match:
params[pid] = match["instanceUid"]
# ── Уровень 2: вложенный refSvcId в dataDescriptor ──
dd = p.get("dataDescriptor")
if not isinstance(dd, dict):
continue # нет dataDescriptor — нечего проверять
# Парсим текущее значение параметра как JSON (map-параметр)
user_val = {}
if pid in params:
try:
user_val = json.loads(params[pid]) if params[pid] else {}
except (json.JSONDecodeError, ValueError):
user_val = {}
# Проверяем каждый sub-ключ dataDescriptor на refSvcId
for sk, sv in dd.items():
ref_id = sv.get("refSvcId")
if not ref_id:
continue # не refSvc-параметр
# Текущее значение sub-ключа
cur = user_val.get(sk, "")
if cur and cur != '""':
continue # уже заполнено — не трогаем
# Загружаем инстансы лениво
if instances is None:
try:
instances = get_instances(client)
except Exception:
return
# Ищем подходящий инстанс
match = next((i for i in instances
if i.get("serviceId") == ref_id
and i.get("explainedStatus") not in ("deleted",)), None)
if match:
user_val[sk] = match["instanceUid"]
# Если были изменения — обновляем значение параметра
if user_val:
params[pid] = json.dumps(user_val)
def send_params_terraform(client, op_uid, params):
"""Terraform steps 3-7: cfsParams → resolveRefSvc → send user → send defaults → validate.
Полный алгоритм отправки параметров операции:
3. GET /instanceOperations/{opUid}?fields=cfsParams — получить шаблон
4. resolveRefSvc — подставить UUID для refSvcId-параметров
5. Отправить пользовательские значения (те что переданы в params)
6. Отправить default-значения для НЕзаполненных параметров
(нормализуя через normalize_value)
7. GET /instanceOperations/{opUid}/validate-cfs — финальная проверка
Args:
client: HttpClient
op_uid: str — UUID операции
params: dict — {numeric_param_id: string_value} — пользовательские значения
Raises:
Exception при ошибке валидации (кроме пустого JSON-ответа — это ок)."""
# Шаг 3: получить cfsParams шаблон операции
op_det = client.get(f"/instanceOperations/{op_uid}?fields=cfsParams")
cfs_params = op_det.get("instanceOperation", {}).get("cfsParams", [])
# Шаг 4: resolveRefSvc — мутирует params, подставляя UUID
resolve_ref_svc(client, cfs_params, params)
# Шаг 5: отправить пользовательские значения
# sent — множество paramId, чтобы не отправить повторно в шаге 6
sent = set()
for pid, pval in params.items():
client.post("/instanceOperationCfsParams", {
"instanceOperationUid": op_uid,
"svcOperationCfsParamId": int(pid),
"paramValue": str(pval)
})
sent.add(int(pid))
# Шаг 6: отправить default-значения для незаполненных параметров
for p in cfs_params:
pid = p["svcOperationCfsParamId"]
if pid in sent:
continue # пользователь уже заполнил — не перезаписываем
# Приоритет значений: paramValue → defaultValue → ""
val = p.get("paramValue")
if val is None:
val = p.get("defaultValue")
if val is None:
val = ""
# Нормализуем под тип (пустой integer → "0", map → "{}")
val = normalize_value(str(val), p.get("dataType", ""), p.get("dataDescriptor"))
client.post("/instanceOperationCfsParams", {
"instanceOperationUid": op_uid,
"svcOperationCfsParamId": pid,
"paramValue": val
})
# Шаг 7: validate-cfs — финальная проверка
# Если параметры невалидны — API вернёт ошибку → исключение
# Если ответ 200 с пустым телом — это ОК (значит валидация прошла).
# HttpClient.get() для пустого тела возвращает {} без ошибок.
# Но если тело не-JSON — будет JSONDecodeError, это тоже ОК для validate-cfs.
try:
client.get(f"/instanceOperations/{op_uid}/validate-cfs")
except json.JSONDecodeError:
pass # пустой/не-JSON ответ — норма для validate-cfs (успех)
# HTTPError (4xx/5xx) пробрасывается выше — это реальная ошибка валидации