Initial project upload
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
스크립트 관리 라우트 및 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)
|
||||
Reference in New Issue
Block a user