feat: from_stands.py — конвертер STANDS YAML → polygon configs (37 сервисов)

This commit is contained in:
2026-07-31 20:19:54 +04:00
parent 9dddc96704
commit 77f12d2f86
39 changed files with 8664 additions and 3 deletions
+346
View File
@@ -0,0 +1,346 @@
"""
from_stands.py — конвертер STANDS YAML → polygon service config.
Читает YAML из STANDS_DIR, генерирует конфиги в SERVICES_DIR.
Запуск: python from_stands.py <STANDS_DIR> [SERVICES_DIR]
"""
import html
import os
import sys
import yaml
# Расширение для YAML-файлов (может быть .yaml или .yml)
YAML_EXTS = (".yaml", ".yml")
# Поля которые переносятся 1:1 из STANDS YAML
TOP_LEVEL_KEYS = (
"name", "service_id", "service_display_name",
"service_short_name", "service_man",
)
# Стандартные 6 операций инстанса
INSTANCE_OPERATIONS = frozenset({
"create", "modify", "delete", "suspend", "resume", "redeploy",
})
# ---------------------------------------------------------------------------
# Утилиты
# ---------------------------------------------------------------------------
def _ue(s):
"""html.unescape строки."""
if isinstance(s, str):
return html.unescape(s)
return s
def _pluralize(word):
"""Простейшая плюрализация: user→users, database→databases."""
if word.endswith("s"):
return word
if word.endswith("y") and word[-2] not in "aeiou":
return word[:-1] + "ies"
return word + "s"
# ---------------------------------------------------------------------------
# Конвертация параметров
# ---------------------------------------------------------------------------
def _convert_param(p, is_modifiable_default=False):
"""Конвертирует один параметр STANDS → polygon cfsParam.
Возвращает dict с полями:
svcOperationCfsParamId, svcOperationCfsParam, dataType,
isRequired, defaultValue, valueList, isModifiable,
dataDescriptor (если есть sub_params)
"""
dt = _ue(p.get("data_type", "string"))
required = p.get("required", False)
default = _ue(str(p.get("default", "")))
is_modifiable = p.get("is_modifiable", is_modifiable_default)
out = {
"svcOperationCfsParamId": p["id"],
"svcOperationCfsParam": p["code"],
"dataType": dt,
"isRequired": required,
"defaultValue": default,
"isModifiable": is_modifiable,
}
# valueList — может быть None, тогда не включаем (input, не select)
vl = p.get("value_list")
if vl is not None:
out["valueList"] = [_ue(str(v)) for v in vl]
# refSvcId
ref = p.get("refSvcId")
if ref is not None:
out["refSvcId"] = ref
# dataDescriptor из sub_params
sub = p.get("sub_params")
if sub:
out["dataDescriptor"] = _convert_sub_params(sub)
return out
def _convert_sub_params(sub_params):
"""Конвертирует sub_params → dataDescriptor.
Формат выхода:
{code: {dataType, defaultValue, isRequired, valueList?, isModifiable?}}
"""
dd = {}
for sp in sub_params:
code = sp["code"]
dt = _ue(sp.get("data_type", "string"))
default = _ue(str(sp.get("default", "")))
entry = {
"dataType": dt,
"defaultValue": default,
"isRequired": sp.get("required", False),
"isModifiable": sp.get("is_modifiable", False),
}
vl = sp.get("value_list")
if vl is not None:
entry["valueList"] = [_ue(str(v)) for v in vl]
# Рекурсия: sub_params внутри sub_params (редко, но возможно)
subsub = sp.get("sub_params")
if subsub:
entry["dataDescriptor"] = _convert_sub_params(subsub)
dd[code] = entry
return dd
# ---------------------------------------------------------------------------
# Конвертация операций
# ---------------------------------------------------------------------------
def _convert_operations(operations):
"""Конвертирует список операций STANDS → polygon.
Возвращает (ops_list, state_out_template):
ops_list — список operation-диктов
state_out_template — {plural: {}} для subresource create-операций
"""
ops_list = []
state_out_template = {}
for op in operations:
op_id = op["id"]
op_name = op.get("name", "")
op_kind = op.get("kind", "instance")
op_action = op.get("action", "")
ops_list.append({
"svcOperationId": op_id,
"operation": op_name,
"kind": op_kind,
"action": op_action,
})
# subresource create → добавить в state_out_template
if op_kind == "subresource" and op_action == "create":
# subresource имя из operation name: create_user → user
sub_name = op_name.replace("create_", "")
plural = _pluralize(sub_name)
state_out_template[plural] = {}
return ops_list, state_out_template
# ---------------------------------------------------------------------------
# Конвертация одного сервиса
# ---------------------------------------------------------------------------
def convert_service(stands_yaml_path):
"""Читает STANDS YAML, возвращает polygon-конфиг как dict."""
with open(stands_yaml_path, "r", encoding="utf-8") as f:
raw = yaml.safe_load(f)
if not raw:
return None
out = {}
# 1. Поля верхнего уровня (1:1)
for key in TOP_LEVEL_KEYS:
val = raw.get(key)
if val is not None:
if isinstance(val, str):
val = _ue(val)
out[key] = val
# 2. lifecycle
lc = raw.get("lifecycle", {})
if lc:
out["lifecycle"] = {
"suspendOnDestroyDefault": lc.get("suspend_on_destroy_default", False),
"adoptExistingOnCreateDefault": lc.get("adopt_existing_on_create_default", False),
}
# 3. Операции
operations = raw.get("operations", [])
ops_list, state_out_template = _convert_operations(operations)
out["operations"] = ops_list
out["stateOut"] = state_out_template
# 4. cfsParams — собираем из ВСЕХ операций, dedup по svcOperationCfsParamId
cfs_params = {}
cfs_params_by_op = {} # op_id → [param, ...]
for op in operations:
op_id = op["id"]
params_for_op = []
for p in op.get("params", []):
pid = p["id"]
if pid not in cfs_params:
is_mod = p.get("is_modifiable", op.get("action") != "create")
cfs_params[pid] = _convert_param(p, is_modifiable_default=is_mod)
params_for_op.append(pid)
cfs_params_by_op[op_id] = params_for_op
out["cfsParams"] = list(cfs_params.values())
out["cfsParamsByOp"] = cfs_params_by_op
# 5. stateParams — дефолты из create-операции (включая map-fixed)
state_params = {}
create_op = None
for op in operations:
if op.get("action") == "create" and op.get("kind") == "instance":
create_op = op
break
if create_op:
for p in create_op.get("params", []):
code = p["code"]
default = _ue(str(p.get("default", "")))
dt = _ue(p.get("data_type", "string"))
sub = p.get("sub_params")
if sub:
# map-fixed / map с sub_params — генерируем JSON из defaults
state_params[code] = _build_default_json(sub)
else:
state_params[code] = default
out["stateParams"] = state_params
return out
def _build_default_json(sub_params):
"""Строит JSON-строку из defaults sub_params."""
parts = []
for sp in sub_params:
code = sp["code"]
default = _ue(str(sp.get("default", "")))
dt = _ue(sp.get("data_type", "string"))
subsub = sp.get("sub_params")
if subsub:
val = _build_default_json(subsub)
elif "bool" in dt:
val = default.lower() if default.lower() in ("true", "false") else "false"
elif "int" in dt or "integer" in dt:
try:
int(default)
val = default
except (ValueError, TypeError):
val = "0"
else:
# string — quoted
escaped = default.replace('"', '\\"')
val = f'"{escaped}"'
parts.append(f'"{code}": {val}')
return "{" + ", ".join(parts) + "}"
# ---------------------------------------------------------------------------
# Точка входа
# ---------------------------------------------------------------------------
def main():
if len(sys.argv) < 2:
print("Usage: python from_stands.py <STANDS_DIR> [SERVICES_DIR]")
print(" STANDS_DIR — путь к STANDS YAML (напр. ../STANDS/test/resources_yaml)")
print(" SERVICES_DIR — путь для выхода (по умолчанию ./services)")
sys.exit(1)
stands_dir = sys.argv[1]
services_dir = sys.argv[2] if len(sys.argv) > 2 else os.path.join(os.path.dirname(__file__) or ".", "services")
if not os.path.isdir(stands_dir):
print(f"ERROR: STANDS_DIR not found: {stands_dir}")
sys.exit(1)
os.makedirs(services_dir, exist_ok=True)
# Найти все YAML-файлы
yaml_files = sorted(
f for f in os.listdir(stands_dir)
if f.lower().endswith(YAML_EXTS)
)
if not yaml_files:
print(f"No YAML files found in {stands_dir}")
sys.exit(1)
print(f"Found {len(yaml_files)} YAML files in {stands_dir}")
print(f"Output: {services_dir}/")
print()
converted = 0
errors = 0
for fname in yaml_files:
src = os.path.join(stands_dir, fname)
dst = os.path.join(services_dir, fname)
try:
cfg = convert_service(src)
if cfg is None:
print(f" SKIP {fname}: empty YAML")
continue
with open(dst, "w", encoding="utf-8") as f:
yaml.dump(
cfg, f,
allow_unicode=True,
default_flow_style=False,
sort_keys=False,
width=200,
)
# Быстрая проверка результата
svc_name = cfg.get("name", "?")
ops_count = len(cfg.get("operations", []))
params_count = len(cfg.get("cfsParams", []))
state_out = list(cfg.get("stateOut", {}).keys())
subresources = " +".join(state_out) if state_out else ""
print(f" OK {fname:40s} svc={svc_name:20s} ops={ops_count} params={params_count} stateOut={subresources}")
converted += 1
except Exception as e:
print(f" FAIL {fname}: {e}")
errors += 1
print()
print(f"Done: {converted} converted, {errors} errors")
if __name__ == "__main__":
main()