""" 스크립트 관리 라우트 및 API """ from flask import Blueprint, render_template, request, jsonify, send_file from flask_login import login_required from pathlib import Path import yaml import json import logging script_manager_bp = Blueprint("script_manager", __name__) # 프로파일 디렉토리 PROFILES_DIR = Path(__file__).parent.parent.parent / "data" / "scripts" / "profiles" UNIFIED_DIR = Path(__file__).parent.parent.parent / "data" / "scripts" / "unified" @script_manager_bp.route("/script_manager") @login_required def index(): """스크립트 관리 메인 페이지""" return render_template("script_manager.html") @script_manager_bp.route("/api/profiles", methods=["GET"]) @login_required def get_profiles(): """모든 프로파일 목록 (카드 뷰용)""" profiles = [] for yaml_file in PROFILES_DIR.glob("*.yaml"): try: with open(yaml_file, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) category = yaml_file.stem.replace('_profiles', '') # mac, guid, server_info for name, config in data.get('profiles', {}).items(): profiles.append({ "name": name, "description": config.get("description", ""), "category": category, "items": _count_items(config, category), "is_default": name == "default" }) except Exception as e: logging.error(f"프로파일 로드 오류 ({yaml_file}): {e}") return jsonify(profiles) @script_manager_bp.route("/api/profiles/", methods=["GET"]) @login_required def get_profiles_by_category(category): """특정 카테고리의 프로파일 목록""" profiles = [] # 파일명 매핑 (server_info -> server_info_profiles.yaml) # category가 mac, guid, server_info 등으로 넘어옴 yaml_file = PROFILES_DIR / f"{category}_profiles.yaml" if not yaml_file.exists(): return jsonify({"error": f"프로파일 파일이 없습니다: {category}"}), 404 try: with open(yaml_file, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) for name, config in data.get('profiles', {}).items(): profiles.append({ "name": name, "description": config.get("description", ""), "category": category, "items": _count_items(config, category), "is_default": name == "default" }) except Exception as e: logging.error(f"프로파일 로드 오류 ({yaml_file}): {e}") return jsonify({"error": str(e)}), 500 return jsonify(profiles) @script_manager_bp.route("/api/profiles//", methods=["GET"]) @login_required def get_profile(category, name): """특정 프로파일 상세 정보 (편집용)""" profile_file = PROFILES_DIR / f"{category}_profiles.yaml" if not profile_file.exists(): return jsonify({"error": "프로파일 파일 없음"}), 404 try: with open(profile_file, 'r', encoding='utf-8') as f: profiles = yaml.safe_load(f)['profiles'] profile = profiles.get(name) if not profile: return jsonify({"error": "프로파일 없음"}), 404 # UI 친화적 형식으로 변환 return jsonify({ "name": name, "description": profile.get("description", ""), "category": category, "config": profile }) except Exception as e: logging.error(f"프로파일 로드 오류: {e}") return jsonify({"error": str(e)}), 500 @script_manager_bp.route("/api/profiles//", methods=["PUT"]) @login_required def update_profile(category, name): """프로파일 업데이트""" profile_file = PROFILES_DIR / f"{category}_profiles.yaml" if not profile_file.exists(): return jsonify({"error": "프로파일 파일 없음"}), 404 try: data = request.json # 기존 프로파일 로드 with open(profile_file, 'r', encoding='utf-8') as f: profiles_data = yaml.safe_load(f) # 업데이트 profiles_data['profiles'][name] = data.get('config', {}) # 저장 with open(profile_file, 'w', encoding='utf-8') as f: yaml.dump(profiles_data, f, allow_unicode=True, default_flow_style=False) return jsonify({"success": True, "message": "프로파일이 저장되었습니다."}) except Exception as e: logging.error(f"프로파일 저장 오류: {e}") return jsonify({"error": str(e)}), 500 @script_manager_bp.route("/api/profiles/", methods=["POST"]) @login_required def create_profile(category): """새 프로파일 생성""" profile_file = PROFILES_DIR / f"{category}_profiles.yaml" if not profile_file.exists(): return jsonify({"error": "프로파일 파일 없음"}), 404 try: data = request.json name = data.get('name') if not name: return jsonify({"error": "프로파일 이름 필요"}), 400 # 기존 프로파일 로드 with open(profile_file, 'r', encoding='utf-8') as f: profiles_data = yaml.safe_load(f) # 중복 체크 if name in profiles_data['profiles']: return jsonify({"error": "이미 존재하는 프로파일"}), 400 # 새 프로파일 추가 profiles_data['profiles'][name] = data.get('config', {}) # 저장 with open(profile_file, 'w', encoding='utf-8') as f: yaml.dump(profiles_data, f, allow_unicode=True, default_flow_style=False) return jsonify({"success": True, "message": "프로파일이 생성되었습니다."}) except Exception as e: logging.error(f"프로파일 생성 오류: {e}") return jsonify({"error": str(e)}), 500 @script_manager_bp.route("/api/profiles//", methods=["DELETE"]) @login_required def delete_profile(category, name): """프로파일 삭제""" profile_file = PROFILES_DIR / f"{category}_profiles.yaml" if not profile_file.exists(): return jsonify({"error": "프로파일 파일 없음"}), 404 try: # default 프로파일은 삭제 불가 if name == "default": return jsonify({"error": "기본 프로파일은 삭제할 수 없습니다."}), 400 # 기존 프로파일 로드 with open(profile_file, 'r', encoding='utf-8') as f: profiles_data = yaml.safe_load(f) # 삭제 if name in profiles_data['profiles']: del profiles_data['profiles'][name] else: return jsonify({"error": "프로파일 없음"}), 404 # 저장 with open(profile_file, 'w', encoding='utf-8') as f: yaml.dump(profiles_data, f, allow_unicode=True, default_flow_style=False) return jsonify({"success": True, "message": "프로파일이 삭제되었습니다."}) except Exception as e: logging.error(f"프로파일 삭제 오류: {e}") return jsonify({"error": str(e)}), 500 @script_manager_bp.route("/api/profiles//export", methods=["GET"]) @login_required def export_profile(category): """프로파일 YAML 파일 다운로드""" name = request.args.get("name", "") if not name: return jsonify({"error": "프로파일 이름 필요"}), 400 profile_file = PROFILES_DIR / f"{category}_profiles.yaml" if not profile_file.exists(): return jsonify({"error": "프로파일 파일 없음"}), 404 try: with open(profile_file, 'r', encoding='utf-8') as f: profiles = yaml.safe_load(f) # 단일 프로파일만 추출 single_profile = { "profiles": { name: profiles['profiles'][name] } } # 임시 파일 생성 temp_dir = Path(__file__).parent.parent.parent / "data" / "temp_zips" temp_dir.mkdir(parents=True, exist_ok=True) export_file = temp_dir / f"{name}_profile.yaml" with open(export_file, 'w', encoding='utf-8') as f: yaml.dump(single_profile, f, allow_unicode=True) return send_file(export_file, as_attachment=True, download_name=f"{name}_profile.yaml") except Exception as e: logging.error(f"프로파일 내보내기 오류: {e}") return jsonify({"error": str(e)}), 500 @script_manager_bp.route("/api/test_racadm", methods=["POST"]) @login_required def test_racadm(): """Racadm 명령어 테스트""" try: import subprocess data = request.json ip = data.get("ip") user = data.get("user", "root") password = data.get("password") command = data.get("command") if not all([ip, user, password, command]): return jsonify({"success": False, "error": "필수 입력값 누락"}), 400 # 명령어 구성 (racadm -r -u -p ) # 보안을 위해 shell=False로 리스트 형태로 전달 full_cmd = ["racadm", "-r", ip, "-u", user, "-p", password] + command.split() logging.info(f"[Test] Executing racadm on {ip}: {command}") # 타임아웃 30초 result = subprocess.run( full_cmd, capture_output=True, text=True, timeout=30, check=False ) return jsonify({ "success": True, "return_code": result.returncode, "output": result.stdout + (result.stderr if result.stderr else "") }) except subprocess.TimeoutExpired: return jsonify({ "success": False, "error": "명령어 실행 시간 초과 (30초)" }) except Exception as e: logging.error(f"[Test] Racadm execution error: {e}") return jsonify({"success": False, "error": str(e)}) def _count_items(profile: dict, category: str) -> int: """프로파일에서 수집할 항목 개수 계산""" count = 0 if category == "mac": count += len(profile.get("nic_patterns", [])) if profile.get("infiniband_slots"): count += len(profile["infiniband_slots"]) if profile.get("include_idrac_mac"): count += 1 elif category == "guid": count += len(profile.get("slot_priority", [])) elif category == "gpu": # GUID slot priority + GPU settings count += len(profile.get("slot_priority", [])) if profile.get("gpu_settings"): count += 1 elif category == "server_info": count += len(profile.get("firmware", [])) count += len(profile.get("bios_settings", [])) count += len(profile.get("idrac_settings", [])) count += len(profile.get("raid_settings", [])) count += len(profile.get("custom_items", [])) return count def register_script_manager(app): """블루프린트 등록""" app.register_blueprint(script_manager_bp)