332 lines
11 KiB
Python
332 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>/<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>/<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>/<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>/<name>/export", methods=["GET"])
|
|
@login_required
|
|
def export_profile(category, name):
|
|
"""프로파일 YAML 파일 다운로드"""
|
|
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
|
|
|
|
|
|
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)})
|
|
|
|
|
|
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)
|