350 lines
11 KiB
Python
350 lines
11 KiB
Python
"""
|
||
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
|
||
|
||
|
||
from utils.pluralize import pluralize as _pluralize
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Конвертация параметров
|
||
# ---------------------------------------------------------------------------
|
||
|
||
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
|
||
|
||
return convert_service_from_dict(raw)
|
||
|
||
|
||
def convert_service_from_dict(raw):
|
||
"""Конвертирует STANDS YAML (уже распарсенный dict) → polygon-конфиг.
|
||
|
||
Отдельная функция для тестов — не требует файла.
|
||
"""
|
||
|
||
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()
|