Files
2026-05-05 17:14:11 +09:00

326 lines
11 KiB
Python

"""
스크립트 관리 라우트 및 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/<category>", 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/<category>/<path:name>", 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/<category>/<path:name>", 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/<category>", 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/<category>/<path:name>", 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/<category>/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 <IP> -u <USER> -p <PW> <CMD>)
# 보안을 위해 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)