""" from_stands.py — конвертер STANDS YAML → polygon service config. Читает YAML из STANDS_DIR, генерирует конфиги в SERVICES_DIR. Запуск: python from_stands.py [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 [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()